查看原文
其他

全链游戏引擎 MUD 从0到V2

MetaCat MetaCat 2023-12-05

作者: LATTICE
翻译:MetaCat
排版:MetaCat

Lattice 致力于开发 MUD 引擎已经近两年了。在这两年里,我们发布了 MUD 的两个主要版本、三款游戏、数百万个实体和无数的工程时间。随着 MUD v2 的完成以及 Open Zeppelin 即将完成的审核,我们想回顾一下并分享我们是如何走到这一步的。是什么让我们如此执著于创建一个开发人员可以用来创建雄心勃勃的应用程序的框架?一路走来,我们遇到了哪些发现、挑战和突破?为了继续我们推动 EVM 应用程序极限的使命,我们下一步对 MUD 有何计划?

请继续阅读以了解更多信息。

来自宇宙的迹象

那是 2021 年,链上游戏正处于起步阶段。《黑暗森林》的成功证明了对完全可修改和可扩展的世界的需求,这些世界可以自主存在并且无需管理员的铁腕。该游戏拥有规模虽小但热情充沛的玩家群,数十个用户构建的插件,并激发了用户协调的公会和联盟的创建。代码是开源的;自治之神很高兴。

不太确定的是,人们会如何尝试建造下一个《黑暗森林》。或者任何与此相关的链上游戏。如果你查看过《黑暗森林》代码库,你会发现一系列非常适合游戏的做法,但不适合简单的抽象。《黑暗森林》使用的格式是:

  • 游戏中事物类型的结构(即Planet,,Player……)

  • 每个结构的映射(以及用于跟踪键的数组)

  • 每种存储类型的 getter 函数

  • 以及任何数据更改时发生的事件(PlanetInitialized、PlanetUpgraded等)

  • 客户端(Planets等)上类似的自定义数据结构,手动加载初始状态和自定义“reducer”来监听 PlanetInitialized, PlatetUpgraded 等事件

当然,这是有组织的。但它是定制化的,并没有提供简单的框架供其他游戏使用。

在网络方面,存在可扩展性问题。为了从合约中访问游戏状态,《黑暗森林》核心开发者使用了 getter 函数,而这些函数无法被 RPC 缓存。因此,这对于 xDai 的 RPC 来说非常昂贵。当《黑暗森林》回合运行时,RPC 成本高达数万美元。

在当时,这些都是事实:为 EVM 设计游戏的挑战是公认的。没有简洁的框架,也没有可复用的代码,一切都是定制化的。

受到《黑暗森林》的启发,我们决定开发一款新游戏,一款桌游和大逃杀游戏的结合体,名为《ZKDungeon》。回想起来,这或许是一项过于雄心勃勃的事业。ZKDungeon 使用了与《黑暗森林》相同的方法,但我们很快意识到开发游戏的大部分时间都花在更改网络堆栈上(更新结构、更新事件、更新客户端上的事件处理逻辑)。这是一个要求很高且笨重的过程,这使得迭代游戏变得困难。我们采用了钻石标准(Diamond Standard),但它并不是万能的:我们仍然需要在智能合约发生变化时更新钻石存储(Diamond storage)。

回想起来,这有点像一场噩梦。很难相信,就在不久前,开发者就是这样构建游戏的。

MUD V1 的起源:开采钻石

我们希望生活在这样一个世界:开发人员不需要管理链上和链下状态之间的同步,并且开发人员可以轻松更新应用程序代码。我们思考了如何将状态管理问题从开发人员手中抽象出来的解决方案,并得出的结论是:我们需要一个链上数据结构,该结构在更新时自动发出事件,客户端可以使用该数据结构进行复制链上状态。但这如何实现呢?

大约在同一时间,我们开始更广泛地研究游戏开发,并遇到了 ECS(Entity Component System)模式。在框架中,状态存储在组件(Component)中,逻辑存在系统(System)中,实体(Entity)是组件中状态的关键,系统通过读取实体“属性”来“作用于”组件中的实体。这似乎是一种抽象,可以减轻我们在定制逻辑中看到的很多痛苦。这似乎也是将逻辑分离为单独的合约,并将逻辑与状态分离的好方法,因为它将是一种“协议内”钻石标准。

当时没有其他人在链上使用 ECS,所以这有点像一场赌博。关于如何在 Solidity 中实现 ECS 也没有明确的路线图,因此我们必须从“第一性原理”出发来思考,以确定如何实现这一目标。

首先,我们需要一个对于任何类型的状态都具有相同签名的事件。换句话说,我们必须从状态更新中删除类型,并拥有一个通用的伞类型(umbrella type)。所有类型的共同点是“bytes”,本质上是“无类型”数据。所以我们知道我们的更新事件必须使用“bytes”来表示状态

但是如何将自定义类型转换为 bytes,同时仍然能够在以后对其进行解码呢?Solidity 有一个称为“ABI encoding”的原语,因此通过使用 abi.encode 你可以将几乎任何内容转换为“字节块”(a blob of bytes)。如果你知道该类型,则可以使用 abi.decode 它将其恢复为原始形式。我们使用这种机制来实现“组件”(Component),即存储数据的地方。

每个组件都是它自己的合约,它实现了一个带有状态设置器和获取器的接口。在内部,它以 bytes 形式发出事件,并将原始 ABI 编码字节存储在 storage 中。然后我们将每个组件的类型存储在另一个组件中,并用它来解码客户端上的状态。“系统”(System)也是他们自己的合约。索引器(indexer)可以侦听所有这些事件、解码数据并将其存储在数据库中。

一个中央合约将所有这些联系在一起,我们称之为“世界”(ECS 中常用的术语,表示“全局状态”)。“系统”(System)会从“世界”(World)上的“组件”(Component)注册表中获取它们关心的组件的地址,然后读写组件上的状态。每个“组件”都有自己的访问控制,控制谁可以写入它们。

每次更新组件时,它都会在中央 World 合约中注册状态更新,该合约会发出一个事件,链下索引器可以使用该事件来复制状态。这意味着不再有特殊事件发出的合约,也不再有自定义网络代码来确保这些事件可以从外部读取,无论是在客户端还是在 The Graph 等索引工具上。

这种方法很有效,它让我们的迭代速度比以前快得多。这种方法最终成为 MUD v1。

发展速度到底有多快?为了测试 MUD v1,我们决定构建一款受 Minecraft 启发的游戏,我们将其称为 OPCraft。在我们设计的 ECS 框架和引擎的支持下,我们只花了两天时间就完成了最初的概念验证。ECS 设计原语如下:

  • 每个块都是一个实体,附加有一个 TerrainType 和 Position 组件

  • 当开采块时,该 Position 组件将被删除,并增加一个 OwnedBy 组件。这使得代码库变得非常简单。

然后我们又花了三个月的时间,完成从原型到最终的抛光好产品。这涉及到在 Soldity 和 WebAssembly 中添加“柏林噪声”(perlin noise)实现,以便在开采区块之前按程序渲染地形,以及其他改进,例如“随机”在地图中放置钻石的方法,以及对图形、声音效果和控件的整体更新。

我们还为玩家位置添加了一个链下点对点广播机制,以及一个根据谁在区块中投入最多钻石,来声明区块所有权,以保护其免受其他玩家侵占的机制(该机制后来被未具名用户用来创建 OPCraft 自治人民共和国)。我们在 OPCraft 中看到的新兴行为,证明了我们关于 MUD 的另一个理论:它不仅对构建雄心勃勃的应用程序的第一方开发人员有用,它的抽象也很容易让第三方开发人员构建插件和 mod

我们在 2022 年 Devcon 期间在第一个 Optimism Bedrock 测试网上运行了 OPCraft。我们的目标是进行为期两周的游戏测试,其目标是技术演示和 MUD v1 的加强。虽然游戏测试成功,但我们遇到了一个大问题:状态膨胀在游戏测试结束时,加载客户端状态需要大约 20 分钟,即使使用索引器也是如此(而非直接从 RPC 区块链节点加载数据)。这部分是由 OPCraft 的架构(为每个块安排唯一的实体,而不是每个玩家的每个块类型仅使用一个实体)引起的,部分是由 MUD v1 的幼稚且低效的数据编码(abi.encode)引起的。事情可能会更好。

于是我们就回到了航站楼。我们知道我们还没有完成 MUD 的开发。

从 MUD V1 到 MUD V2:重返天空

MUD v2 的出现是为了解决三个主要问题:我们的数据编码方法、我们的 MUD 原生数据模型以及 ECS 模型的局限性。2023 年初,alvarius 将这些问题整理成了两个现在标志性的 Github issues,一个关于数据建模(https://github.com/latticexyz/mud/issues/347 ),另一个关于World 框架(https://github.com/latticexyz/mud/issues/393 )。

正如上一节所提到的,abi.encode MUD v1 中的方法是幼稚且低效的。它使用了大量的填充(每个值占用 32 个字节,与数据大小无关,并为元数据添加了另外 32 个字节),因此事件中发出数据,并将数据存储在 storage 中不是最佳的。

对于 MUD v2,我们改进了许多事情:我们实现了更高效的数据编码,不会向链上存储和事件添加不必要的填充。我们还开发了一种更先进的索引器,以紧凑打包的形式而不是填充的形式存储数据(这有助于缓解状态膨胀)。

但还有其他一些限制需要克服。ECS 模型将开发人员限制为,数据库中只能使用单个主键(primary key),并且这种模式只有具有游戏背景的人才熟悉,而熟悉关系数据库的一般开发人员并不了解。MUD v1 中的数据模型是这样的:实体 ID 是主键,它拥有与其关联的组件。但在关系数据库(如 Postgres)中,你可以拥一张有多列作为主键(复合主键)的表(例如,多个键的组合唯一标识一行)。我们希望 MUD v2 以更通用的方式表示数据,并支持 multiple key-values(就像在所有传统关系型数据库所支持的那样)。

我们能够对表中存储的数据实现非常高效且紧凑的“位打包”(bitpacking)。

这意味着在 MUD v2 中添加多个键值,将使我们以一种将表中的行数从块总数的大约 3 倍(三个表,每个表每个块一行)减少到大约用户数 x 块类型的数量(这是原始表数量的 0.06%)的方式构建 OPCraft。考虑到 OPCraft 中开采了大约 1000 万个区块,但只有大约 2000 名玩家和大约 10 种块类型)。MUD v2 中的这种数据建模为我们的下一款游戏《Sky Strife 》的开发铺平了道路。

此外,在 MUD v1 中,每个组件都有自己的合约,系统由客户端直接调用,并且没有放置共享逻辑(例如访问控制或帐户委托等功能)的中心位置。我们想将所有这些抽象出来;开发人员唯一应该关心的是,他们正在使用的数据类型。他们应该能够在配置中定义数据,并生成与数据交互的 Solidity 库。为此,我们实现了一个中央存储引擎。

中央存储引擎解锁了很多东西:它改善了开发人员的体验,并为账户委托(account delegation)或存储钩子(storage hooks)等功能开辟了道路,这些功能发生在“系统”(System)之上的一层。以前开发人员必须为每个“系统”创建帐户委托逻辑,因为每个“系统”都是单独调用的。相反,现在一切都由中央 World 合约管理。

《Sky Strife》的发展以及游戏所展示出的需求,给 MUD 带来了进一步的提升的空间。这方面的一个例子是 MUD 的分段状态能力。Sky Strife 比赛的时间很短,只有大约十五分钟,而且许多比赛同时进行。如果你的客户端必须加载所有比赛的状态,而不是玩家在特定时间参与的比赛的状态,这将是昂贵且低效的。为了使这一点成为可能,MUD World 现在可以使用“复合主键”来定义状态片段,索引器和客户端都可以忽略客户端不关心的键。尽管它最初是出于扩展《Sky Strife》和《OPCraft》的需要,但它现在对所有 MUD 开发人员开放。

通过改进的数据编码、更适合数据库的数据模型以及从 ECS 迁移到关系数据库的功能,MUD v2 的更新已经完成。2023 年 9 月,我们选择 Open Zeppelin 作为 MUD v2 的审核方,他们正在审核相关合约和代码生成的库。

下一步是什么?

接下来,我们对“模块”(Modules)感到兴奋,模块是表、系统和其他资源的捆绑包,可以安装到现有已部署的 World 中,并用新功能扩展它们。模块可以开发和部署一次,然后安装在任意数量的 World 中。我们计划建立一个“模块”的链上注册表,有点像 npm,但是针对 MUD Worlds。在以太坊上,有许多大家共享的库(例如 Open Zeppelin 库),但没有“包管理器”,每个人都专门为自己的项目重新部署相同的库。使用模块,你只需实现一次,然后就可以将它们部署到任何可能的现有 World 中。

数十个团队正在使用 MUD 进行构建,在 v2 审核完成后,我们可以正式推荐其在主网部署中的使用。考虑到我们最近发布了 Redstone:一个超级划算的游戏和 World 链,这一点尤其令人兴奋。随着我们接触到更广泛的受众,我们将继续改进框架,并突破以太坊应用程序的极限。我们很高兴看到链上应用和自主世界的下一个时代开始。

我们要感谢构建 MUD 的团队、在 Discord 上提交反馈的开发人员以及 MUD 代码库的所有贡献者,是他们使我们的工作成为可能。

原文链接:https://lattice.xyz/blog/mud-zero-to-v2

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存