诺亚·布拉格的首次 Stoke Fire 直播
过去一年里,我一直对 Ethereum 很感兴趣,尤其是 Base 生态。问题在于,即便花了好几个小时阅读关于 Base 的资料,我仍然没搞懂 Base 到底是什么。
每隔几个月,我都会回头去 Base 官网的开发者版块看看,是否有适合新手在 Base 上构建项目的入门路径,而得到的指引似乎总是“这里有一些针对非常具体事项的零散教程,如果有问题,就来 Discord 上问我们。”
所以,当看到我在 Twitter 上关注的一位独立创始人 Noah Bragg(诺亚·布拉格) 开始直播他在 Base 生态之上构建一款简单游戏的过程时,我感到很兴奋。

独立创始人 诺亚·布拉格 正在直播在 Ethereum 区块链上构建游戏的过程。
我观看了 诺亚·布拉格的首次直播,并在下方分享了我的收获。
游戏:Stoke Fire
- Stoke Fire 是一款资源管理游戏,玩家在村庄中砍柴为持续燃烧的篝火添柴,以保持村庄温暖。

- 灵感来源
- Age of Empires II(《帝国时代2》),诺亚·布拉格小时候玩过。
- A Dark Room(《黑暗房间》),一款基于文本的网页游戏。
- Manor Lords(《庄园领主》),一款由单人开发者耗时六年打造的全面战争模拟游戏。
- 游戏将采用免费游玩模式以鼓励参与。
- 游戏会将存档状态保存在 NFT(非同质化代币) 中,以便在不同钱包之间转移,不过诺亚·布拉格预计人们不会像交易艺术类 NFT 那样去交易游戏存档 NFT。
- 他选择在 Base 区块链上构建,因为他认为得益于 Coinbase 的投入,消费者会聚集在那里。
抢先体验
- Stoke Fire 的抢先体验可通过诺亚·布拉格在 Hypersub 上的 I Must Build 订阅获得。
- Hypersub 就像是链上版的 Patreon。
直播中发生了什么
- 诺亚·布拉格在游戏中加入了用木材建造小屋的功能。
- 他开始实现根据村庄可用资源吸引村民的功能,但由于精神疲惫,最终中途暂停了开发。
Solidity
- 诺亚·布拉格正在使用 Solidity 进行 Smart Contract(智能合约) 开发。
来自 Michael(迈克尔)的备注:Solidity 依然让我感到厌恶。在一个对正确性和可读性要求至关重要的领域,Solidity 却引入了大量不必要的陷阱和坑。感觉他们像是孜孜不倦地研究了 C++ 和 JavaScript,只为把这些语言中最糟糕的特性照搬过来。
Diamond
- Diamond 是一个用于部署发布后可变的 Smart Contract 的框架。
- 通常情况下,Ethereum 上的 Smart Contract 一旦部署就不可更改。
- Diamond 提供了“可升级的” Smart Contract,因此你可以在部署后对其进行修改。
- Diamond 会削弱 Smart Contract 的保障,但它为像 Stoke Fire 这样的项目提供了迭代便利。
- “Facet(切面)”是(我认为是)Smart Contract 中可修改的部分。
来自迈克尔的备注:管理 Facet 似乎很繁琐,也是诺亚·布拉格所展示代码中最脆弱的部分。他必须手动声明多个不同的数组,然后在添加新的 Facet 时手动对其进行索引,有一次还在一处隐藏的声明中费了好大劲才让 Facet 数量与数组大小对上。聊天区的评论者说可以使用辅助工具自动获取 Facet,所以也许有更简单的方法来实现这一点。
Forge
- 诺亚·布拉格正在使用 Forge 测试他的 Smart Contract 逻辑。
- 诺亚·布拉格对 Forge 评价积极。
- 他喜欢 Forge 允许他用 Solidity 编写测试,因为这样可以与生产环境的 Smart Contract 共享大量代码。
- Forge 让测试不同的区块链条件(如时间戳和钱包余额)变得很容易。
- 诺亚·布拉格有时难以解析 Forge 的输出,而我个人则觉得其输出极其冗杂、难以阅读。

我觉得 Forge 的测试输出非常冗杂
- 当 Forge 中的断言失败时,它不会打印导致失败的行号。在直播中的某个时刻,Forge 打印的失败信息仅仅是
5 != 4,开发者得自行去源码中定位断言的位置。我从未见过哪个测试框架在断言失败时不打印行号。
- 当 Forge 中的断言失败时,它不会打印导致失败的行号。在直播中的某个时刻,Forge 打印的失败信息仅仅是
- 为了断言诺亚·布拉格的生产代码抛出了特定错误,他必须在测试代码中重新定义该错误,这让我觉得很奇怪。
Warpcast 有网页应用
- Farcaster 就像是 Ethereum 版的 Twitter 或 Mastodon。
- Farcaster 给人的印象是 Warpcast 只有移动端,但通过诺亚·布拉格的直播我才意识到 Warpcast 也有网页应用。
- 我认为你仍然需要移动应用来创建账户。
直播
- 诺亚·布拉格的直播峰值有 250 名观众。
- 最后发现,他不确定这个数字是指同时在线观众数,还是在任意时间点收看过直播的总累计人数。
- 大多数观众来自 Twitter。
- 他坦言自己对直播已经生疏,直播过程中出现了很多冷场。
改进机会
便捷的开发脚本
有一次,诺亚·布拉格花了好几分钟试图回忆如何只运行单个测试,而不是每次都执行整个测试套件。在聊天区用户的帮助下,他最终找到了语法:
forge test --match-contract BuildFacetTest -vvvv对于难以记忆的命令行语法,我建议编写一个便捷脚本并将其保存在仓库中。这样,你就无需在众多技术栈中记忆运行单个测试的正确语法,而只需运行如下命令:
./dev-scripts/run-single-test BuildFacetTest我在好几个仓库中都这样做。
诺亚·布拉格已经在使用 make,所以这些也可以做成 make 命令。
让测试更易于维护
诺亚·布拉格为建造小屋功能编写的最终集成测试如下所示:
function testBuildHut() public {
vm.prank(USER);
ResourceFacet(address(diamond)).chopWood(1, someWhatRandNum);
vm.warp (1719981068 + 1 days); //set the block time to the future so I can chop wood again
vm.prank(USER);
ResourceFacet(address(diamond)).chopWood(1, someWhatRandNum);
vm.prank(USER);
BuildFacet(address(diamond)).buildHut(1);
Village memory village = VillageFacet(address(diamond)).getVillage(1);
assertEq(village.timeLastChoppedWood, block.timestamp);
assertGe(village.wood, 4); //at minimum 2 wood per chop.
assertLe(village.wood, 12); //at maximun 6 wood per chop.
assertEq(village.huts, 1);
}我认为这个测试有几处可以改进的地方。
首先,该测试依赖于一个让逻辑变得非常混乱的 pseudorandom number generator(伪随机数生成器)(PRNG)。在测试中,诺亚·布拉格已将 PRNG 设为固定种子值,以便随机序列在不同测试间重复出现,但这仍然让测试逻辑难以理解。
在直播中,诺亚·布拉格不得不盲目地在测试中不断增加砍柴次数,以观察何时能够通过。他最后干脆将随机种子改成了另一个值,以便只砍两次就能通过测试。也正因如此,该测试无法断言剩余木材的精确数量,而只能猜测它在某个范围内。如果不实际执行,就无法验证该测试的正确性。
如果是我来处理这段代码,我可能会为测试实现 mock 掉砍柴功能,让每次砍柴产生的木材数量可以精确指定。或者,如果想测试真实的木材产出代码,则使用一个会产生特定数字序列的 fake 随机数生成器,而不是硬编码一个种子。
另一个问题是,许多函数参数在调用处可读性很差。例如 buildHut(1) 或 chopWood(1, ...),不清楚其中的 1 代表什么,也不清楚测试中出现的多个 1 这些魔法数字是指同一个值,还是恰好都等于 1。
未解的问题
为什么要用区块链?
直播结束时,我最大的疑问是:为什么要用区块链?
诺亚·布拉格提到他想在区块链上尝试一些更具原创性的东西,这也正是我感兴趣的原因,但我仍然想不明白区块链能带来什么帮助。
到目前为止,区块链似乎让一切复杂了 10 倍,相比之下把所有东西都放在单实例的 SQLite 数据库中,并没有任何优势。
同样,对于我原本希望弄明白的“Base 到底是什么?”这个问题,我仍未找到答案。我仍然不确定诺亚·布拉格为什么要选择在 Base 上构建,而不是直接在 Ethereum 或其他链上构建。他提到了 Coinbase 的投入,但我不明白这对像诺亚·布拉格这样的开发者意味着什么。
好大的整数啊!
我注意到诺亚·布拉格经常在一些似乎完全没必要的地方使用 uint256,比如时间戳或木材数量。当游戏目前每次“砍柴”操作只奖励 2 到 4 块木材时,很难想象需要用 uint256 来存储结果。

如此大的整数类型难道不会增加 Gas(燃料费) 费用吗?
通过开发我自己的 EVM(Ethereum 虚拟机) 实现,我知道 Ethereum 网络会对需要处理的数据收费,因此将一个 256 位的值压入栈的成本是将 32 位字压入栈的 8 倍。(编辑:实际上压入一个 256 位字和一个 32 位字的成本是相同的。感谢 a14u 的指正。)
我不确定这只是一个疏忽,还是 Gas 费用实际上比我想象的要低。
为什么要用下划线?
诺亚·布拉格使用了一种命名约定,即在所有函数参数名前加上前缀下划线。

我从未见过在函数参数名前加下划线前缀的约定,也不明白诺亚·布拉格为什么要这么做。
我在 Python 和 JavaScript 中见过这种约定,用来向读者暗示该变量是私有的/受保护的,但函数参数不本来就是私有的吗?在我对 Solidity 代码有限的阅读中,我从未见过这种约定。
随机一篇博客