ECS: 设计模式

从 OOP 与面向数据的差异出发,整理 ECS 中实体、组件和系统的基本设计动机。

面向数据与 ECS

在进行编程时,我们的本质工作是对实际存在的事物进行某种程度的抽象,将概念抽象方便在编程中表述的通用逻辑。

对于事物的划分有许多办法,最为流行的也许是 面向对象 OOP,通过将 概念 本身抽象成为对象和它们的实例来进行编程,这种方法称为面向对象设计

也有其它不一样的方法,在游戏和高性能计算领域,面向数据 的设计模式非常受欢迎,通常来说,这种方法将 概念 本身的 组成部分 单独抽象出来成为独立的 部分,并由这些 部分 来组成最终的 对象

面向数据 相比较 面向对象 的最大优势是灵活性和性能, 单独抽象出来 部分 被一起存放,这将有利于计算机 Cache Line, 除了在缓存方面更高效之外, 通常也有利于进行 并行计算

如果说 面向对象 是让人类高兴的设计模式,那么 面向数据 则是让机器高效,我们知道计算设备的主要瓶颈往往不在于 计算 本身,而在于等待 I/O, 面向数据 的设计往往是以特殊的结构来组织数据,使得计算设备等待 I/O 的时间大幅减少,最终达成加速整体计算的目的

这里的 I/O 指的是任何意义上的 I/O,例如: 寄存器,cache,内存,硬盘 ,它们的速度逐个递减,计算核心电路不得不等待更多的 时钟周期 来获取数据, 如此一来,目标很明确,最大程度的减少加载数据需要等待的 时钟周期 , 例如避免需要多次跳转或者多条 load 指令等等

也可以看看:AOS (Array of Structs), SOA (Struct of Arrays), AOSOA (Array of Struct of Array)

ECS (Entity-Component-System) 是一种常见的 面向数据 设计实践,它在游戏领域被广泛应用,并且在类似需要存在大量实体的程序中展现出很大潜力和价值,它有以下特点

  • 组成 实体 (对象) 的 部分 连续存储在称为 组件数组 (所以它们被称为组件) 中
  • 操作逻辑与数据相互独立,逻辑函数接受数据块作为输入,而非通过对象方法调用,这被称为 系统

我们可以看到,这样的模式中,不再有 对象 的存在,取而代之的是 实体, 之所以采用不同的术语是因为 实体 本身由构成它的 组件 们所描述,想象以下一个简单的 ECS 模式

Positions [] = [1, 0, 3]
Velocities[] = [3, 5, 9]
 
Entity 0:
   Positions [0]
   Velocities[0]
Entity 1:
   Positions [1]
   Velocities[1]
Entity 2:
   Positions [2]
   Velocities[2]

可以看到 实体 本身只是其组件数组中对应的 索引,例如 实体 0 的 位置是 Positions [0] = 1, 而它的速度则是 Velocities[0] = 3

这里只是一个简单的例子,实际的 ECS 系统如果照此设计显然将存在大量问题,例如

  • 即使某些 实体 不存在一些组件,我们仍然需要在对应的 组件数组 中存放其索引位置的元素,即使它应该是无效的,这显然会浪费大量内存,并且造成空洞
  • 我们无法知道哪些 实体 拥有哪些 组件,我们必须在每个系统中迭代所有 组件 来测试它们是否满足我们所需的 组件 要求