学习区块链的朋友应该都了解去年又一款迷恋猫的爆款游戏,他是一款基于以太网的Dapp游戏。此游戏将区块链应用推向了高潮,网上各种解读接踵而来。
但对于学习区块链的技术人员肯定想从代码层次学习下,想借此掌握一些Dapp开发的技巧,我也是这么想的,但是不巧的是我的水平有限(虽然代码真的很简单,代码量也不多),读了一遍源码发现脑子里没有留下什么。

于是就在网上各种找资料,无意中发现了Loom Network的一个游戏教程cryptozombies,就尝试了下,发现停不下来,就把Solidity 教程: 智能合约基础教程给学完了。在学习的过程中发现很多知识也在迷恋猫的代码中出现了,瞬间感觉开朗了很多。

此篇博客算是学完课程之后的回顾和总结吧。

僵尸工厂

这是个僵尸游戏,所以游戏的开始肯定得有个创造僵尸的地方,这里就是从僵尸工厂开始的。先贴下代码,相信有点代码基础的同学都可以看懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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) private {
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);
_createZombie(_name, randDna);
}

}

上述代码中个人感觉需要重点掌握的应该是event,何为event呢?event是合约和区块链通讯的一种机制,最重要的是你的前端应用可以”监听”某些事件,并做出反应
NewZombie是一个event,在_createZombie最后一行代码中调用,我们仔细观察下_createZombie函数的逻辑,新建了一个Zombie,并且push到了zombies的数组中,也对相应的映射进行的修改,至此新建zombie的逻辑已经写完,那么最后一行调用NewZombie事件有什么用呢?
也就是说调用event并不是业务逻辑需要,而是框架需要,是为了告诉区块链去做一件事。那么具体什么事呢?这里就得说说区块链的日志
日志在区块层面,可以用一种特殊的可索引的数据结构来存储数据,所以说日志是区块链中存储数据的,而且还是可以被检索的。Solidity的event就是用日志实现的当event被调用时,会触发event的参数存储到交易的日志中
NewZombie事件被调用的时候将新建zombie的id、name和dna信息存在了日志中,通过前端应用就可以将其读出。

最后看下官方给出的前端应用调用合约的代码,这里也展示了event的具体用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 下面是调用合约的方式:
var abi = /* abi是由编译器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 发布之后在以太坊上生成的合约地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能访问公共的函数以及事件

// 某个监听文本输入的监听器:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
//调用合约的 `createRandomZombie` 函数:
ZombieFactory.createRandomZombie(name)
})

// 监听 `NewZombie` 事件, 并且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})

// 获取 Zombie 的 dna, 更新图像
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果dna少于16位,在它前面用0补上
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr

let zombieDetails = {
// 前两位数构成头部.我们可能有7种头部, 所以 % 7
// 得到的数在0-6,再加上1,数的范围变成1-7
// 通过这样计算:
headChoice: dnaStr.substring(0, 2) % 7 + 1
// 我们得到的图片名称从head1.png 到 head7.png

// 接下来的两位数构成眼睛, 眼睛变化就对11取模:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 再接下来的两位数构成衣服,衣服变化就对6取模:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
//最后6位控制颜色. 用css选择器: hue-rotate来更新
// 其实迷恋猫的各种形态也是从它的dna中像这样解析出来的
// 360度:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}

msg.sender

对于不了解智能合约开发的同学来说,看到msg.sender这个变量的时候,肯定一脸懵逼,这个从哪来的。。。其实msg.sender是一个全局变量,就像javascript一样也有很多全局变量。这些全局变量可以被所有函数调用,其中一个全局变量就是msg.sender它指的是当前调用者(或智能合约)的address。 注意:在Solidity中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以msg.sender总是存在的。

与其它合约交互

电影中的僵尸都是成群结队的去撕咬异类,需要寻找食物,那么刚刚新建的僵尸也需要去寻找食物,所以这里作者将迷恋猫作为了僵尸的食物,要想拿到迷恋猫的信息就得与迷恋猫交互,这就是本节的重点,与其他合约交互。照例先贴下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 语句
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
// 子类访问父类的内部方法注意private和internal的区别
_createZombie("NoName", newDna);
}

function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}

细心的同学在代码的开头就发现了solidity也可以像python那么import一个文件,接着有个ZombieFeeding的合约,此合约继承自ZombieFactory(继承的语法是使用is关键字)。
这里漏介绍了一段代码,那就是KittyInterface,这个看着像个合约,但仔细一看不是,这就是solidity中的接口,是不是有点java的感觉,有类名,方法名,只是没有方法具体的实现。

KittyInterface只声明了迷恋猫的getKitty函数,其实迷恋猫合约里有很多函数,这里只声明使用到的函数,其它函数可以查看合约地址。使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。

现在我们从java角度看下接口如何使用。在java中,我们先写一个接口类I,声明了个方法f。然后又写了个实现类Impl,并完成了f的具体逻辑。最后有个类O,调用了f,所以在O内有个对象I,并且被Impl初始化。上面的代码是不是也是这个套路,只不过KittyInterface的实现类被迷恋猫写好了,不在这个僵尸项目中。

到目前为止,我们已经写两个合约一个接口,我们只用编译和部署ZombieFeeding,就可以将这个合约部署到以太坊了。我们最终完成的这个合约继承自 ZombieFactory,因此它可以访问自己和父辈合约中的所有public方法。

说到访问权限上面ZombieFactory到代码有处需要改下,_createZombie方法是private,但是在ZombieFeeding中被调用了,此时会编译错误,因为private意味着它只能被合约内部调用internal就像private但是也能被继承的合约调用,所以需要将ZombieFactory._createZombie改为internal。(顺便提下external只能从合约外部调用;最后public 可以在任何地方调用,不管是内部还是外部。)

最后我们来看一个与我们的刚部署的合约进行交互的例子, 这个例子使用了JavaScriptweb3.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)
})

后续内容将在下一篇中继续介绍。