【问题标题】:What is the 'canonical' or best way of associating components with particular entities in an ECS?将组件与 ECS 中的特定实体相关联的“规范”或最佳方式是什么?
【发布时间】:2020-08-17 10:10:05
【问题描述】:

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

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

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

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

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

我还发现了一些支持“每个系统一个组件类型”的争论——这将简化系统 3 的一些挑战,但总体而言意义不大。

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

【问题讨论】:

    标签: architecture game-engine entity-component-system


    【解决方案1】:

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

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

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

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

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

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

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

    每种类型的组件都有一个单独的列表,并且没有 实际存储的“实体”列表 - 系统迭代其相关 组件,并且必须以某种方式找到其他关联的组件 属于同一个实体,通过一些连接它们的唯一 ID。 应该支持将组件保存在这样的列表中 改善缓存局部性(虽然我不知道如何,因为你仍然会 正在搜索几个大列表以查找相关组件 特定实体),并且没有实际的“实体”类型是 据说是“纯”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;
    };
    

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

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

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

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

    我还发现一些人支持“每个系统使用一种组件类型”——这将简化系统 3 的一些挑战,但总体而言意义不大。

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

    【讨论】:

      猜你喜欢
      • 2018-10-01
      • 2012-07-06
      • 2011-03-29
      • 2020-02-24
      • 2018-05-16
      • 2019-11-05
      • 1970-01-01
      • 2013-09-30
      • 1970-01-01
      相关资源
      最近更新 更多