Curio是如何把ECS游戏引擎内置到OP Stack中的?
5月31日,Curio(@0xcurio)开源了 Keystone,一个内置了游戏Tick和ECS全链游戏引擎的L2链,该链基于OP Stack制作。相比通过智能合约编写 ECS 状态,这种设计允许所有 ECS 操作(如查询和状态设置)具有更快的性能。通过自定义预编译,智能合约可以访问底层的 ECS 链状态。游戏逻辑可以用 Go 语言编写,而不是 Solidity,这可以大规模并行化。本文将对Curio项目本身,以及它的原理做深度剖析,并探讨它是如何实现上述目的的。
Curio 由工程师和游戏玩家 Kevin Zhang (@kzdagoof)和 Yijia Chen (@0x1plus)于 2022 年创立,致力于制作完全由智能合约驱动的全链游戏。这使得一种新的多人计算方式成为可能,允许所有参与者为「共享宇宙」做出贡献,创始人表示这让游戏可以几乎完全由玩家创造。该公司的第一款游戏 Treaty 是一款链上策略游戏,用户可以在其中编写和部署智能合约。2023年2月21日,Curio宣布完成 290 万美元种子轮融资,本轮融资由 Bain Capital Crypto 领投,TCG Crypto、Formless Capital、Smrti Lab、Robot Ventures、Zonff Partners 和多家天使投资人参投。
全链游戏目前有多种叙事方式,常见的有去中心化游戏(DeGame),自治世界(Autonomous Worlds),Curio提出了自己的想法:用户生成逻辑(User Generated Logic),简称UGL。我估摸着,Curio在制作自己的第一款全链游戏 Treaty的过程中,遇到了不少的困难,就萌生了制作自己的第一个链并且把游戏引擎内置进去,这个想法和 Argus不谋而合,区别在于Argus采用了分片机制,而Curio走了捷径,直接采用OP Stack。
MUD 的 ECS 游戏框架
我们先来看MUD的ECS框架是如何工作的?如果您看过我写的科普文章《深度解析全链游戏引擎MUD》(),那应该了解到ECS 通过将逻辑、数据和实体分离,提高了游戏开发的灵活性和可维护性。编程语言采用 Solidity,游戏对象的属性状态储存在智能合约中。以 ERC-20 合约为例:ERC-20 合约将每个地址的代币余额存储在一个映射中(从 address 到 uint256 余额)。我们可以将每个 ERC-20 合约视为一个具有两列的表:"地址" 和 "余额"。这对应于具有单个模式值("余额")的组件。表中的每行都将一个实体("地址")与一个组件值("余额")关联起来。一个地址可以在许多独立的 ERC-20 合约中持有余额,这对应于一个实体与许多独立的组件值关联。在当前的 ERC-20 参考实现中,状态和逻辑是耦合在同一个合约中的。在 ECS 中,将有一个通用的 "转账系统" 来处理从一个地址向另一个地址转账代币的逻辑,通过修改代币组件中存储的状态。
因为MUD里面的游戏对象属性状态储存在智能合约,所以ECS的状态更改(客户端的状态同步到区块链节点)每次都会通过智能合约来同步,而这个同步的过程无法并行进行且需要频繁调用合约。所以Curio希望通过引入预编译合约来解决。
预编译合约
以太坊虚拟机(EVM)中的预编译合约是一种特殊类型的智能合约,其代码直接硬编码在以太坊节点的代码中,而不是在 Solidity 或其他 EVM 兼容语言中编写的。这种合约通常用于执行复杂的计算任务,因为硬编码的实现通常比在 EVM 中解释执行的代码更高效。预编译合约通常用于优化性能和降低 gas 成本。
实现预编译函数涉及以下步骤:
1. 选择地址:预编译函数需要一个地址。以太坊选择了 `1` 到 `ff`(包括 `ff`)这些地址来存储预编译合约。
2. 实现功能:预编译函数需要实现某种功能。这通常涉及到一些复杂的计算,例如椭圆曲线操作或大整数运算。这个函数通常使用 Golang 或 C++ 编写,然后直接集成到以太坊节点的代码中。
3. 计算 gas 成本:预编译函数需要一个函数来计算其运行所需的 gas。这个函数应该根据输入数据的大小和操作的复杂性来确定 gas 成本。
4. 集成到以太坊节点:预编译函数需要集成到以太坊节点的代码中。这通常涉及修改以太坊节点的代码以添加新的预编译合约,并重新编译和部署节点。
然后,智能合约可以通过调用预编译合约的地址来使用这个预编译函数。EVM 将检查该地址是否存在预编译合约,如果存在,EVM 将直接调用节点代码中的硬编码函数,而不是在 EVM 中解释执行合约代码。
需要注意的是,添加新的预编译函数需要对以太坊的协议进行修改,这通常需要通过社区的共识。此外,由于预编译函数是硬编码在以太坊节点的代码中的,因此每个运行这个新版本的以太坊节点都需要包含这个新的预编译函数的代码。这意味着在实践中,添加新的预编译函数是一个复杂且需要深思熟虑的过程。
Curio 的解决方案
我们在上面预编译合约的讨论中,可以发现,虽然使用预编译合约可以极大的提高性能,但是需要修改链的节点代码以及重新编译和部署节点。以太坊主链根本做不到这点。于是Curio选择了 OP Stack,在这个定制化的Layer2里面,他们修改了节点代码以添加ECS的预编译合约。正是通过这个自定义的预编译合约,智能合约可以直接访问底层的 ECS 链状态。因此,这种设计允许所有 ECS 操作(如查询和状态设置)具有更快的性能。又因为OP Stack 节点使用的是“Go-Ethereum”客户端,这就允许 Keystone 游戏逻辑可以用 Go 语言编写,而不是 Solidity,这可以大规模并行化。
Keystone中的ECS主要是通过'engine'和'game'这两个目录的代码实现的。
在'engine'目录中,我们看到了定义了ECS的主要数据结构,例如World
和Component
。World
是一个ECS基础世界结构,它包括实体(Entities)和组件(Components)两个主要部分。每个Component
包含一个数据类型(DataType),以及用于存储实体到值(EntitiesToValue)和值到实体(ValueToEntities)的映射。
在'game'目录中,我们看到了一些特定的游戏组件,如位置组件(PositionComp)、目标位置组件(TargetPositionComp)、标签组件(TagComp)等。这些组件都有各自的数据类型和是否需要存储值到实体的标志(ShouldStoreValueToEntities)。
集成了游戏引擎的链的优点
通过上面的讨论,我们可以看到,将ECS(Entity-Component-System)状态直接构建到定制化的区块链中,可以比通过智能合约编写ECS状态实现更快的性能。这种性能提升来自以下几个方面:
1. 数据结构优化:在智能合约中编写ECS状态通常需要使用一些非优化的数据结构,而在Keystone中,它们可以使用高效的数据结构(如稀疏集合)来存储和操作ECS数据,从而提高查询和设置状态的速度。
2. 避免智能合约执行开销:EVM(以太坊虚拟机)需要解释执行智能合约的代码,这会带来一定的开销。然而,将ECS状态直接构建到区块链中可以避免这种开销,因为ECS操作是在区块链的核心代码中直接执行的,而不是通过解释执行智能合约。
3. 并行化:Keystone允许在Go中编写游戏逻辑,这意味着可以利用Go的并行和并发特性来提高ECS操作的速度。这在智能合约中是无法实现的,因为EVM是单线程的。
4. 预编译合约:通过使用预编译合约来访问ECS状态,可以提高ECS操作的速度。预编译合约是在区块链节点代码中直接硬编码的函数,执行速度比在EVM中解释执行代码要快。
5. 状态更新优化:Keystone采用了一种方法,允许在子世界中进行状态更新,然后将这些更新应用到父世界。这种方法可以减少不必要的状态更新,从而提高状态设置的速度。
游戏节拍器在哪里
GameTick的概念通常在游戏开发中用于管理游戏内的时间进程。每一个tick代表了游戏主循环的一个周期,各种游戏事件可以根据这些ticks进行调度。这也是我们说传统游戏是“loop-based”的原因。
而区块链的状态本身并不包含我们通常理解的"当前时间"的概念。区块链是基于区块的概念运作的,这些区块按照线性顺序被添加到链中。虽然这些区块通常包含一个时间戳,但这并不像在传统计算环境中那样被用作"当前时间"的度量。
Curio宣称集成了GameTick在区块链中(GameTick built into the chain),但是我查遍了整个代码库,也没找到关于GameTick的代码片段,所以对 Keystone 如何实现在区块链中的游戏节拍感到十分好奇,希望Curio有机会可以对这个功能做更多的细节说明。不过我对此的一个猜测是这样的,在Keystone的GameTick环境中,next_tick字段可能用于确定游戏循环的下一个周期应该在何时发生,这基于区块链节点服务器的内部时钟,它被用来管理游戏时间在游戏自身的内部逻辑内的进程,与区块链内区块的进程是分开的。