编写区块链游戏学智能合约 教程2:僵尸攻击人类
该教程来自 CryptoZombies
网址:https://cryptozombies.io/zh/course/
CryptoZombies 是个在编游戏的过程中学习 Solidity 智能协议语言的互动教程。编游戏的同时学习以太坊的智能协议。关键是它免费。
本课会使用到一些高级的 Solidity 概念.
1. 映射(Mapping)和地址(Address)
我们通过给数据库中的僵尸指定“主人”, 来支持“多玩家”模式。
如此一来,我们需要引入2个新的数据类型:mapping(映射) 和 address(地址)。
Addresses (地址)
以太坊区块链由 account (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 以太
(在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。
每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
(这是 CryptoZombies 团队的地址,如果你喜欢 CryptoZombies 的话,请打赏我们一些以太币!)
我们将在后面的课程中介绍地址的细节,现在你只需要了解地址属于特定用户(或智能合约)的。
所以我们可以指定“地址”作为僵尸主人的 ID。当用户通过与我们的应用程序交互来创建新的僵尸时,新僵尸的所有权被设置到调用者的以太坊地址下。
Mapping(映射)
在第1课中,我们看到了 结构体
和 数组
。 映射
是另一种在 Solidity 中存储有组织数据的方法。
映射是这样定义的:
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;
映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address
,值是一个 uint
,在第二个例子中,键是一个uint
,值是一个 string
。
2. Msg.sender
现在有了一套映射来记录僵尸的所有权了,我们可以修改 _createZombie
方法来运用它们。
为了做到这一点,我们要用到 msg.sender
。
msg.sender
在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender
,它指的是当前调用者(或智能合约)的 address
。
注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以
msg.sender
总是存在的。
以下是使用 msg.sender
来更新 mapping
的例子:
mapping (address => uint) favoriteNumber;function setMyNumber(uint _myNumber) public {// 更新我们的 `favoriteNumber` 映射来将 `_myNumber`存储在 `msg.sender`名下favoriteNumber[msg.sender] = _myNumber;// 存储数据至映射的方法和将数据存储在数组相似
}function whatIsMyNumber() public view returns (uint) {// 拿到存储在调用者地址名下的值// 若调用者还没调用 setMyNumber, 则值为 `0`return favoriteNumber[msg.sender];
}
在这个小小的例子中,任何人都可以调用 setMyNumber
在我们的合约中存下一个 uint
并且与他们的地址相绑定。 然后,他们调用 whatIsMyNumber
就会返回他们存储的 uint
。
使用 msg.sender
很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。
3. Require
在第一课中,我们成功让用户通过调用 createRandomZombie
函数 并输入一个名字来创建新的僵尸。 但是,如果用户能持续调用这个函数来创建出无限多个僵尸加入他们的军团,这游戏就太没意思了!
于是,我们作出限定:每个玩家只能调用一次这个函数。 这样一来,新玩家可以在刚开始玩游戏时通过调用它,为其军团创建初始僵尸。
我们怎样才能限定每个玩家只调用一次这个函数呢?
答案是使用require
。require
使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:
function sayHiToVitalik(string _name) public returns (string) {// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较// 两字符串的 keccak256 哈希值来进行判断)require(keccak256(_name) == keccak256("Vitalik"));// 如果返回 true, 运行如下语句return "Hi!";
}
如果你这样调用函数 sayHiToVitalik(“Vitalik”)
,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。
因此,在调用一个函数之前,用 require
验证前置条件是非常有必要的。
4. 继承(Inheritance)
我们的游戏代码越来越长。 当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。
有个让 Solidity 的代码易于管理的功能,就是合约 inheritance
(继承):
contract Doge {function catchphrase() public returns (string) {return "So Wow CryptoDoge";}
}contract BabyDoge is Doge {function anotherCatchphrase() public returns (string) {return "Such Moon BabyDoge";}
}
由于 BabyDoge
是从 Doge
那里 inherits
(继承)过来的。 这意味着当你编译和部署了 BabyDoge
,它将可以访问 catchphrase()
和 anotherCatchphrase()
和其他我们在 Doge
中定义的其他公共函数。
这可以用于逻辑继承(比如表达子类的时候,Cat
是一种 Animal
)。 但也可以简单地将类似的逻辑组合到不同的合约中以组织代码。
5. 引入(Import)
在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import
语句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {}
这样当我们在合约(contract)目录下有一个名为 someothercontract.sol
的文件( ./
就是同一目录的意思),它就会被编译器导入。
6. Storage与Memory
在 Solidity 中,有两个地方可以存储变量 —— storage
或 memory
。
Storage
变量是指永久存储在区块链中的变量。Memory
变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的结构体
和 数组
时:
contract SandwichFactory {struct Sandwich {string name;string status;}Sandwich[] sandwiches;function eatSandwich(uint _index) public {// Sandwich mySandwich = sandwiches[_index];// ^ 看上去很直接,不过 Solidity 将会给出警告// 告诉你应该明确在这里定义 `storage` 或者 `memory`。// 所以你应该明确定义 `storage`Sandwich storage mySandwich = sandwiches[_index];// 这样 `mySandwich` 是指向 `sandwiches[_index]`的指针// 在存储里,另外 ``` // mySandwich.status = "Eaten!";// 这将永久把 `sandwiches[_index]` 变为区块链上的存储// 如果你只想要一个副本,可以使用`memory`:Sandwich memory anotherSandwich = sandwiches[_index + 1];// ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了// 另外anotherSandwich.status = "Eaten!";// ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响// 不过你可以这样做:sandwiches[_index + 1] = anotherSandwich;// ...如果你想把副本的改动保存回区块链存储}
}
如果你还没有完全理解究竟应该使用哪一个,也不用担心 —— 在本教程中,我们将告诉你何时使用 storage
或是 memory
,并且当你不得不使用到这些关键字的时候,Solidity 编译器也发警示提醒你的。
现在,只要知道在某些场合下也需要你显式地声明 storage
或 memory
就够了!
7. internal 和 external
除 public
和 private
属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal
(内部) 和 external
(外部)。
internal
和 private
类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。(嘿,这听起来正是我们想要的那样!)。
external
与public
类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external
和 public
。
声明函数 internal
或 external
类型的语法,与声明 private
和 public
类 型相同:
contract Sandwich {uint private sandwichesEaten = 0;function eat() internal {sandwichesEaten++;}
}contract BLT is Sandwich {uint private baconSandwichesEaten = 0;function eatWithBacon() public returns (string) {baconSandwichesEaten++;// 因为eat() 是internal 的,所以我们能在这里调用eat();}
}
8. 与其他合约的交互
如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface
(接口)。
先举一个简单的栗子。 假设在区块链上有这么一个合约:
contract LuckyNumber {mapping(address => uint) numbers;function setNum(uint _num) public {numbers[msg.sender] = _num;}function getNum(address _myAddress) public view returns (uint) {return numbers[_myAddress];}
}
这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了。
现在假设我们有一个外部合约,使用 getNum
函数可读取其中的数据。
首先,我们定义LuckyNumber
合约的 interface
:
contract NumberInterface {function getNum(address _myAddress) public view returns (uint);
}
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数 —— 在本例中为getNum
—— 在其中我们没有使用到任何其他的函数或状态变量。
其次,我们并没有使用大括号({
和 }
)定义函数体,我们单单用分号(;
)结束了函数声明。这使它看起来像一个合约框架。
编译器就是靠这些特征认出它是一个接口的。
在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值.
9. 使用接口
继续前面 NumberInterface
的例子,我们既然将接口定义为:
contract NumberInterface {function getNum(address _myAddress) public view returns (uint);
}
我们可以在合约中这样使用:
contract MyContract {address NumberInterfaceAddress = 0xab38...;// 这是FavoriteNumber合约在以太坊上的地址NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);// 现在变量 numberContract 指向另一个合约对象function someFunction() public {// 现在我们可以调用在那个合约中声明的 `getNum`函数:uint num = numberContract.getNum(msg.sender);// ...在这儿使用 `num`变量做些什么}
}
通过这种方式,只要将您合约的可见性设置为public
(公共)或external
(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。
10. 处理多返回值
function multipleReturns() internal returns(uint a, uint b, uint c) {return (1, 2, 3);
}function processMultipleReturns() external {uint a;uint b;uint c;// 这样来做批量赋值:(a, b, c) = multipleReturns();
}// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {uint c;// 可以对其他字段留空:(,,c) = multipleReturns();
}
11. if 语句
if语句的语法在 Solidity 中,与在 JavaScript 中差不多:
function eatBLT(string sandwich) public {// 看清楚了,当我们比较字符串的时候,需要比较他们的 keccak256 哈希码if (keccak256(sandwich) == keccak256("BLT")) {eat();}
}
下面就是这节课完整的代码,应用了上面介绍的知识。
zombiefactory.sol
pragma solidity ^0.4.19;contract ZombieFactory {event NewZombie(uint zombieId, string name, uint dna);uint dnaDigits = 16;uint dnaModulus = 10 ** dnaDigits;struct Zombie {string name;uint dna;}Zombie[] public zombies;mapping (uint => address) public zombieToOwner;mapping (address => uint) ownerZombieCount;function _createZombie(string _name, uint _dna) internal {uint id = zombies.push(Zombie(_name, _dna)) - 1;zombieToOwner[id] = msg.sender;ownerZombieCount[msg.sender]++;NewZombie(id, _name, _dna);}function _generateRandomDna(string _str) private view returns (uint) {uint rand = uint(keccak256(_str));return rand % dnaModulus;}function createRandomZombie(string _name) public {require(ownerZombieCount[msg.sender] == 0);uint randDna = _generateRandomDna(_name);randDna = randDna - randDna % 100;_createZombie(_name, randDna);}
}
zombiefeeding.sol
pragma solidity ^0.4.19;import "./zombiefactory.sol";contract KittyInterface {function getKitty(uint256 _id) external view returns (bool isGestating,bool isReady,uint256 cooldownIndex,uint256 nextActionAt,uint256 siringWithId,uint256 birthTime,uint256 matronId,uint256 sireId,uint256 generation,uint256 genes);
}contract ZombieFeeding is ZombieFactory {address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;KittyInterface kittyContract = KittyInterface(ckAddress);function feedAndMultiply(uint _zombieId, uint _targetDna, string species) public {require(msg.sender == zombieToOwner[_zombieId]);Zombie storage myZombie = zombies[_zombieId];_targetDna = _targetDna % dnaModulus;uint newDna = (myZombie.dna + _targetDna) / 2;if (keccak256(species) == keccak256("kitty")) {newDna = newDna - newDna % 100 + 99;}_createZombie("NoName", newDna);}function feedOnKitty(uint _zombieId, uint _kittyId) public {uint kittyDna;(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);feedAndMultiply(_zombieId, kittyDna, "kitty");}
}
ZombieFactory.sol 文件。
- 为了存储僵尸的所有权,用到两个映射:一个记录僵尸拥有者的地址
zombieToOwner
,另一个记录某地址所拥有僵尸的数量ownerZombieCount
。
然后在创建僵尸的方法里对上面两个变量赋值
zombieToOwner[id] = msg.sender;ownerZombieCount[msg.sender]++;
id
是僵尸id ,msg.sender
是调用者地址.
- 为了确保每个用户只能创建一个僵尸,我们在
createRandomZombie
的前面放置require
语句
require(ownerZombieCount[msg.sender] == 0);
zombiefeeding.sol文件。
通过吃 CryptoKitties 生成一个新的僵尸。
为了做到这一点,我们要读出 CryptoKitties 智能合约中的 kittyDna。这些数据是公开存储在区块链上的。
- 创建了一个新的合约
ZombieFeeding
,继承自’ZombieFactory’ - 因为这是一个新文件,所以通过
Import
引入 zombiefactory.sol - 创建了一个接口
KittyInterface
,里面有一个函数getKitty
,该函数来自 CryptoKitties 智能合约,它返回所有的加密猫的数据,包括它的“基因”(我们的僵尸游戏要用它生成新的僵尸)。
getKitty
函数源码
function getKitty(uint256 _id) external view returns (bool isGestating,bool isReady,uint256 cooldownIndex,uint256 nextActionAt,uint256 siringWithId,uint256 birthTime,uint256 matronId,uint256 sireId,uint256 generation,uint256 genes
) {Kitty storage kit = kitties[_id];// if this variable is 0 then it's not gestatingisGestating = (kit.siringWithId != 0);isReady = (kit.cooldownEndBlock <= block.number);cooldownIndex = uint256(kit.cooldownIndex);nextActionAt = uint256(kit.cooldownEndBlock);siringWithId = uint256(kit.siringWithId);birthTime = uint256(kit.birthTime);matronId = uint256(kit.matronId);sireId = uint256(kit.sireId);generation = uint256(kit.generation);genes = kit.genes;
}
- 用
ckAddress
变量存储CryptoKitties 合约的地址 - 创建了一个用
ckAddress
初始化的KittyInterface
类型的对象kittyContract
ZombieFeeding
合约有两个函数,- 当一个僵尸猎食其他生物体时,它自身的DNA将与猎物生物的DNA结合在一起,形成一个新的僵尸DNA,这个函数是
feedAndMultiply
.
这个函数第一行代码是通过添加一个require
语句来确保msg.sender
是这个僵尸的主人
第二行是获取僵尸的引用。
第三行是确保 _targetDna 不长于16位。
第四行是算出新的dna赋值给变量newDna
第五行是判断是否是通过攻击 CryptoKitties 上猫来生成的新僵尸
第六行是如果第五行为真,就在dna上做一标记,最后两位数为99
第八行调用父类的 _createZombie 函数。_createZombie 是internal
属性的 - 第二个函数
feedOnKitty
, 读取 CryptoKitties 的 dna,也就是kittyContract.getKitty
返回值的第10个值,然后调用feedAndMultiply
函数生成新的僵尸。
- 当一个僵尸猎食其他生物体时,它自身的DNA将与猎物生物的DNA结合在一起,形成一个新的僵尸DNA,这个函数是
将上面的合约部署后,我们看一下和合约进行交互的例子, 这个例子使用了 JavaScript 和 web3.js:
var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)// 假设我们有我们的僵尸ID和要攻击的猫咪ID
let zombieId = 1;
let kittyId = 1;// 要拿到猫咪的DNA,我们需要调用它的API。这些数据保存在它们的服务器上而不是区块链上。
// 如果一切都在区块链上,我们就不用担心它们的服务器挂了,或者它们修改了API,
// 或者因为不喜欢我们的僵尸游戏而封杀了我们
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {let imgUrl = data.image_url// 一些显示图片的代码
})// 当用户点击一只猫咪的时候:
$(".kittyImage").click(function(e) {// 调用我们合约的 `feedOnKitty` 函数ZombieFeeding.feedOnKitty(zombieId, kittyId)
})// 侦听来自我们合约的新僵尸事件好来处理
ZombieFactory.NewZombie(function(error, result) {if (error) return// 这个函数用来显示僵尸:generateZombie(result.zombieId, result.name, result.dna)
})
编写区块链游戏学智能合约 教程2:僵尸攻击人类相关推荐
- 区块链游戏FOMO3D智能合约核心分析
最近做一个区块链的项目,需要彻底分析FOMO3D的智能合约,顺便熟悉一下区块链的开发流程. 首先为了能跑FOMO3D的智能合约我尝试了truffle+galanche,对我来说不太理想,我就自己用py ...
- 分享实录|区块链技术与智能合约入门(开发实例)
2019独角兽企业重金招聘Python工程师标准>>> 1 什么是区块链 1.1白话讲解区块链 现在区块链特别火,可能大家都听说过区块链,听说过比特币,那到底什么是区块链? 前几天和 ...
- 【区块链DAPP】智能合约概述
智能合约概述 智能合约是运行在区块链公链上的一种代码,该代码由Solidity编写,并通过区块链的智能合约虚拟机来执行,以达到对区块链编程的目标.可以将区块链公联理解为操作系统,Solidity是编写 ...
- 区块链中的智能合约是什么?
链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. "智能合约是一套以数字形式定义的承诺,承诺控制着数字资产并包含了合约参与者约定的权利和义务,由计算机系统自动执 ...
- 行走在区块链上的智能合约
链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 我和你打一个赌,我赌明天是雨天,你赌是晴天,赌注100大洋.假设明天是晴天,然后你跑过来管我要100大洋的赌金,我装疯卖 ...
- 区块链中的智能合约(Smart Contract)
1994年,法律学者.密码学家Nick Szabo认识到智能合约的去中心化分类账的应用.他理论上认为,这些合同可以用代码编写,可以在系统上存储和复制,并由构成区块链的计算机网络进行监督.这些智能合约也 ...
- 区块链 Fisco bcos 智能合约(19)-区块链性能腾飞:基于DAG的并行交易执行引擎PTE
在区块链世界中,交易是组成事务的基本单元. 交易吞吐量很大程度上能限制或拓宽区块链业务的适用场景,愈高的吞吐量,意味着区块链能够支持愈广的适用范围和愈大的用户规模. 当前,反映交易吞吐量的TPS(Tr ...
- 区块链中的“智能合约”有何应用?
链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 如刺金般闪耀的区块链时代,投资者的热潮还将持续升温,与此同时金融的大佬已经开始注意到区块链应用落地场景的实现,在金融界实 ...
- tron区块链php对接,Tron区块链技术 - Tron智能合约概述
Tron区块链技术:多年来, 以太坊 一直是分散世界中开发智能合约的主流平台之一.然而,最近TRON作为一个准备面对以太坊的竞争平台在分散网络中崛起. TRON区块链技术是什么? Tron区块链是 ...
最新文章
- 【ShareCode】不错的技术文章 -- 如何使用异或(XOR)运算找到数组中缺失的数?...
- java intent bundle_Android 通过Intent使用Bundle传递对象详细介绍
- 横向ListView(四) —— 添加滚动条
- 蜘蛛纸牌java注释_自己摸索的纸牌游戏代码,感觉还有很多知识不懂,任重道远啊!...
- python 32位和64位的区别在哪
- 0.《Apollo自动驾驶工程师技能图谱》
- mysql 手动配置服务器_Win7系统下手动配置Apache+PHP+MySQL环境WEB服务器 -电脑资料...
- BugkuCTF-MISC题zip伪加密
- mysql目录树_MySQL B+树目录及索引优化_mysql
- windows xp中安装PadWalker
- linux分支结构,实验四 Shell脚本中的分支结构
- iOS开发系列-ARC浅解
- android qq输入法表情,QQ输入法-问题反馈
- 本地测试用的帐号csv文件
- CAD工具——批量打印
- 《Dreamweaver CS6 完全自学教程》笔记 第九章:插入多媒体对象
- mysql help_深入理解mysql帮助命令(help)
- 2022年Web 前端怎样入门?最新Web前端入门的学习路线
- 加强杂交和环境选择的高维目标进化算法
- 单商户商城系统功能拆解39—分销应用—分销等级
热门文章
- Qt - WPS文本编辑器(WPS新建文档)
- 家族信托二十大功能全解读
- D轮融资10亿元,“无名氏”城云科技是怎么混上来的?
- lua 不支持中文字符_英雄联盟跳票?不支持中文?别急看这里!界面最强翻译!...
- 江南大学计算机考研考场在哪,2019年江南大学考研考场安排
- 推荐《第一本经济学》
- ikbc机械键盘打字出现重复_只做精品的ikbc机械键盘:静音+六键无冲,180天续航...
- 给大家推荐一本Python书,京东断货王!刚刚又火爆IT圈!
- IDEA断点调试技巧,多张动图包教包会
- 解决在爬虫时出现的 ‘NoneType‘ object has no attribute ‘text‘