学习区块链的朋友应该都了解去年又一款迷恋猫的爆款游戏,他是一款基于以太网的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 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 = var ZombieFactoryContract = web3.eth.contract(abi)var contractAddress = var ZombieFactory = ZombieFactoryContract.at(contractAddress)$("#ourButton" ).click(function (e ) { var name = $("#nameInput" ).val() ZombieFactory.createRandomZombie(name) }) var event = ZombieFactory.NewZombie(function (error, result ) { if (error) return generateZombie(result.zombieId, result.name, result.dna) }) function generateZombie (id, name, dna ) { let dnaStr = String (dna) while (dnaStr.length < 16 ) dnaStr = "0" + dnaStr let zombieDetails = { headChoice : dnaStr.substring(0 , 2 ) % 7 + 1 , eyeChoice : dnaStr.substring(2 , 4 ) % 11 + 1 , shirtChoice : dnaStr.substring(4 , 6 ) % 6 + 1 , 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 (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" ); } }
细心的同学在代码的开头就发现了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 可以在任何地方调用,不管是内部还是外部。)
最后我们来看一个与我们的刚部署的合约进行交互的例子, 这个例子使用了JavaScript
和web3.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 = var ZombieFeedingContract = web3.eth.contract(abi)var contractAddress = var ZombieFeeding = ZombieFeedingContract.at(contractAddress)let zombieId = 1 ;let kittyId = 1 ;let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId$.get(apiUrl, function (data ) { let imgUrl = data.image_url }) $(".kittyImage" ).click(function (e ) { ZombieFeeding.feedOnKitty(zombieId, kittyId) }) ZombieFactory.NewZombie(function (error, result ) { if (error) return generateZombie(result.zombieId, result.name, result.dna) })
后续内容将在下一篇中继续介绍。