我经常听到赞扬的关于实体系统的两个主要好处是:1)由于不必纠缠复杂的继承层次结构,因此可以轻松构造新型的实体,以及2)缓存效率。
请注意,(1)是基于组件的设计的好处,而不仅仅是ES / ECS。您可以通过许多没有“系统”部分的组件来使用它们,并且它们可以正常工作(并且很多独立游戏和AAA游戏都使用这种架构)。
标准的Unity对象模型(using GameObject
和MonoBehaviour
objects)不是ECS,而是基于组件的设计。当然,较新的Unity ECS功能是实际的ECS。
系统需要能够使用多个组件,即渲染系统和物理系统都需要访问变换组件。
某些ECS按实体ID对它们的组件容器进行排序,这意味着每个组中的相应组件将以相同的顺序排列。
这意味着,如果要在图形组件上进行线性迭代,则还应在相应的转换组件上进行线性迭代。您可能正在跳过某些变换(因为您可能具有未渲染的物理触发体积等),但是由于您始终在内存中一直向前跳过(通常距离不是特别大),因此您仍会继续来提高效率。
这类似于将阵列结构(SOA)作为HPC的推荐方法。CPU和缓存可以处理多个线性阵列,几乎可以处理单个线性阵列,并且比处理随机内存访问要好得多。
在某些ECS实现中(包括Unity ECS)使用的另一种策略是根据组件的相应实体的原型来分配组件。也就是说,恰恰是一套组件的所有实体(PhysicsBody
,Transform
)将分别从不同组件的实体分配(例如PhysicsBody
,Transform
,和 Renderable
)。
这种设计中的系统通过首先找到满足其要求的所有原型(具有必需的组件集),迭代该原型列表以及迭代存储在每个匹配原型中的组件来工作。这允许在原型中进行完全线性和真实的O(1)组件访问,并允许系统以非常低的开销找到兼容的实体(通过搜索一小部分原型而不是搜索可能成千上万的实体)。
您可以让组件存储指向其他组件的指针,或者指向存储实体的指针的实体的指针。
引用同一实体上其他组件的组件不需要存储任何内容。要引用其他实体上的组件,只需存储实体ID。
如果单个实体允许一个组件存在多次,并且您需要引用一个特定实例,则存储该实体的另一个实体的ID和组件索引。但是,许多ECS实施不允许这种情况,特别是因为这会使这些操作效率降低。
您可以确保每个组件数组都为“ n”个大数组,其中“ n”为系统中存在的实体数量
使用句柄(例如索引+生成标记)而不是指针,然后可以调整数组的大小而不必担心破坏对象引用。
std::deque
如果您出于某些原因要允许指针,或者您已经测量到问题,您也可以使用类似于许多常见实现的“块数组”方法(数组的数组)(尽管没有所述实现的小块大小)。数组大小调整性能。
其次,所有这些都假设实体在每个帧/刻度内线性地在列表中进行处理,但实际上,这种情况并不常见
它取决于实体。是的,在许多用例中,这是不正确的。的确,这就是为什么我如此强调基于组件的设计(好的)与实体系统(CBD的一种特定形式)之间的区别的原因。
您的某些组件肯定很容易进行线性处理。即使在通常的“树木繁茂”的使用案例中,我们也绝对可以看到,使用紧密排列的阵列可以提高性能(大多数情况下,涉及N最多为数百的情况,例如典型游戏中的AI代理)。
一些开发人员还发现,使用面向数据的线性分配数据结构的性能优势超过了使用“更智能”的基于树的结构的性能优势。当然,这完全取决于游戏和特定的用例。
假设您使用扇形/门户渲染器或八叉树执行遮挡剔除。您也许可以在一个扇区/节点内连续存储实体,但是无论您是否喜欢它都将四处走动。
您会惊讶于该阵列仍能提供多少帮助。与“任何位置”相比,您在较小的内存区域中跳跃,即使进行了所有跳跃,您仍然更有可能最终出现在缓存中。使用一棵特定大小或更小的树,您甚至可以将整个对象预取到缓存中,而不会在该树上出现缓存未命中的情况。
还存在一些树结构,它们以紧密排列的数组形式存在。例如,对于八叉树,您可以使用类似堆的结构(父级先于子级,兄弟姐妹彼此相邻),并确保即使“钻取”树时也始终在数组中向前迭代,这有助于CPU优化了内存访问/缓存查找。
这是很重要的一点。x86 CPU是一个复杂的野兽。CPU在您的机器代码上有效地运行了一个微代码优化器,将其分解为较小的微代码并重新排序指令,预测内存访问模式等。 CPU或缓存的工作方式。
然后,您将拥有其他系统,它们可能更喜欢以其他顺序存储的实体。
您可以多次存储它们。用这种方法将数组精简到最低限度的细节后,您可能会发现实际上节省了内存(因为您删除了64位指针并且可以使用较小的索引)。
您可以插入实体数组而不是保留单独的数组,但是仍然浪费内存
这与良好的缓存使用情况相反。如果您只关心变换和图形数据,为什么还要让机器花时间为物理和AI提取所有其他数据,以及输入和调试等等?
这就是通常倾向于ECS与整体游戏对象的观点(尽管与其他基于组件的体系结构相比并不适用)。
值得一提的是,据我所知,大多数“生产级” ECS实施都使用交错存储。我之前提到的流行原型方法(例如,在Unity ECS中使用)非常明确地构建为对与原型相关联的组件使用交错存储。
如果AI不影响用于实体渲染的变换或动画状态,则AI是毫无意义的。
仅仅因为AI无法有效地线性访问转换数据并不意味着没有其他系统可以有效地使用该数据布局优化。您可以使用打包数组来转换数据,而不必阻止游戏逻辑系统按常规方式执行游戏逻辑。
您还忘记了代码缓存。当您使用ECS的系统方法(与某些更幼稚的组件体系结构不同)时,您可以确保运行的是相同的小代码循环,并且不会在虚拟函数表中来回跳转到Update
散落在各处的各种随机函数中您的二进制文件。因此,在AI案例中,您确实希望将所有不同的AI组件(因为肯定有多个组件,以便可以组成行为!)保存在单独的存储桶中,并分别处理每个列表,以便获得最佳的代码缓存使用率。
使用延迟的事件队列(系统会生成事件列表,但在系统完成对所有实体的处理之前不会调度事件),可以确保在保留事件的同时很好地使用了代码缓存。
通过使用一种方法,其中每个系统都知道要从该帧中读取哪些事件队列,您甚至可以使读取事件更快。至少比没有要快。
记住,性能不是绝对的。您无需消除所有最后的单个高速缓存遗漏,就可以开始看到优质的面向数据设计的性能优势。
仍在积极进行研究,以使许多游戏系统在ECS架构和面向数据的设计模式下更好地工作。与近年来我们用SIMD所做的一些令人惊奇的事情类似(例如JSON解析器),我们看到越来越多的ECS框架所做的事情对传统游戏体系结构而言似乎并不直观,但提供了许多好处(速度,多线程,可测试性等)。
也许有一种混合方法,每个人都在使用,但没人在谈论
这是我过去一直提倡的,尤其是对于那些对ECS体系结构持怀疑态度的人:对性能至关重要的组件使用面向数据的良好方法。使用更简单的体系结构,其中简单可以缩短开发时间。不要像ECS所建议的那样,将每个组件都塞进严格的组件化定义中。以这样的方式开发您的组件架构:您可以轻松地在有意义的地方使用类似于ECS的方法,而在没有ECS的意义(或者比树形结构更没有意义的东西)中使用更简单的组件结构。 。
我个人最近才转换为ECS的真正力量。尽管对我来说,决定因素是ECS很少提及的因素:与过去使用紧密耦合的基于逻辑的基于组件的设计相比,对游戏系统和逻辑的编写测试几乎微不足道。由于ECS体系结构将所有逻辑放入系统中,而这仅消耗组件并产生组件更新,因此构建一组“模拟”组件来测试系统行为非常容易;因为大多数游戏逻辑应该只存在于系统内部,所以有效地意味着测试所有系统将为您的游戏逻辑提供相当高的代码覆盖率。系统可以使用模拟依赖项(例如GPU接口)进行测试,其复杂性或性能影响比您少得多
顺便说一句,您可能会注意到,很多人在谈论ECS时并没有真正理解它的含义。我看到经典的Unity被称为ECS,它的使用频率令人沮丧,这说明太多的游戏开发人员将“ ECS”等同于“ Components”,并且几乎完全忽略了“ Entity System”部分。您会看到Internet上的ECS上充满了很多爱,而实际上大部分人只是在提倡基于组件的设计,而不是实际的ECS。在这一点上,争论起来几乎毫无意义。ECS已从其原始含义变为通用术语,您也可以接受“ ECS”与“面向数据的ECS”的含义不同。:/