【问题标题】:Designs of an entity component system实体组件系统的设计
【发布时间】:2014-07-06 11:06:37
【问题描述】:

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

首先,关于术语:

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

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


1。 “天真的”方式

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

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


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

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

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
    }
}

优点

  • 比天真的方式更快

缺点

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

3。不使用系统

Danvilanswer below 中描述了此方法。

优点

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

缺点

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

4。使用备用套件

skypjackanswer below 中描述了此方法。他非常详细地解释了他的方法,所以我建议你阅读他的回答。

【问题讨论】:

    标签: c++11 theory entity-component-system


    【解决方案1】:

    我会说你所谓的“系统”实际上是一个组件。渲染示例:有一个组件 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();
            }
        }
    };    
    

    【讨论】:

    • 我将此设计添加到上面的列表中。它当然是设计#3 的替代方案,但我不确定它将如何改变代码的清晰度。另外,如果某些组件没有“快速链接”会发生什么?您将不得不查找它们,这会再次导致大量dynamic_casting。例如,在 Rendering::render(...) 内部,如果没有 Entity::renderer 链接,则您必须找出 每个 Entity 是否存在该链接。 (希望你明白这一点,因为我不太确定这是否表达得很好)
    • 确实如此,但没有必要对每次调用“render”都执行此操作。请参阅Renderer 中的mesh 示例。代码冗长/不完整,但您应该明白这一点。或者,您可以使用 OnComponentsChanged 函数来通知组件实体的组件已更改并且需要更新链接。
    • 是的,我明白了,但这并不能回答我评论的第二部分(关于循环的东西)
    • @MartinD.:“快速链接”有点像你的布尔值。每个系统都需要一个。如果由于某种原因这太侵入性,那么系统本身可以管理链接(如#3)。但这需要一些额外的代码来添加/删除/更改实体。
    • @MartinD。我认为这对于第一个原型来说是一个不错的选择。只是不要使用位掩码。正如您所指出的,它们根本无法扩展。
    【解决方案2】:

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

    主要好处是:

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

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

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

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

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

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

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

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


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

    【讨论】:

    • 我真的很喜欢这种方法。我想知道,为了进一步提高缓存一致性,您是否尝试过“物化连接”?我看待 ECS 的方式是,它本质上是一种传统的数据库布局。每个组件都是一个属性表,以实体 ID 作为主键进行索引。例如,物理系统将加入位置和速度表(即组件)来进行处理。您将执行此连接的有效方法描述为哈希连接。也许这个合并的结果可以在帧之间缓存?
    • @BuschnicK 我用group functionalityEnTT 实现了一种物化视图。如果您对该主题感兴趣,我还会写一系列关于不同解决方案的帖子(here 是第 1 部分,here 是第 2 部分)。未来还会有更多。
    • @BuschnicK 博客文章(第 2 部分)已经包含有关组功能的一些详细信息。可能我以后会写更多关于它的内容。很高兴知道有人感兴趣。 ;-)
    • @BuschnicK this 是您要找的。让我知道你的想法。 ;-)
    • 好的,谢谢。在您在这里发布之前已经通过我的 RSS 阅读器阅读了它?
    猜你喜欢
    • 2018-11-10
    • 2011-07-04
    • 2018-01-05
    • 1970-01-01
    • 2016-01-23
    • 2016-11-05
    • 2018-08-13
    • 2016-09-01
    • 2020-10-18
    相关资源
    最近更新 更多