Lattice (@latticexyz) 的 MUD 是 Web3 领域最为久远和出名的全链游戏引擎,在之前的第一代版本时,曾清晰地表明,MUDv1 是基于 ECS 的一套框架,我也写了一篇相关的介绍文章《深度解析全链游戏引擎 MUD》。而几周前,Lattice 公布了 V2 版本,对 MUD 整体架构做了巨大改变,弱化了与 ECS 的联系,引入了一个新的链上数据库 “Store”,该数据库基于 “table” 的数据结构,那么应该如何理解 “ECS” 和 “table” 的关系?新组件 “Store” 到底是如何工作的?“ECS” 到底适不适合制作全链游戏?我们通过本文一探究竟。
MUDv2 概览#
MUDv2 是一个用于构建以太坊应用的框架。它通过一个紧密集成的软件栈来压缩构建 EVM 应用的复杂性。MUDv2 包括:Store(一个链上数据库)、World(一个入口点框架,提供标准化的访问控制、升级和模块)、基于 Foundry 的快速开发工具、能够反映链上状态的客户端数据存储,以及 MODE(一个可以使用 SQL 查询并反映你链上状态的 Postgres 数据库)。
MUDv2 不是一个 rollup 或者链,它是一套可以一起工作以构建链上应用的库和工具。MUDv2 不限定于以太坊主网,它可以在任何 EVM 兼容的链上工作。MUDv2 不仅仅适用于自治世界和链上游戏,也不会强制将数据模型强加给开发者,你可以用 MUD 做任何你可以用扁平的 Solidity 映射和数组做的事情。MUDv2 的数据可用性方案,与 ENS 和 Uniswap 这样的常规部署在以太坊主网的应用一样。
MUDv2 主要思想是,所有链上状态都保存在 Store(MUD 链上数据库)中。我们目前处理智能合约状态的方式导致了一些主要问题,例如,状态和逻辑的耦合使得升级逻辑非常困难。使用 MUD,你永远不会使用 Solidity 编译器驱动的数据存储。所有状态都使用 Store 保存和检索,这是一个高效的链上数据库。Store 就像 SQLite:它是一个在 Yul 中手动优化的嵌入式数据库。它有表格,有列和行。
MUDv2 逻辑是无状态的,且具有自定义权限,可以跨合约调用。MUD 推荐使用 World:一个入口点 kernel,负责从不同的合约中调解对 Store 的访问。World 在部署时创建一个 Store。每个 Store 中的表格都在一个命名空间下注册,用一个名字表示,就像一个扁平的文件系统路径。
MUDv2 无需索引器或子图,前端会自行同步:当使用 Store(以及扩展的 World)时,你的链上数据自我检查,任何改变都通过标准事件进行广播。这些事件和模式会被 MODE 利用:MODE 将你的链上状态转化为一个 SQL 数据库,并保持毫秒级的延迟更新。
ECS 的本质是什么#
通过查阅文献,我个人觉得(作者 @hicaptainz 不是开发人员如有错误请指正),ECS(实体 - 组件 - 系统)模式本质是一种数据结构的建模方式,它的核心在于如何存储和组织数据。
-
实体(Entity):在 ECS 模式中,实体是一个抽象的概念,它并不直接持有数据,而是通过组件来关联数据。实体可以被看作是一个或多个组件的容器,它的主要作用是为组件提供一个唯一的标识。
-
组件(Component):组件是数据的载体。在 ECS 模式中,所有的数据都被封装在组件中。每个组件都代表了一种特定的属性或者行为,例如位置、速度、颜色等。组件只包含数据,不包含任何逻辑或行为。
-
系统(System):系统是处理数据的地方。系统会根据实体的组件来决定如何处理这些实体。每个系统都有一个或多个特定的任务,例如渲染、物理模拟、AI 逻辑等。
从这个角度来看,ECS 模式的确是一种数据结构建模的方式。它将数据(组件)和行为(系统)分离,使得数据的存储和处理更加灵活和高效。这种模式的优点在于:
-
可组合性:通过组合不同的组件,可以创建出具有各种属性和行为的实体,而不需要创建大量的类或结构。
-
数据局部性:由于组件只包含数据,因此可以将相关的数据紧密地存储在一起,提高缓存利用率,从而提高性能。
-
可重用性:系统只关心数据,而不关心数据来自哪个实体,因此可以在多个实体之间重用同一个系统。
-
并行性:由于数据和行为的分离,使得在多线程环境下对数据的并行处理变得更加容易。
ECS 数据结构建模的几种类型#
那么可以实现 ECS 的数据结构方式到底有哪几种呢?一般来说,比较流行的两种是 Archetype 和 Sparse set ,其他不太常见的还有 Bitset 和 Reactive ECS。它们各自有其优点和适用场景。
-
Archetype(也叫 Table based ECS):Archetype (原型)是一种优化了数据局部性的存储方法,它将具有相同组件集的实体存储在一个表格中,其中组件是列,实体是行。Unity 的 ECS 系统就采用了这种方法。Archetype 的优点在于,由于相同的组件集在内存中是连续的,因此可以极大地提高缓存效率和数据访问速度。此外,由于每个 Archetype 都知道它包含的组件,因此可以在运行时动态地创建和销毁实体,而无需进行大量的内存操作。
-
Sparse Set (也叫 Sparse ECS):Sparse Set(稀疏集)是一种高效的数据存储和访问方法,它结合了数组和哈希表的优点。基于稀疏集的 ECS 将每个组件存储在自己的稀疏集中,该集合以实体 ID 为 key。这种方法的优点是可以有效地利用缓存,同时也可以节省空间。然而,这种方法的缺点是实现起来比较复杂。
至于 Bitset 和 Reactive ECS,它们也有其独特的优点,但可能并不适合所有的应用场景。
-
Bitset:Bitset 是一种简单且高效的方法,用于检查实体是否具有特定的组件。然而,由于 Bitset 本身无法存储组件数据,因此通常需要与其他存储方法结合使用。
-
Reactive ECS:Reactive ECS 是一种更高级的模式,它允许系统对组件的添加、删除和修改进行响应。这种模式可以使代码更加清晰和易于理解,但可能会增加实现的复杂性。
如果有读者看过我的文章《Curio 是如何把 ECS 游戏引擎内置到 OPStack 中的?》,立马可以发现,Curio 采用的正是 Sparse Set(稀疏集)的方式来储存和操作 ECS 数据,那么 MUDv2 呢?显然就是 “Archetype” 了,也就是 “table based ECS”。
Store 组件的工作原理#
MUDv2 的 Store 组件提供的数据模型可以很好地支持 ECS 模式。在 Store 中,你可以创建一个表格来表示实体,每个实体都有一个唯一的键。你可以为每个实体添加各种组件,这些组件可以是表格中的字段。然后,你可以编写系统来处理这些组件,这些系统可以是智能合约中的函数。
例如,你可以创建一个 "Player" 的表格,它有 "position" 和 "health" 两个字段。然后,你可以编写一个 "move" 的函数来改变玩家的位置,一个 "damage" 的函数来减少玩家的生命值。所以,虽然 MUD 的 Store 组件并不直接实现 ECS 模式,但它提供的数据模型可以很好地支持 ECS 模式的实现。
也许有人会问,这种基于 table 的链上数据库也支持 OOP(面向对象编程)模式吗?
Store 组件提供了一种基于表格的数据模型,这种模型更接近于关系型数据库或者说是数据驱动的设计,而不是面向对象编程(OOP)的模型。在面向对象编程中,数据和操作这些数据的方法被封装在对象中,而在 Store 的模型中,数据被存储在表格中,操作数据的逻辑则由智能合约中的函数来处理。
然而,这并不意味着你不能在智能合约中使用面向对象的设计模式。你可以创建一个智能合约来表示一个对象,这个智能合约可以有一些状态变量来表示对象的属性,也可以有一些函数来表示对象的方法。然后,你可以使用 Store 来持久化这些对象的状态。
例如,你可以创建一个 "Player" 的智能合约,这个智能合约有 "position" 和 "health" 两个状态变量,以及 "move" 和 "damage" 两个函数。然后,你可以使用 Store 来存储每个 "Player" 实例的 "position" 和 "health"。所以,虽然 Store 的数据模型并不直接支持面向对象编程,但你可以在智能合约中使用面向对象的设计模式,并使用 Store 来持久化对象的状态。
但正如我们一直强调的,对于全链游戏来说,ECS 模式还是更加合适一些,因为在这里的一个大优势是,新的系统(System)可以在开发的任何阶段引入,并且会自动与具有正确组件的任何现有和新的实体匹配。这促进了一种设计,其中系统被开发为单一责任,小的功能单位,可以轻松地部署到不同的项目中,从而实现全链游戏的 “可组合性”。
链上数据的储存#
既然 Store 组件是建立一个链上数据库,那么这些链上数据到底是存在什么地方呢?我们先回忆一下 EVM 的储存方式。
在以太坊虚拟机(EVM)中,链上数据主要存储在两种数据结构中:存储(Storage)和内存(Memory)。
-
存储(Storage):这是每个智能合约的持久化存储,它的数据在交易之间是持久的。存储是由键值对组成的,其中键和值都是 256 位的。在 Solidity 中,合约的状态变量就存储在这里。例如,如果你在合约中定义了一个
uint256 public counter;
,那么这个counter
变量就存储在存储中。每次你调用改变counter
的函数,它就会在存储中更新。 -
内存(Memory):这是每个函数调用的临时存储,它的数据在函数调用结束后就会被清除。内存是线性的,可以想象成一个字节数组。在 Solidity 中,函数的局部变量就存储在这里。
此外,还有一个叫做栈(Stack)的数据结构,它用于存储函数调用的参数和返回值,以及一些临时的计算结果。栈的大小是有限的,最多可以包含 1024 个元素。
在 EVM 中,读取和写入存储的成本都是非常高的,因为它需要消耗 gas。因此,智能合约的开发者通常会尽量优化他们的代码,以减少对存储的操作。相比之下,读取和写入内存的成本要低得多,但是由于内存的数据在函数调用结束后就会被清除,所以它只适合存储临时的数据。
MUDv2 的 Store 组件正是将数据存储在 EVM 的存储(Storage)中。每个智能合约在 EVM 中都有自己的存储空间,这个存储空间是持久的,也就是说,即使在交易之间,存储中的数据也会保持不变。
Store 组件提供了一种更高级的抽象,使得开发者可以更方便地在智能合约中存储和读取数据。开发者可以定义自己的表格,每个表格都有一个特定的模式,定义了表格中的字段和它们的类型。然后,开发者可以使用 Store 提供的 API,如 set、get 等函数,来操作这些表格中的数据。
这些操作最终都会被转换为对 EVM 存储的读写操作。例如,当你调用 MyTable.set 函数来设置一条记录时,这个函数会将数据编码为字节,然后写入到 EVM 的存储中。当你调用 MyTable.get 函数来获取一条记录时,这个函数会从 EVM 的存储中读取数据,然后将数据解码为你定义的类型。
被忽略的状态机问题#
我们在上文的分析中,似乎看到的都是 ECS 模式的优点,那么真的就没有缺点吗?这里就要从 EVM 自身和状态机讲起了。
状态机,又称状态自动机,是描述对象状态变化以及如何响应输入(比如事件或者条件)的模型。在状态机中,系统在任何给定时间都处于某种状态,并且会在输入的影响下从一个状态转移到另一个状态。在传统游戏引擎中,状态机通常用于管理游戏对象(如角色、敌人、AI 等)的行为状态。这些状态可能包括行走、跑动、跳跃、攻击、防御、死亡等。状态机可以帮助我们清晰地定义和控制游戏对象在不同状态之间的转换。
对于 EVM(Ethereum Virtual Machine),我们可以把它看作一个全球分布式的状态机。在 Ethereum 网络中,每个区块都会包含一系列的交易,而这些交易就是输入。EVM 会执行这些交易,根据交易的内容来更新 Ethereum 的全局状态。Ethereum 的全局状态包含了所有账户的余额信息,智能合约的代码,以及智能合约存储的数据等等。当一个交易被执行时,它可能会转移资金,调用合约,或者改变合约的存储状态,这些都会导致 Ethereum 的全局状态发生改变。
所以对于全链游戏来说,在游戏引擎中储存状态机是一件必须要做的事情。但是在 ECS 模式中储存状态机(特别是一个分布式状态机),会有一系列的问题。
状态机在 ECS 中的问题:使用 ECS 开发游戏,可能会遇到是否要在 ECS 中存储状态机的问题。这个问题比预期的要复杂得多,原因有很多。例如,更改实体的状态、让实体参与多个状态机、获取当前状态的实体、更改状态机的状态列表等操作在 ECS 中都有挑战。
标签状态的问题:一种直观的实现状态机的方式是为每个状态创建一个标签。这种方法在查询给定状态的所有实体时表现良好,因为 ECS 实现通常擅长找到给定组件 / 标签的所有实体。然而,其他操作,如更改状态,需要添加一个标签并删除另一个标签,这可能会引入复杂性和性能问题。
我们具体到 Archetype 这种数据结构来分析。
实施 Archetype 的方法是将所有具有相同组件集合的实体聚集在一张表中。避开具体细节,这种方式为组件的高效迭代缓存提供了便利,因为可以连续迭代多个组件。但是,这也带来了一些代价。
为了保持具有相同组件集的实体的聚合,每次添加或删除组件时,都需要在不同的表间移动实体。这同样适用于标签。因此,频繁地添加或删除和状态有关的标签可能会变得非常昂贵,因为每次状态变化时都需要复制实体的所有组件(实际上,由于存储方式的原因,每个组件需要被复制两次)。
为了解决上面的问题,Bevy ECS 的作者提出了一种新的方法来实现存储变体:将 Archetype 与表格分开。在这种方法中,表格只存储实体的(数据)组件,而 Archetype 存储组件 + 标签。多个 Archetype 可以指向同一个表格。有了这个,应用程序可以在常数时间内添加和删除标签(以及状态),这比必须复制所有组件有了巨大的改进。
也许,MUDv3 会朝着这个方向改进,我们看看它在未来是否会在 table 之外新加一个 Archetype 存储组件吧。
小结#
现在我们来试着回答开始的问题。table 是实现 “Archetype ECS” 的一种建模方式,MUDv2 并没有弃用 ECS,只不过刚好 table 这个组件也可以实现 OOP 的架构。这个 table 是通过 Store 组件来生成的,并且储存在 EVM 的 Storage 里面,因此所有对 table 的读写操作都会转化为对 Storage 的读写操作。虽然 ECS 架构用来储存 EVM 状态机有不少的困难,但是仍旧有不少办法来帮助解决。如果您对这些优化方法感兴趣,可以关注作者 twitter(@hiCaptainZ)来获取最新信息。作者目前在 Gametaverse(@GametaverseDAO)任职 Head of Research,曾获北卡大学光学工程硕士和香港中文大学金融硕士,目前研究领域为 Web3 Gaming,AI 和 ZKP,预计未来几周的研究课题如下:
- 传统游戏引擎与 Web3 游戏引擎的对比分析
- 如何确定游戏的可玩性
- Web3 中的博彩游戏与 GambleFi 的崛起
- 全链游戏的可组合性到底有哪些
- Web3 游戏的经济系统设计
- 不同游戏题材如何影响 Tokenomics 的设计
- ECS 架构是不是一种范式(优势和劣势)
- 如何从零到一设计一种 ECS
- AI 如何辅助链游制作
- ZK 零知识证明全赛道一览
- ZK 技术与机器学习 ZK-ML