2

我已经看到了大量关于如何在实体组件系统中存储信息的不同的、相互矛盾的建议——一些吹捧“纯度”或缓存优化,没有太多解释。作为一般总结:

  1. 实体列表,将组件存储在其结构中(例如,某种unordered_map<type_info, IComponent*>C++ 中的)。系统会跟踪它们自己的列表,其中当前活动的实体拥有它们所操作的组件。

  2. 与 1 相同,除了系统全局迭代所有实体并检查它们包含哪些组件。

  3. 每种类型的组件都有一个单独的列表,并且没有实际存储的“实体”列表 - 系统迭代它们的相关组件,并且必须以某种方式找到属于同一实体的其他关联组件,通过一些唯一的 ID 连接他们。支持将组件保存在这样的列表中,因为据说可以改善缓存局部性(尽管我不知道如何,因为您仍将搜索几个大列表以查找特定实体上的相关组件),并且没有实际的 ' entity' 类型应该是“纯” ECS 的标志。

  4. 每个组件类型都有自己的全局容器/列表,但仍然有一个实体结构列表,用于跟踪哪些组件属于某个特定实体。系统的行为与 1 或 2 相同。

我还发现一些支持“每个系统一个组件类型”的争论——这将简化系统 3 的一些挑战,但总体上没有什么意义。

所以我的问题是 - 消除不同实现的所有噪音,这些中的任何一种都是制作 ECS 的“理想”或“规范”方式吗?很难分析每种设计的优缺点以及它们的许多不同变化。我通常按​​照上面列表中的“1”来实现它们,事实证明这很方便,但不一定是最佳的。

4

1 回答 1

0

不幸的是,我认为我不能为您提供比“视情况而定”更好的方法,因为任何接近理想主义的东西都将与特定引擎的设计要求及其程序员的敏感性有关。不过,我也许可以就您列出的各个部分提供一些想法。

  1. 实体列表,将组件存储在其结构中(例如,C++ 中的某种 unordered_map<type_info, IComponent*>)。系统会跟踪它们自己的列表,其中当前活动的实体拥有它们所操作的组件。

如果您偏爱 ECS 架构设计以提高效率而不仅仅是其灵活性(尽管偏爱 ECS 只是为了灵活性并没有错),将组件列表直接存储在单个实体内部而不是并行存储在外部(并且只有一个列表来存储所有组件相同类型的)通常会因为超出每实体组件查找的原因而与该目标背道而驰。

ECS 的过程(或有时是功能)范式经常与面向对象编程进行对比。这包括性能。在最关键的执行路径中,OOP 的基本性能缺点之一是面向对象的设计倾向于交叉/捆绑数据,而不是基于最佳内存访问模式,而是基于人为因素和 SE 因素,以便封装和维护不变量。ECS 与此相反,它立即打破封装,支持将逻辑(系统)与数据(组件)分离。这样的设计立即为更优化的数据表示和内存布局以及热/冷拆分等优化开辟了空间,但如果您最终将所有此类数据直接存储/捆绑/交织在实体内部,则不会。

因此,如果我要冒险尝试任何类似规范的东西,并利用 ECS 可能提供的全部好处,它不会将组件数据直接存储在实体内部。它将它们存储在外部,例如,很少访问的数据可以与经常访问的数据分开和分离,而不是交错在一起并浪费地加载到关键路径中的缓存行中。

  1. 与 1 相同,除了系统全局迭代所有实体并检查它们包含哪些组件。

根据我的理解,这将是#1 所必需的。由于上述类似原因,这将是次优的。

每种类型的组件都有一个单独的列表,并且没有实际存储的“实体”列表 - 系统迭代它们的相关组件,并且必须以某种方式找到属于同一实体的其他关联组件,通过一些连接的唯一 ID他们。支持将组件保存在这样的列表中,因为据说可以改善缓存局部性(尽管我不知道如何,因为您仍将搜索几个大列表以查找特定实体上的相关组件),并且没有实际的 ' entity' 类型应该是“纯” ECS 的标志。

这通常需要甚至有可能接近最佳参考位置。最简单的例子之一是热/冷场分裂。考虑这样一个案例:

struct Foo
{
    // Accessed all the time every single frame.
    int32_t x, y;

    // Accessed hardly ever and only by the UI.
    uint64_t id;
};

Foo在这种情况下,我们将在关键执行路径中浪费每个 CPU 缓存行的一半内存,通过加载这个字段来顺序迭代数组,该id字段甚至不会在这些路径中与x和一起访问y。使用 ECS,我们可以将经常访问的字段分开,x并且y,与很少访问的字段分开,id,通过创建两个或多个单独的组件类型来存储它们以及两个或多个组件列表。关键执行路径甚至不会费心访问包含很少访问字段的列表。如果我们考虑 SIMD 矢量化等,它会更加复杂,但是这个热/冷场拆分示例应该是开始理解为什么这种分离从性能角度非常有益的最容易的地方。

即使系统加载了多个组件类型列表/数组,它们也不会加载不会在其顺序循环中访问的组件类型数据。可以这么说,它们不会加载到不相关的列表中。此外,以顺序方式访问多个并行数组并不一定更慢(实际上经常与这种情况相反,尤其是当它为矢量化提供更多机会时)。对于交错的 AoS 代表的随机访问,它们确实往往较慢,但不是顺序访问。

  1. 每个组件类型都有自己的全局容器/列表,但仍然有一个实体结构列表,用于跟踪哪些组件属于某个特定实体。系统的行为与 1 或 2 相同。

原型就是这样的例子之一,尽管它确实是特定于实现的。为简单起见,我首先要观察到,我们必须能够对数据进行细粒度分离,以便组织数据以获得最佳访问模式。

我还发现一些支持“每个系统一个组件类型”的争论——这将简化系统 3 的一些挑战,但总体上没有什么意义。

从我的角度来看,这是荒谬的。除了非常严格之外,您还可以在那时以面向对象的方式对事物进行编程,并在您的组件中拥有方法,并使它们的数据成员私有,并在只有一个地方可以访问它们的情况下获得封装的好处。如果我们将自己限制为每个系统使用一种组件类型,我们就会放弃封装和信息隐藏的好处,而几乎没有什么好处。

于 2020-11-02T08:27:44.063 回答