6

我想知道如何用 C++ 实现最快版本的实体组件系统(ECS 从现在开始)。

首先,关于术语:

  • 场景实体(以及某些实现中的系统)的容器
  • 组件是一个简单的数据存储(如位置、碰撞框、要渲染的图像等)
  • 系统对符合系统要求的组件执行逻辑(这可能是物理、玩家输入、简单渲染等)
  • 一个实体包含几个组成最终行为的组件

我在下面列出了我们提出的所有设计。


1.“幼稚”的方式

场景包含所有无序的实体。
随着系统的更新,每个系统都必须遍历所有实体并检查每个实体是否包含所有必需的组件,然后对这些实体执行更新。

显然,当有很多系统和/或很多实体时,这种方式的性能不太好。


2.使用“位掩码枚举”和映射

每个组件都包含一个位掩码形式的类型标识符(例如1u << 5/ binary [0...]100000)。然后每个实体可以组成所有组件的类型标识符(假设所有类型 ID 在实体内部都是唯一的),所以它看起来像

1u << 5 | 1u << 3 | 1u << 1
binary [0...]101010

场景包含某种地图,系统可以在其中轻松查找合适的实体:

MovementSystem::update() {
    for (auto& kv : parent_scene_) { // kv is pair<TypeID_t, vector<Entity *>>
        if (kv.first & (POSITION | VELOCITY))
            update_entities(kv.second); // update the whole set of fitting entities
    }
}

优点

  • 比天真的方式更快

缺点

  • 系统每次更新时都必须查找适当的实体。
  • 位掩码(枚举)被限制为位数(32 位uint32_t,至少 64 位unsigned long long),在某些情况下,您可能需要比位掩码允许的更多组件。

3. 不使用系统

Danvil下面的回答中描述了这种方法。

优点

  • 完全摆脱位掩码。
  • 可能比设计#2 更快。

缺点

  • 依赖dynamic_cast于查找组件,而设计#2 可以直接查找组件,然后安全地查找static_cast它。

4. 使用备用套件

skypjack下面的答案中描述了此方法。他非常详细地解释了他的方法,所以我建议你阅读他的答案。

4

2 回答 2

4

我发现另一种很有前途的方法,我在我的一个项目中使用过(参见EnTTGitHub)是基于sparse sets
我在组件池中使用它们,以跟踪哪个实体具有关联的组件以及它的slot是什么。

主要好处是:

  • 您可以免费获得一小部分具有特定组件的所有实体(有关更多详细信息,请参见此处),当您迭代它们时,这将极大地提高性能。

  • 它将实际分配的组件数量保持在最低限度。此外,组件在内存中都保持紧凑。

  • 缓存未命中率至少会减少,因为您只需为使用的内容付费。换句话说,您只会得到那些实际分配给实体的组件,并且它们在内存中都彼此靠近,根本没有漏洞

您以一个最大长度等于每个组件池的实体数量的额外数组的价格获得它(请注意,通常这些数组在现实世界的软件中更小)。

基准测试表明,性能远优于基于位掩码描述符的知名实体组件系统(有关详细信息,请参阅上面的链接)。我还验证了内存压力或多或少是相同的,因为您摆脱了位掩码描述符数组,但您在组件池中引入了一组迷你数组。

在查找多个组件时对实体集的迭代也可以通过一个技巧得到高度改进:找到最短的集合并迭代其实体(一个非常快速的操作),然后验证第 n 个实体是否具有其他组件并最终返回它。
基准测试证明它仍然比基于密集集(每个实体都有所有组件)的基于位掩码的设计更快。如果集合不那么密集(对于现实世界的软件来说这是一个合理的假设),性能肯定优于基于位掩码的解决方案。

最后,与解决方案#4 不同的是,在这种情况下不需要动态转换。

整个事情给了你一些我称之为实体组件注册表的东西。系统可以定义为捕获注册表的 lambdas 或您可以将注册表传递给的函子。无需向注册表本身注册系统。


我希望你明白这个实现背后的想法。
如果您需要更多详细信息,请随时询问。

于 2017-05-18T21:04:07.267 回答
0

我会说你所谓的“系统”实际上是一个组件。渲染示例:有一个组件Pose(用于 3D 位置旋转)和一个组件Mesh(保存顶点缓冲区)。现在,不再使用检查它是否可以呈现该特定实体的函数,而是添加一个组件Renderer。该组件连接到PoseMesh组件。“系统”渲染现在只需与组件通信Renderer。并且每个实体要么是可渲染的,要么现在是可渲染的,不需要每次都检查组件,所有渲染工作都作为一个组件收集。


代码示例:

struct Pose : public Component { float x,y; };

struct Mesh : public Component { std::vector<Vertex> vertices; };

struct Renderer : public Component {
   Entity* entity;
   void render() {
       if(!mesh|| entity->componentsChanged) {
           mesh = entity->getComponent<Mesh>();
           if(!mesh) throw error;
       }
       if(!entity->pose) throw error;
       glTranslate(entity->pose->x, entity->pose->y);
       ...
   }
private:
   Mesh* mesh;
};

struct Entity {
    std::vector<Component*> components;
    bool componentsChanged;
    template<typename C> C* getComponent() const {
        for(Component* c : components) {
            C* cc = dynamic_cast<C>(c);
            if(cc) return cc;
        }
        return NULL;
    }
    // "fast links" to important components
    Pose* pose;
    Renderer* renderer;
    PhysicsStuff* physics;
};

struct Rendering
{
private:
    void render(const std::vector<Entity*>& entities) {
        for(Entity* e : entities) {
            if(!e->renderer) continue;
            e->renderer->render();
        }
    }
};    
于 2014-05-18T13:59:06.267 回答