有关C ++中实体组件系统之间链接的建议


10

在阅读了有关实体组件系统的一些文档之后,我决定实施我的。到目前为止,我有一个World类,其中包含实体和系统管理器(系统),Entity类(其中包含作为std :: map的组件)以及一些系统。我在世界中将实体作为std :: vector持有。到目前为止没有问题。让我感到困惑的是实体的迭代,我对此一无所知,所以我仍然无法实现这一部分。每个系统都应该保存他们感兴趣的实体的本地列表吗?还是应该仅遍历World类中的实体并创建一个嵌套循环以遍历系统并检查实体是否具有系统感兴趣的组件?我的意思是 :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

但是我认为,在嵌入脚本语言的情况下,位掩码系统会带来一定的灵活性。或者为每个系统使用本地列表将增加类的内存使用量。我非常困惑。


您为什么期望使用位掩码方法来妨碍脚本绑定?顺便说一句,在for-each循环中使用引用(如果可能,则为const)以避免复制实体和系统。
本杰明·克洛斯特

使用位掩码(例如int)将仅容纳32个不同的分量。我并不是在暗示会有32个以上的组件,但是如果有的话该怎么办?我将不得不创建另一个int或64bit int,它不会是动态的。
deniz

您可以使用std :: bitset或std :: vector <bool>,具体取决于您是否希望它是运行时动态的。
本杰明·克洛斯特

Answers:


7

每个系统都有本地列表将增加类的内存使用量。

这是传统的时空权衡

虽然遍历所有实体并检查其签名直接成为代码,但是随着系统数量的增加,它可能变得效率低下-想象一个专门的系统(让它输入),在数千个不相关的实体中寻找其可能感兴趣的单个实体。

也就是说,根据您的目标,此方法可能仍然足够好。

虽然,如果您担心速度,当然还有其他解决方案可以考虑。

每个系统都应该保存他们感兴趣的实体的本地列表吗?

究竟。这是一种标准方法,应该为您提供不错的性能,并且相当容易实现。我认为内存开销是可忽略的-我们正在谈论存储指针。

现在如何维护这些“兴趣列表”可能并不那么明显。对于数据容器,std::vector<entity*> targets系统内部的类就足够了。现在我要做的是:

  • 实体在创建时为空,不属于任何系统。
  • 每当我向实体添加组件时:

    • 获取其当前的位签名
    • 将组件的大小映射到足够大的块大小的世界池(我个人使用boost :: pool)并在那里分配组件
    • 获取实体的新位签名(只是“当前位签名”加上新组件)
    • 迭代通过世界上所有的系统,如果有一个系统的签名符合实体的当前签名与匹配新的签名,它变得很明显,我们应该的push_back指针到我们的实体存在。

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

删除实体是完全类似的,只有系统与当前签名匹配(这意味着该实体已经存在)并且与新签名不匹配(这意味着该实体不再应该存在)时,我们删除的唯一区别是)。

现在您可能正在考虑使用std :: list,因为从向量中删除是O(n),更不用说每次从中间删除时都必须移动大块数据。实际上,您不必-由于我们不在乎此级别的处理顺序,因此我们可以仅调用std :: remove并接受一个事实,即每次删除时,我们只需要执行O(n)搜索即可即将被移除的实体。

std :: list会给您O(1)删除,但另一方面,您还有一点额外的内存开销。还要记住,大多数情况下,您将处理实体而不是删除它们-当然使用std :: vector可以更快地完成。

如果您非常重视性能,则可以考虑使用另一种数据访问模式,但是无论哪种方式,您都可以维护某种“关注列表”。不过请记住,如果您将实体系统API保持足够抽象,那么如果帧速率由于这些原因而下降,那么改善系统的实体处理方法就不是问题-因此,现在,选择最容易编码的方法-仅然后分析并根据需要进行改进。


5

有一种方法值得考虑,其中每个系统都拥有与其自身关联的组件,而实体仅引用它们。基本上,您的(简化的)Entity类如下所示:

class Entity {
  std::map<ComponentType, Component*> components;
};

说了RigidBody附加到的组件后Entity,您可以从Physics系统中请求该组件。系统创建组件并让实体保留指向它的指针。您的系统如下所示:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

现在,乍一看可能有点反常,但是优点在于组件实体系统更新其状态的方式。通常,您会遍历系统并请求它们更新关联的组件

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

将系统所有组件都包含在连续内存中的优势在于,当系统迭代每个组件并对其进行更新时,它基本上只需要

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

它不必遍历所有可能不具有需要更新组件的实体,并且还可以实现非常好的缓存性能,因为所有组件都可以连续存储。如果不是这种方法的最大优点,那就是它。在给定的时间,您通常会拥有成千上万的组件,因此不妨尝试并尽可能地提高性能。

那时,您Worldupdate需要遍历系统并调用它们,而无需迭代实体。这是(恕我直言)更好的设计,因为这样系统的职责就更加清晰了。

当然,有无数种这样的设计,因此您必须仔细评估游戏的需求并选择最合适的游戏,但正如我们在这里看到的那样,有时微小的设计细节可能会有所作为。


好答案,谢谢。但是组件没有功能(例如update()),只有数据。然后系统处理该数据。所以根据您的示例,我应该为组件类添加虚拟更新,并为每个组件添加实体的指针,对吗?
deniz 2013年

@deniz这一切都取决于您的设计。如果您的组件没有任何方法,只有数据,那么系统仍然可以在它们上迭代并执行必要的操作。至于链接回实体,是的,您可以在组件本身中存储指向所有者实体的指针,或者让系统维护组件句柄和实体之间的映射。不过,通常情况下,您希望组件尽可能独立。完全不知道其父实体的组件是理想的。如果您需要朝这个方向进行交流,请选择事件之类的东西。
pwny

如果您说提高效率会更好,那么我会使用您的模式。
deniz

@deniz确保您确实早早地配置了代码,并经常确定对您的特定
引擎

好吧:)我会做一些压力测试
deniz 2013年

1

在我看来,一个好的架构是在实体中创建一个组件层,并在该组件层中分离每个系统的管理。例如,逻辑系统具有一些影响其实体的逻辑组件,并存储与实体中所有组件共享的公共属性。

此后,如果要在不同点或按特定顺序管理每个系统的对象,最好在每个系统中创建活动组件的列表。您可以在系统中创建和管理的所有指针列表都少于一个已加载资源。

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.