继续追寻中本聪的脚步

区块链游戏开发教程_CryptoKitties源码详解_区块链大本营出品

自从去年11月底 CryptoKitties 上线,不到一周,它的创世猫便卖出了超过11万美金(约合70万人民币)的天价。之后第二天,它更是让以太坊的全部交易量飙升6倍,瘫痪了整个区块链网络。

CryptoKitties 带火区块链游戏的同时,各种山寨也尾随而来,其中就包括西二旗某大厂试水区块链的汪星人游戏 CryptoDogs,真是连名字都可以照搬。不过话又说回来,作为去中心化应用dapp中第一款现象级的产品,CryptoKitties 自然避免不了被人研究、模仿,直至学得最好的那个人把它超越。

所以,作为一名有追求的区块链爱好者,直接山寨人家的 “Bad Artist” 式做法,我们肯定会有所不为;但是,”Good artists steal”,如何学到 CryptoKitties 的精髓并把它用到将来区块链开发的创新之中,才是大家真正关心的话题。

要做到这一点,不读源代码,没有亲手撸一遍 CryptoKitties 程序,肯定是不够的。

这也正是这篇 Medium 热文的写作目的,通过对代码的学习,来帮大家深刻理解 CryptoKitties 这个游戏。

CryptoKitties 概述

如果从没接触过 CryptoKitties,你很有可能还不了解这个游戏到底是什么。其实,CryptoKitties 的本质是一个购买、销售、繁殖数字猫咪的游戏。每只数字猫咪的外观由其基因所决定,因此每只猫咪的外观都各不相同。你可以让两只猫咪通过繁殖来产生一个后代,其外观由父母双方的基因共同决定。你也可以用以太币从他人手中买下你所喜欢的猫咪,或是拍卖你所拥有的数字猫咪来换取以太币。

要想快速了解这个游戏的工作原理,最好的方法就是直接阅读源代码。

CryptoKitties 的源代码大部分是开源的(也有小部分没有开源,后面会讲到)。CryptoKitties 源码大约有2000行,本文将主要讲解其中相对重要的部分。

阅读完整代码,请查阅 EthFiddle 上的合约代码副本:
ethfiddle-com/09YbyJRfiI

CryptoKitties游 戏的源代码分成了一个个的子合约,而非一个包含所有逻辑的单一文件。

子合约通过下述方式继承主合约:

  1. contract KittyAccessControl
  2. contract KittyBase is KittyAccessControl
  3. contract KittyOwnership is KittyBase, ERC721
  4. contract KittyBreeding is KittyOwnership
  5. contract KittyAuction is KittyBreeding
  6. contract KittyMinting is KittyAuction
  7. contract KittyCore is KittyMinting

最终应用程序指向的合约是 KittyCore 合约,这个合约继承了前面所有合约的属性和方法。接下来,让我们一个一个地来分析这些合约。

KittyAccessControl:谁控制合约?

该合约负责管理各种不同的地址,同时定义了各种仅限于特定角色执行的限制性操作,这些角色被命名为“CEO”、“CFO”和“COO”。

KittyAccessControl 合约主要功能是管理其他合约,所以它不涉及游戏的具体逻辑。

该合约定义了以太坊地址“CEO”、“CFO”和“COO”的使用方法,它们对该合约有着特殊的所有权以及控制权限。

KittyAccessControl 合约还定义了一些函数修饰符,如 onlyCEO (该函数只有“CEO”才能执行),同时该合约还定义了一些暂停/恢复合约的方法以及提现方法。

1modifier onlyCLevel() {
2 require(
3 msg.sender == cooAddress ||
4 msg.sender == ceoAddress ||
5 msg.sender == cfoAddress
6 );
7 _;
8}
9
10 //…some other stuff
11
12// Only the CEO, COO, and CFO can execute this function:
13function pause() external onlyCLevel whenNotPaused {
14 paused = true;
15}

pause() 函数主要是方便开发人员进行版本更新,以防那些预见不到的 bug。但我同事 Luke 指出,这个方法可以让开发人员有权完全冻结合约,令所有人都无法转让、出售或繁殖他们的猫咪!当然,开发人员并不想这么做。但真正有趣的地方确实,大多数人仅仅因为该游戏运行在以太坊上,就会以为它是一款完全是去中心化的游戏。

我们接着来看其他合约。

KittyBase:数字猫是什么猫?

KittyBase 合约是我们定义最底层的代码的地方,这些代码会用到整个游戏的所有核心功能上,包括数据存储结构、相关常量与数据类型,以及管理这些数据的内部函数。

KittyBase 合约定义了应用程序的很多核心数据。首先它将Kitty定义为结构体类型:

1struct Kitty {
2 uint256 genes;
3 uint64 birthTime;
4 uint64 cooldownEndBlock;
5 uint32 matronId;
6 uint32 sireId;
7 uint32 siringWithId;
8 uint16 cooldownIndex;
9 uint16 generation;
10}

从这里可以看到,一只猫咪的本质就是一长串的无符号整数。

接下来一一介绍猫咪的每个属性的具体参数:

  • genes — 是256位的整数,主要作为猫的遗传密码,是决定猫外貌的核心数据;
  • birthTime — 猫出生时所打包的区块的时间戳;
  • cooldownEndBlock — 猫可以再次繁殖的最小时间;
  • matronId&sireId — 猫母亲的ID号和猫父亲的ID号;
  • siringWithId — 如果猫怀孕了,则设置为父亲的ID,否则为零;
  • cooldownIndex — 猫繁殖所需的冷却时间(猫需要等待多久才能繁殖);
  • generation — 猫的“世代号”(指明这是第几代猫)。合约创造的第一只猫是0代,新一代猫的“世代号”是其父母中较大的一代再加1。

需要注意的是,在 CryptoKitties 游戏中,猫是无性别之分的,任意2只猫都可以一起繁殖。

KittyBase 合约声明了一个“猫”结构的数组,如下所示:

Kitty[] kitties;

该数组包含了所有猫咪的数据,所以它就像一个猫咪数据库一般。无论什么时候生成一个新的猫咪,它都会被添加到该数组内,数组的索引就是猫咪的 ID 号。下图显示的是创世猫,其 ID 号为“1”。

CryptoKitties源码详解

该合约中还包含有从猫咪 ID 号到其所有者地址的一个映射,主要用于追踪猫咪的所有者。

mapping (uint256 => address) public kittyIndexToOwner;

合约中还定义了其他一些映射,但此处不再深究每一个细节。

当猫咪的所有者从一个人转移到另外一个人时,kittyIndexToOwner 映射就会更新,从而识别新的所有者。

1/// @dev Assigns ownership of a specific Kitty to an address.
2function _transfer(address _from, address _to, uint256 _tokenId) internal {
3 // Since the number of kittens is capped to 2^32 we can’t overflow this
4 ownershipTokenCount[_to]++;
5 // transfer ownership
6 kittyIndexToOwner[_tokenId] = _to;
7 // When creating new kittens _from is 0x0, but we can’t account that address.
8 if (_from != address(0)) {
9 ownershipTokenCount[_from]–;
10 // once the kitten is transferred also clear sire allowances
11 delete sireAllowedToAddress[_tokenId];
12 // clear any previously approved ownership exchange
13 delete kittyIndexToApproved[_tokenId];
14 }
15 // Emit the transfer event.
16 Transfer(_from, _to, _tokenId);
17}

现在我们来看看当一个新的猫咪生成时都发生了些什么:

1function _createKitty(
2 uint256 _matronId,
3 uint256 _sireId,
4 uint256 _generation,
5 uint256 _genes,
6 address _owner
7)
8 internal
9 returns (uint)
10{
11 // These requires are not strictly necessary, our calling code should make
12 // sure that these conditions are never broken. However! _createKitty() is already
13 // an expensive call (for storage), and it doesn’t hurt to be especially careful
14 // to ensure our data structures are always valid.
15 require(_matronId == uint256(uint32(_matronId)));
16 require(_sireId == uint256(uint32(_sireId)));
17 require(_generation == uint256(uint16(_generation)));
18
19 // New kitty starts with the same cooldown as parent gen/2
20 uint16 cooldownIndex = uint16(_generation / 2);
21 if (cooldownIndex > 13) {
22 cooldownIndex = 13;
23 }
24
25 Kitty memory _kitty = Kitty({
26 genes: _genes,
27 birthTime: uint64(now),
28 cooldownEndBlock: 0,
29 matronId: uint32(_matronId),
30 sireId: uint32(_sireId),
31 siringWithId: 0,
32 cooldownIndex: cooldownIndex,
33 generation: uint16(_generation)
34 });
35 uint256 newKittenId = kitties.push(_kitty) – 1;
36
37 // It’s probably never going to happen, 4 billion cats is A LOT, but
38 // let’s just be 100% sure we never let this happen.
39 require(newKittenId == uint256(uint32(newKittenId)));
40
41 // emit the birth event
42 Birth(
43 _owner,
44 newKittenId,
45 uint256(_kitty.matronId),
46 uint256(_kitty.sireId),
47 _kitty.genes
48 );
49
50 // This will assign ownership, and also emit the Transfer event as
51 // per ERC721 draft
52 _transfer(0, _owner, newKittenId);
53
54 return newKittenId;
55 }

从上面这段代码中可以看出,这个函数传递了母亲和父亲的 ID 号、猫咪的世代号、256位遗传密码和所有者的地址。然后创建猫咪,并将其加入到 Kitty 数组中,最后调用 _transfer() 将其分配给新的所有者。

是不是很酷!现在我们已经知道 CryptoKitties 游戏如何将一只猫咪定义为一种数据类型,如何将所有猫咪都存储在区块链中,以及如何跟踪这些猫咪的所有者。

KittyOwnership:作为通证的猫咪

CryptoKitties 游戏遵循 ERC721 通证标准,这是一种不可替代通证,它可以很好地追踪数字收藏品的所有权,比如数字扑克牌或 MMORPG(大型多人在线角色扮演游戏)中的稀有物品。

关于可互换性的说明:Ether 是可互换的通证,因为任意5个 ETH 都与其他5个 ETH %D杂,可能存在不易发现的 bug。通过设置两个“兄弟”合约,可以做到在升级这个两个合约的同时不中断追踪猫咪所有权的主合约。

这个 KittyAuctions 合约包含有 setSaleAuctionAddress() 函数和 setSiringAuctionAddress() 函数。setGeneScienceAddress() 函数只能由 “CEO” 调用,而且只能由 “CEO” 来设置处理这些函数的外部合约地址。

这就意味着,即使 CryptoKitties 合约本身不可改变,“CEO” 也可以灵活地改变这些拍卖合约的地址,从而改变拍卖规则。我认为,这不一定是坏事,因为开发人员有时候需要修正 bug,当然,我们还是要注意一下这里。

在本文中,我不准备详细讨论拍卖合约和竞标合约的逻辑,主要是想避免文章篇幅过长(因为它已经够长了!)。

KittyMinting“零代猫”工厂

这一部分,我们来讲一讲 CryptoKitties 是怎样来创建零代猫的。

我们最多可以制作5000只可以赠送的“营销”猫(“营销”猫是指专门为社区的初期发展而创建的猫)。除了“营销”猫之外,其他所有的零代猫只能由确定的算法创造出来,然后由算法决定进行拍卖的起始价格。无论这些猫是如何被创造出来的,它们的总数最多只能有50000只,这是整个游戏有的硬性限制。除此之外,其他猫咪的产生就只能依靠不断地繁殖。

合约能够创建的“营销”猫的数量和零代猫的数量都是硬编码的,如下所示:

1uint256 public constant PROMO_CREATION_LIMIT = 5000;
2uint256 public constant GEN0_CREATION_LIMIT = 45000;

下面是创建“营销“猫和零代猫的代码,这个代码规定了只有 “COO” 可以来创建此这些猫咪。

1/// @dev we can create promo kittens, up to a limit. Only callable by COO
2/// @param _genes the encoded genes of the kitten to be created, any value is accepted
3/// @param _owner the future owner of the created kittens. Default to contract COO
4function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
5 address kittyOwner = _owner;
6 if (kittyOwner == address(0)) {
7 kittyOwner = cooAddress;
8 }
9 require(promoCreatedCount < PROMO_CREATION_LIMIT);
10
11 promoCreatedCount++;
12 _createKitty(0, 0, 0, _genes, kittyOwner);
13}
14
15/// @dev Creates a new gen0 kitty with the given genes and
16/// creates an auction for it.
17function createGen0Auction(uint256 _genes) external onlyCOO {
18 require(gen0CreatedCount < GEN0_CREATION_LIMIT);
19
20 uint256 kittyId = _createKitty(0, 0, 0, _genes, address(this));
21 _approve(kittyId, saleAuction);
22
23 saleAuction.createAuction(
24 kittyId,
25 _computeNextGen0Price(),
26 0,
27 GEN0_AUCTION_DURATION,
28 address(this)
29 );
30
31 gen0CreatedCount++;
32}

通过 createPromoKitty() 函数,“COO” 可以用任何他想要的基因来创建一只新猫咪,然后发给任何他想给的人(但通过这个合约,“COO” 最多可以创建5000只猫咪)。我猜,他们这样做的目的是想把猫奖励给早期测试者,或是送给自己的朋友、家人,或是用作项目的推广,等等。但这也意味着,你的猫咪可能并不像你所想的那样独一无二,因为它有可能会有5000个相同的副本,也就可能存在5000个跟你的猫咪长的一模一样的猫!

在 createGen0Auction() 函数中,“COO” 也给新的猫提供基因的遗传密码,但没有将这个遗传密码分配给特定的人的地址,而是发起了一个拍卖,让用户利用竞标的形式购买猫咪。

KittyCore主合约

这是 CryptoKitties 合约的主合约(main),它被编译并运行在以太坊区块链上。这个智能合约是连接其他合约的纽带。

由于这款游戏遵循继承结构,它继承了我们之前所看到的所有合约,同时增加了几个新的方法,例如下面这个,使用猫咪 ID 来获取所有猫的数据的函数:

1/// @notice Returns all the relevant information about a specific kitty.
2/// @param _id The ID of the kitty of interest.
3function getKitty(uint256 _id)
4 external
5 view
6 returns (
7 bool isGestating,
8 bool isReady,
9 uint256 cooldownIndex,
10 uint256 nextActionAt,
11 uint256 siringWithId,
12 uint256 birthTime,
13 uint256 matronId,
14 uint256 sireId,
15 uint256 generation,
16 uint256 genes
17) {
18 Kitty storage kit = kitties[_id];
19
20 // if this variable is 0 then it’s not gestating
21 isGestating = (kit.siringWithId != 0);
22 isReady = (kit.cooldownEndBlock <= block.number);
23 cooldownIndex = uint256(kit.cooldownIndex);
24 nextActionAt = uint256(kit.cooldownEndBlock);
25 siringWithId = uint256(kit.siringWithId);
26 birthTime = uint256(kit.birthTime);
27 matronId = uint256(kit.matronId);
28 sireId = uint256(kit.sireId);
29 generation = uint256(kit.generation);
30 genes = kit.genes;
31}

这是一个公开方法,它会从区块链上返回一个特定猫咪的所有数据。我认为,他们 Web 页面所展示的猫咪的所有数据都是通过这个方法从以太坊区块链上查询到的。

网址:www.cryptokitties-co

讲到这里,大家可能会疑惑,为什么没有看到任何图像数据?是什么决定了猫咪的样子呢?

从上面的代码中我们可以看出,一个“猫咪”实际上就是一串256位的无符号整数,这256位整数就代表其遗传密码。

在 Solidity 合约代码中没有任何地方存储猫的图像或猫的描述信息,也没有任何地方明确定义了这个256位整数的实际含义。所以,推断可知,对于遗传密码的解释发生在 CryptoKitty 的 Web 服务器上。

所以,CryptoKitties 虽说是用区块链做出来的游戏,也是对区块链应用的一个非常好的拓展,但它并不是100%的区块链应用(因为它用到了传统服务器)。未来某一天,一旦他们的网站突然关闭,又没有备份所有图像的话,你的猫咪就只剩下一长串毫无意义的256位整数。

在合约代码中,我找到了一个名为 ERC721Metadata 的合约,但是它好像什么事情都没做。

所以我猜想,他们最初的计划是将所有内容都存储在区块链中,但随着项目的进展,他们却决定不再这么做了(因为在以太坊中存储大量数据的成本太高),最终他们决定将大部分内容存储到 Web 服务器上。

最后,总结一下:

把 CryptoKitties 的代码撸到现在,我们搞明白的事情是以下这几个:

  1. 猫咪是如何被表示成数据的;
  2. 所有已生成的猫咪如何被存储到一个智能合约中的,以及该合约是如何跟踪猫咪的所有者的;
  3. 零代猫是如何产生的;
  4. 猫咪是如何进行繁殖的,新的猫咪是怎样生成的。

原文链接:

medium-com/loom-network/how-to-code-your-own-cryptokitties-style-game-on-ethereum-7c8ac86a4eb3

赞(0) 群聊
文章链接:https://tucaod.xyz/2032.html
资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

神吐槽 抢沙发

讨论群终身200元

永远不解散,不涨价。不荐股、不带单。
ai学习安装,美股分析交流,不构成任何投资建议。终身免费指导。
AI产业链资料整理+资料库

联系我们

觉得文章有用就可以进群

继续追寻中本聪的脚步

支付宝扫一扫打赏