本文主要讲解了如何在以太坊的 CryptoZombies 游戏中实现核心游戏机制,包括使用时间戳实现技能冷却,使用带参数的modifier来实现等级限制,以及如何通过for循环来高效检索玩家拥有的僵尸,避免gas消耗。通过这些机制,让游戏更具动态性和可玩性。
欢迎回来,加密技术构建者们!在我们的上一篇文章中,我们强化了我们的僵尸工厂,通过 Ownable
模式掌握了安全性,并通过结构体打包和 view
函数优化了 gas。但是一个安全高效的合约还不是一个游戏。游戏的发展在哪里?策略在哪里?你的僵尸变得更强大的感觉在哪里?
今天,我们正从架构转向行动。我们正在通过实现使 CryptoZombies 具有吸引力的核心游戏机制,将灵魂注入我们的不死军队:冷却时间、等级门控能力和高效的链上数据检索。
在任何好的游戏中,强大的动作都不能被滥用。它们需要一个冷却期。由于 Solidity 的原生时间处理,在链上实现这一点非常简单。
1. 在 Solidity 中定义时间:
我们首先设置一个冷却期。使用 1 days
瞬间就能理解,而且比手动计算 86400
秒要干净得多。
uint cooldownTime = 1 days; // Solidity 原生理解这个
2. 更新 Zombie 结构体:
我们需要跟踪每个僵尸何时可以再次行动。我们在结构体中添加了一个 readyTime
,使用 uint32
非常适合存储时间戳,并通过结构体打包(正如我们上次讨论的那样!)帮助优化 gas。
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime; // 这将存储一个 Unix 时间戳
}
3. 构建冷却机制:
我们创建 internal 辅助函数来管理逻辑。这使我们的代码模块化和干净。
// 通过将 readyTime 设置为未来日期来启动冷却
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
// 检查冷却期是否结束
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
它是如何工作的:在僵尸进食(feedAndMultiply
)之后,我们调用 _triggerCooldown(myZombie)
。在任何操作之前,我们检查 require(_isReady(myZombie))
。这种简单的模式是我们游戏节奏的支柱,全部由区块链的时间戳驱动。
没有进度的游戏是什么?我们希望奖励玩家升级他们的僵尸,使其拥有新的能力。与其为每个级别编写一个单独的修饰器,不如创建一个单一、强大且可重用的工具。
制作 aboveLevel
修饰器:
此修饰器接受参数,使其非常灵活。
// _level 是我们传入的最低要求
// _zombieId 是我们正在检查的僵尸
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_; // 如果为真,则继续。如果为假,则恢复。
}
解锁能力:
现在,将函数置于等级要求之后变得清晰而直观。
// 等级 2 解锁名称更改
function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
// 等级 20 解锁 DNA 更改——强大的终局能力
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
这种模式是代码可重用性的杰作。我们现在可以添加无数与任何级别相关的新能力,只需最少的简单代码。
在这里,我们遇到了 Solidity 中最令人费解的事情之一。我们如何获得单个地址拥有的所有僵尸?直观上“高效”的解决方案实际上是一个消耗 gas 的陷阱。
诱人但错误的方式:
mapping (address => uint[]) public ownerToZombies; // 这是一个 gas 陷阱!
为什么这是一个陷阱?因为转移。从存储中间的数组中删除僵尸是一场消耗 gas 的噩梦。它需要移动所有后续元素,从而导致数十次昂贵的存储写入。对于大型军队来说,成本变得不可预测,并且可能是天文数字。
优雅的解决方案:按需迭代。
我们在免费的 view
函数中使用 for
循环。虽然这在传统编程中看起来效率低下,但它是链上的最佳解决方案。
function getZombiesByOwner(address _owner) external view returns (uint[] memory) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
// 迭代现有的每个僵尸
for (uint i = 0; i < zombies.length; i++) {
// 检查所有者是否匹配
if (zombieToOwner[i] == _owner) {
// 如果匹配,则将其添加到我们的结果数组中
result[counter] = i; // 'i' 是僵尸的 ID
counter++;
}
}
return result;
}
权衡:我们接受 view
函数(对用户免费)中的一次性计算成本(循环),以避免在 transfer
或 create
等状态更改函数期间产生大量且重复的存储成本。这是一种违反直觉的、区块链原生的思维方式,它定义了专家智能合约设计。
结论
我们已经从一个简单的数据存储转变为一个动态的游戏世界。我们的僵尸现在可以在计时器上运行,通过关卡升级并解锁新的能力,并且可以有效地查询,而不会增加存储成本。
这才是区块链开发的真正艺术所在:既是游戏设计师,又是 gas 经济学家。通过利用 Solidity 的原生特性并做出明智的架构权衡,你可以创建由安全、去中心化的区块链完全驱动的深度参与体验。
舞台已经搭建完毕。我们的僵尸已经过训练、升级和组织。他们已经准备好战斗了。在我们的下一章中,我们将把它们彼此释放,并构建任何伟大游戏的核心:战斗系统。
准备好迎接战争吧!
- 原文链接: blog.blockmagnates.com/b...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!