将相同组件集的实体分组到线性内存中


12

我们从基本的系统-组件-实体方法开始

让我们创建组合(源自长期文章)仅仅是出于对信息类型的组件。它是在运行时动态完成的,就像我们将组件一个接一个地添加/删除到实体中一样,但是让我们更精确地命名它是因为它仅涉及类型信息。

然后,我们构造为每个实体指定集合的实体。创建实体后,它的组合是不可变的,这意味着我们无法直接对其进行修改,但是仍然可以获得现有实体对本地副本的签名(以及内容),对其进行适当的更改,然后创建一个新实体它的。

现在是关键概念:每创建一个实体,就会将其分配给一个名为assemblage bucket的对象,这意味着具有相同签名的所有实体都将位于同一容器中(例如,在std :: vector中)。

现在,系统只是遍历他们感兴趣的每个环节并完成工作。

这种方法有一些优点:

  • 组件存储在几个(精确地:存储桶数)连续的内存块中-这提高了内存友好性,并且更容易转储整个游戏状态
  • 系统以线性方式处理组件,这意味着改进了缓存一致性-再见字典和随机存储器跳转
  • 创建新实体就像将组合映射到存储桶并将所需组件推回其向量一样容易
  • 删除一个实体就像调用std :: move一样容易,将最后一个元素与删除的元素交换,因为此时顺序并不重要

在此处输入图片说明

如果我们有很多具有完全不同的签名的实体,那么缓存一致性的好处就会减少,但是我认为大多数应用程序都不会发生这种情况。

重新分配向量后,指针失效也存在问题-可以通过引入以下结构来解决:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

因此,只要出于游戏逻辑中的某种原因,我们想要跟踪一个新创建的实体,就在存储桶中注册一个entity_watcher,并且一旦在删除期间必须将该实体std :: move移开,我们便会查找其观察者并进行更新他们real_index_in_vector的新价值。在大多数情况下,这对每个实体删除都仅施加了一个字典查找。

这种方法还有其他缺点吗?

尽管很明显,为什么没有提到解决方案?

编辑:我正在编辑问题以“回答答案”,因为评论不足。

您将失去可插拔组件的动态特性,该特性是为摆脱静态类构造而专门创建的。

我不。也许我没有足够清楚地解释它:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

就像获取现有实体的签名,对其进行修改并再次将其作为新实体上传一样简单。可插拔,动态性质?当然。在这里,我想强调的是,只有一个“组合”和一个“桶”类。存储桶由数据驱动,并在运行时以最佳数量创建。

您需要遍历可能包含有效目标的所有存储桶。没有外部数据结构,冲突检测可能同样困难。

好,这就是为什么我们拥有上述外部数据结构。解决方法很简单,只需在System类中引入一个迭代器即可检测何时跳转到下一个存储桶。的跳跃是纯粹透明的逻辑。


我还阅读了Randy Gaul的文章,内容是将所有组件存储在向量中,并让它们的系统对其进行处理。我在那里看到两个大问题:如果我只想更新实体的子集(例如考虑剔除)怎么办?因此,组件将再次与实体耦合。对于每个组件迭代步骤,我需要检查是否已选择其所属的实体进行更新。另一个问题是某些系统需要处理多种不同的组件类型,从而再次失去缓存的一致性。有什么想法如何处理这些问题?
tiguchi

Answers:


7

本质上,您已经设计了带有池分配器和动态类的静态对象系统。

我写了一个对象系统,它在我上学时的工作原理几乎与您的“组合”系统相同,尽管我总是倾向于在自己的设计中称“组合”为“蓝图”或“原型”。与天真的对象系统相比,该体系结构更容易让人感到痛苦,与我所比较的一些更灵活的设计相比,该体系结构没有可衡量的性能优势。在游戏编辑器上工作时,无需修改或重新分配对象即可动态修改对象的功能非常重要。设计人员将需要将组件拖放到您的对象定义上。运行时代码甚至可能需要在某些设计中有效地修改组件,尽管我个人不喜欢这样做。根据您如何在编辑器中链接对象引用,

在大多数非平凡情况下,您将获得比您想象的更差的缓存一致性。例如,您的AI系统不在乎Render组件,但最终陷入了作为每个实体一部分对它们进行迭代的困境。被迭代的对象更大,并且缓存行请求最终会提取不必要的数据,并且每个请求返回的整个对象更少。它仍然会比朴素的方法更好,并且即使在大型AAA引擎中也使用朴素的方法对象组合,因此您可能不需要更好的东西,但至少不要以为您无法进一步改进它。

您的方法对某些人来说确实最有意义组件,但不是全部。我强烈不喜欢ECS,因为它主张始终将每个组件放在单独的容器中,这对于物理或图形或其他方面都有意义,但如果允许多个脚本组件或可组合的AI则毫无意义。如果让组件系统不仅用于内置对象,而且还可以作为设计人员和游戏程序员编写对象行为的一种方式,则将所有AI组件(通常会交互)或所有脚本组合在一起是有意义的组件(因为您要批量更新所有组件)。如果您想要性能最高的系统,则将需要混合使用组件分配和存储方案,并花时间总结出哪种类型最适合每种特定类型的组件。


我说过:我们无法更改实体的签名,我的意思是我们不能直接就地对其进行修改,但是我们仍然可以仅将现有组合获取到本地副本,进行更改,然后作为新实体再次上传-这些正如我在问题中所显示的,操作非常便宜。再一次-只有一个“桶”类。可以按照标准方法在运行时动态创建“组合” /“签名” /“让我们随便命名”,甚至可以将实体视为“签名”。
Patryk Czachurski

我说过,您不一定要处理这个问题。“创建一个新实体”可能意味着打破该实体的所有现有句柄,具体取决于您的句柄系统的工作方式。您的通话是否足够便宜。我发现这是必须面对的痛苦。
肖恩·米德迪奇

好的,现在我对您的观点了。无论如何,我认为,即使添加/删除要贵一点,它也会偶尔发生,以至于值得大大简化实时访问组件的过程。因此,“改变”的开销是可以忽略的。关于您的AI示例,这几个系统仍然需要来自多个组件的数据仍然值得吗?
Patryk Czachurski

我的观点是,在AI方面,您的方法会更好,但是对于其他组件,不一定是这样。
肖恩·米德迪奇

4

您要做的是重新设计的C ++对象。这种感觉显而易见的原因是,如果将“实体”一词替换为“类”,将“组件”替换为“成员”,则这是使用mixins的标准OOP设计。

1)您失去了可插拔组件的动态特性,该特性是专门为摆脱静态类构造而创建的。

2)内存一致性在一种数据类型中最重要,而不是在一个位置统一多个数据类型的对象中。这是创建组件+系统以摆脱类+对象内存碎片的原因之一。

3)此设计还恢复为C ++类样式,因为当您在组件+系统设计中,实体只是一个标签/ ID,以使内部工作对人类而言易于理解时,您会将实体视为一个连贯的对象。

4)一个组件自己进行序列化本身比一个复杂对象对其内部的多个组件进行序列化一样容易,即使实际上不像程序员那样容易跟踪。

5)沿着这条路径前进的下一个逻辑步骤是删除系统,并将该代码直接放入实体中,该实体具有需要工作的所有数据。我们都可以看到这意味着=)


2)也许我不完全了解缓存,但可以说有一个系统可以处理10个组件。在标准方法中,处理每个实体意味着要访问RAM 10次,因为即使使用池,组件也会分散在内存中的随机位置中-因为不同的组件属于不同的池。立即缓存整个实体并处理所有组件而没有单个缓存未命中,甚至不必进行字典查找,是否“重要”?另外,我提出的编辑,以覆盖1)点
Patryk Czachurski

@Sean Middleditch在他的回答中很好地描述了这种缓存故障。
Patrick Hughes

3)无论如何它们都不是连贯的对象。约翰指出,关于组件A在内存中紧随组件B之后,它只是“内存一致性”,而不是“逻辑一致性”。铲斗在创建时甚至可以按照任何所需顺序对组件进行洗牌,并且仍将保留原则。4)如果我们有足够的抽象,“跟踪”可能同样容易-我们正在谈论的仅仅是一个带有迭代器的存储方案,也许字节偏移量映射可以使处理像在标准方法中一样容易。
Patryk Czachurski 2013年

5)我认为这个想法没有任何指向这个方向的。并不是我不想同意您的意思,我只是好奇这个讨论可能会带来什么,尽管无论如何它可能会导致某种“衡量”或众所周知的“过早优化”。:)
Patryk Czachurski

@PatrykCzachurski,但是您的系统无法使用10个组件。
user253751 '18

3

像实体一样保持实体并没有您想象的那么重要,这就是为什么除了“因为它是一个单元”之外,很难想到一个正当理由的原因。但是,由于您实际上是在为缓存一致性而不是逻辑一致性进行此操作,因此这可能很有意义。

您可能遇到的一个困难是不同存储桶中组件之间的交互。例如,找到您的AI可以射击的东西并不是一件容易的,您需要遍历可能包含有效目标的所有存储桶。没有外部数据结构,冲突检测可能同样困难。

为了继续组织实体以实现逻辑一致性,我必须将实体保持在一起的唯一原因是为了执行任务。我需要知道您是否刚刚创建了实体类型A或类型B,并且通过以下方式解决了这个问题:您猜对了:添加了一个新组件,该组件标识了将该实体组合在一起的组合。即使那样,我也没有将所有组件都聚集在一起完成一项艰巨的任务,我只需要知道它是什么。因此,我认为这部分不是非常有用。


我不得不承认我不太理解你的答案。您所说的“逻辑一致性”是什么意思?关于交互中的困难,我进行了编辑。
Patryk Czachurski 2013年

如下所示的“逻辑一致性”:将组成Tree实体的所有组件保持在一起是“逻辑上的”。
约翰·麦克唐纳
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.