本文深入探讨了如何在以太坊上创建类CryptoKitties的游戏,通过分析其源代码的不同合同,实现了数字猫的购买、出售和繁殖等功能。文章详细阐述了逻辑结构、核心数据和功能模块,为开发者提供了再次创作的基础,且提供了一些编码示例及解释。

CryptoKitties 很好地展示了区块链除了简单的金融交易外还可以用于什么。
我希望未来我们能看到更多创新的区块链应用于这样的游戏,因此我想写一个关于CryptoKitties背后代码的快速指南,以展示它是如何在表面下实现的。
这篇文章是为开发者写的,虽然它并不是绝对初学者的Solidity介绍,但我尽力包含文档链接,以使其对所有级别的开发者尽可能可及。
让我们开始吧…
几乎所有的CryptoKitties代码都是开源的,因此了解它如何工作的最佳方式就是阅读源代码。
总共大约2000行,所以在这篇文章中,我将逐一讲解我认为最重要的部分。但如果你想自己查看,这里有EthFiddle上的完整合约代码副本:
如果你不熟悉CryptoKitties,它基本上是一个购买、销售和繁殖数字猫的游戏。每只猫都有独特的外观,这由它的基因定义。当你把两只猫交配在一起时,它们的基因以独特的方式组合,产生一个后代,你可以再繁殖或者出售。
CryptoKitties的代码分成了多个小合约,以便将相关代码打包在一起,而不是有一个包含所有内容的巨大文件。
主要的kitty合约的子合约继承看起来是这样的:
contract KittyAccessControl
contract KittyBase is KittyAccessControl
contract KittyOwnership is KittyBase, ERC721
contract KittyBreeding is KittyOwnership
contract KittyAuction is KittyBreeding
contract KittyMinting is KittyAuction
contract KittyCore is KittyMinting所以KittyCore最终是应用程序指向的合约地址,并且它继承了之前合约的所有数据和方法。
让我们逐一了解这些合约。
1. KittyAccessControl: 谁控制合约?该合约管理只能由特定角色执行的操作的各种地址和限制。即CEO,CFO和COO。
该合约用于管理合约,与游戏机制没有任何关系。它基本上有“setter”方法用于“CEO”,“COO”和“CFO”,这些是拥有合约特定功能的特殊以太坊地址。
KittyAccessControl定义了一些函数修饰符,例如onlyCEO(限制一个函数只有CEO可以执行),并添加方法来执行暂停/解除暂停合约或提取资金:
modifier onlyCLevel() {
    require(
        msg.sender == cooAddress ||
        msg.sender == ceoAddress ||
        msg.sender == cfoAddress
    );
    _;
}//...还有其他内容// 只有CEO,COO和CFO才能执行此函数:
function pause() external onlyCLevel whenNotPaused {
    paused = true;
}pause()函数可能是为了让开发者能够在发现意外错误时用更高版本进行更新…但正如我的同事Luke 指出的,这实际上允许开发人员完全冻结合约,使得没有人可以转移、销售或繁殖他们的猫咪!并不是说他们会希望这样做——但指出这一点很有意思,因为大多数人认为DApp只因为在以太坊上就完全去中心化。
继续…
在这里,我们定义了核心功能共享的最基本的代码。这包括我们的主要数据存储、常量和数据类型,以及管理这些项的内部函数。
KittyBase定义了很多应用的核心数据。首先,它将Kitty定义为一个结构体:
struct Kitty {
    uint256 genes;
    uint64 birthTime;
    uint64 cooldownEndBlock;
    uint32 matronId;
    uint32 sireId;
    uint32 siringWithId;
    uint16 cooldownIndex;
    uint16 generation;
}所以一只kitty其实就是一堆无符号整数…我想。
分解它的每个部分:
genes — 代表猫的遗传代码的256位整数,决定猫的外观的核心数据birthTime — 猫诞生时的块的时间戳cooldownEndBlock — 该猫可以再次进行繁殖的最小时间戳matronId & sireId — 分别是猫的母亲和父亲的IDsiringWithId — 如果猫现在怀孕,则设置为父亲的ID,否则为零cooldownIndex — 该猫的当前冷却持续时间(猫在繁殖后必须等待多长时间才能再繁殖)generation — 该猫的“代数”。合同铸造的第一只猫的代数为0;新猫的代数是父母代数的较大者,加1。请注意,在CryptoKitties中,猫是无性别的,任何两只猫都可以共同繁殖——因此猫没有性别。
KittyBase合约接下来声明了一个Kitty结构体的数组:
Kitty[] kitties;这个数组保存了所有存在的Kitties的数据,因此它有点像一个主猫数据库。每当创建一只新猫时,它就会添加到这个数组中,数组中的索引成为猫的ID,就像Genesis的ID为“1”:

我的数组索引是“1”!
该合约还包含一个映射 从猫的ID到其所有者的地址,以跟踪谁拥有一只kitty:
mapping (uint256 => address) public kittyIndexToOwner;一些其他的映射也被定义,但为了保持这篇文章的合理长度,我不打算逐一覆盖每个细节。
每当一只kitty从一个人转移到另一个人时,kittyIndexToOwner映射会更新以反映新所有者:
CryptoKitties00.js – Medium
| /// @dev 将特定Kitty的所有权分配给一个地址。 | |
| function_transfer(address_from,address_to,uint256_tokenId)internal{ | |
| // 由于猫的数量限制为2^32,所以我们不能溢出这个 | |
| ownershipTokenCount[_to]++; | |
| // 转移所有权 | |
| kittyIndexToOwner[_tokenId]=_to; | |
| // 创建新猫时,_from为0x0,但我们不能根据该地址进行计算。 | |
| if(_from!=address(0)){ | |
| ownershipTokenCount[_from]--; | |
| // 一旦猫咪被转移,还清除父亲允许的地址 | |
| deletesireAllowedToAddress[_tokenId]; | |
| // 清除先前批准的所有权交换 | |
| deletekittyIndexToApproved[_tokenId]; | |
| } | |
| // 发出转移事件。 | |
| Transfer(_from,_to,_tokenId); | |
| } | 
查看原文 CryptoKitties00.js 托管于❤的 GitHub
转移所有权将Kitty的ID的kittyIndexToOwner设置为接收者的\_to地址。
现在让我们看看新kitty被创建时发生了什么:
CryptoKitties01.js – Medium
| function_createKitty( | |
| uint256_matronId, | |
| uint256_sireId, | |
| uint256_generation, | |
| uint256_genes, | |
| address_owner | |
| ) | |
| internal | |
| returns(uint) | |
| { | |
| // 这些要求并不是严格必要的,我们的调用代码应该确保这些条件从未被破坏。 | |
| // 但是!_createKitty()已经是 | |
| // 一个昂贵的调用(存储),确保我们的数据结构始终有效是没坏处的。 | |
| require(_matronId==uint256(uint32(_matronId))); | |
| require(_sireId==uint256(uint32(_sireId))); | |
| require(_generation==uint256(uint16(_generation))); | |
| // 新kitty以与父母代数/2相同的冷却开始 | |
| uint16cooldownIndex=uint16(_generation/2); | |
| if(cooldownIndex>13){ | |
| cooldownIndex=13; | |
| } | |
| Kittymemory_kitty=Kitty({ | |
| genes: _genes, | |
| birthTime: uint64(now), | |
| cooldownEndBlock: 0, | |
| matronId: uint32(_matronId), | |
| sireId: uint32(_sireId), | |
| siringWithId: 0, | |
| cooldownIndex: cooldownIndex, | |
| generation: uint16(_generation) | |
| }); | |
| uint256newKittenId=kitties.push(_kitty)-1; | |
| // 这几乎永远不会发生,40亿只猫是很多,但 | |
| // 但我们还是100%确保永远不会让这发生。 | |
| require(newKittenId==uint256(uint32(newKittenId))); | |
| // 发出出生事件 | |
| Birth( | |
| _owner, | |
| newKittenId, | |
| uint256(_kitty.matronId), | |
| uint256(_kitty.sireId), | |
| _kitty.genes | |
| ); | |
| // 这将分配所有权,并且根据ERC721草案 | |
| _transfer(0,_owner,newKittenId); | |
| returnnewKittenId; | |
| } | 
查看原文 CryptoKitties01.js 托管于❤的 GitHub
所以这个函数传入母亲和父亲的ID,幸久猫的代数,256位基因编码,以及所有者的地址。然后它创建kitty,将其推送到主Kitty数组中,然后调用_createKitty()函数分配给新的所有者。
酷——所以现在我们可以看到CryptoKitties如何将kitty定义为数据类型,以及它如何在区块链上存储所有的kitties,并且追踪谁拥有哪只kitty。
这提供了基本的非同质化代币交易所需的方法,按草案ERC721规范进行。
CryptoKitties符合ERC721代币规范,这是一种非同质化代币类型,非常适合跟踪数字收藏品的所有权,例如数字纸牌或MMORPG中的稀有物品。
关于可替代性的说明: 以太 是可替代的 ,因为任何5 ETH和其他5 ETH没有区别。但对于 非同质化 代币如CryptoKitties,并不是每只猫的价值都相同,因此它们不可互换。
你可以从它的合约定义中看到KittyOwnership从一个ERC721合约中继承:
contract KittyOwnership is KittyBase, ERC721 {而所有ERC721代币遵循同样的标准,因此KittyOwnership合约填充以下函数的实现:
由于这些方法是公共的,这为用户提供了一种标准的方式与CryptoKitties代币交互,就如同他们与任何其他ERC721代币交互一样。你可以通过直接与以太坊区块链上的CryptoKitties合约交互,将你的代币转移给其他人,而无需通过他们的网页界面,从这个意义上来说,你确实拥有这些kitties。(除非CEO暂停合约 😉)。
我不会详细介绍这些方法的实现,但你可以在EthFiddle上查看它们(搜索“KittyOwnership”)。
这篇文章在首次发布时收到了大量积极反馈,所以我们建立了CryptoZombies:一个互动教程,用于在以太坊上构建你自己的游戏。它将逐步引导你学习编码Solidity,并构建自己的以太坊游戏。如果你喜欢这篇文章,查看一下!
该文件包含必要的方法,以使猫咪交配,包括追踪交配请求,并依赖外部基因组合合约。
“外部基因组合合约”(geneScience)存储在一个单独的未开源合约中。
KittyBreeding合约包含一个供CEO设置此外部合约地址的方法:
他们这样做是为了让游戏不会太简单——如果你可以直接读取kitty的DNA是如何决定的,那么就很容易知道应该交配哪些猫以获得一只“华丽猫”。
这个外部的geneScience合约在giveBirth()函数中被用来确定新猫的DNA(我们稍后会看到)。
现在让我们看看当两只猫交配在一起时会发生什么:
此函数接受母亲和父亲的ID,在主kitties数组中查找它们,将母亲的siringWithId设置为父亲的ID。(当siringWithId非零时,它表示母亲怀孕了)。
它还对两个父母执行triggerCooldown,使它们在设定的时间内无法再交配。
接下来,有一个公共的giveBirth()函数用于创建一只新猫:
代码中的内联注释是相当自解释的。基本上,代码首先进行一些检查以确定母亲是否准备好分娩。然后使用geneScience.mixGenes()来确定孩子的基因,将新kitty的所有权分配给母亲的所有者,接着调用我们在KittyBase中查看的_createKitty()函数。
请注意,geneScience.mixGenes()函数是一个黑盒,因为该合约是闭源的。因此我们实际上不知道孩子的基因是如何确定的,但我们知道这是母亲基因、父亲基因和母亲的cooldownEndBlock的某种函数。
在这里我们有用于拍卖或竞标猫咪或交配服务的公共方法。实际的拍卖功能在两个兄弟合约中处理(一个用于销售,另一个用于交配),而拍卖创建和竞标主要通过核心合约的这一部分进行调解。
根据开发者的说法,他们将这个拍卖功能分割成“兄弟”合约,因为 “它们的逻辑比较复杂,总是存在细微错误的风险。通过将它们保留在各自的合约中,我们可以在不干扰跟踪kitty所有权的主要合约的情况下对其进行升级。”
因此,本KittyAuctions合约包含函数setSaleAuctionAddress()和setSiringAuctionAddress(),类似于setGeneScienceAddress(),只能由CEO调用,并设置一个管理这些功能的外部合约的地址。
注意: “交配”是指出借你的猫——将其放上拍卖,另一用户可以支付你以太来使他们的猫与你的猫交配。哈哈。
这意味着即使CryptoKitties合约本身是不可修改的,CEO仍有灵活性可以在稍后更改这些拍卖合约的地址,从而改变拍卖的规则。再次,这不一定是坏事,因为有时开发者需要修复错误,但这是需要注意的事情。
为了避免这篇文章过于冗长,我不会深入详细讲解拍卖和出价逻辑是如何处理的,但你可以查看EthFiddle上的代码(搜索KittyAuctions)。
这个最后的部分包含我们用于创建新gen0猫咪的功能。我们可以产生最多5000只“促销”猫,可以赠送(在社区新颖时特别重要),而所有其他猫咪可以在创建后立即以算法确定的起价进行拍卖。无论它们是如何创建的,gen0猫的硬性限制为5万只。之后,所有都要依靠社区进行繁殖、繁殖、繁殖!
合约能够创建的促销猫和gen0猫的数量在这里是硬编码的:
uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;而且这是"Coo"可以创建促销和gen0猫的代码:
所以通过createPromoKitty(),看来Coo可以根据他想要的基因创建一只新kitty,并送给他想要的人(最多5000只猫)。我猜他们把这用于早期的测试者、朋友和家人,以及为了推广目的赠送免费的猫咪等。
但这也意味着你的猫可能没有你想象的那么独特,因为他可能会印制5000个相同的副本!
对于createGen0Auction(),Coo也提供了新kitty的基因代码。但它不分配给具体人的地址,而是创建了一次拍卖,用户可以出价以太来购买这只kitty。
这是主要的CryptoKitties合约,即在以太坊区块链上编译并运行的合约。该合约将所有内容结合在一起。
由于继承结构,它继承了之前我们查看的所有合约,并添加了一些最终的方法,例如通过其ID获取所有Kitty数据的这个函数:
这是一个公共方法,将从区块链返回特定kitty的所有数据。我想这就是他们的网络服务器查询以在网站上展示猫咪的内容。
正如我们从上面的代码中看到的,“kitty”基本上是一个代表其基因代码的256位无符号整数。
在Solidity合约代码中没有任何信息存储猫的图像、描述或决定该256位整数实际代表什么。那些基因代码的解释发生在CryptoKitty的网络服务器上。
所以虽然这是一个非常聪明的演示区块链上的游戏,但它实际上并不完全是100%基于区块链的。如果他们的网站在未来被下线,除非有人备份了所有的图像,否则你将只拥有一个毫无意义的256位整数。
在合约代码中,我确实发现了一个名为ERC721Metadata的合约,但它最终并没有用于任何事情。因此,我的猜测是他们最初计划将所有内容存储在区块链中,但后来决定不这样做(在以太坊上存储大量数据的成本太高?),因此,他们最终需要将其存储在他们的网页服务器上。
好的……总结一下,我们讨论了:
这仅仅是一个高层次的概述。如果你在寻找关于如何构建自己游戏的更深入教程,你可能会对我们将在接下来的几周内发布的互动编码学校感兴趣。
- 原文链接: medium.com/loom-network/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!