如何通过缓存友好的组件存储安全地支持组件到对象的通信?


9

我正在制作一个使用基于组件的游戏对象的游戏,但是我很难为每个组件实现一种与其游戏对象进行通信的方式。我将不解释所有内容,而是解释相关示例代码的每个部分:

class GameObjectManager {
    public:
        //Updates all the game objects
        void update(Time dt);

        //Sends a message to all game objects
        void sendMessage(Message m);

    private:
        //Vector of all the game objects
        std::vector<GameObject> gameObjects;

        //vectors of the different types of components
        std::vector<InputComponent> input;
        std::vector<PhysicsComponent> ai;
        ...
        std::vector<RenderComponent> render;
}

GameObjectManager拥有所有的游戏对象及其组件。它还负责更新游戏对象。它通过按特定顺序更新分量向量来实现。我使用向量而不是数组,因此可以同时存在的游戏对象数量实际上没有限制。

class GameObject {
    public:
        //Sends a message to the components in this game object
        void sendMessage(Message m);

    private:
        //id to keep track of components in the manager
        const int id;

        //Pointers to components in the game object manager
        std::vector<Component*> components;
}

GameObject类知道它的分量,可以向他们发送信息。

class Component {
    public:
        //Receives messages and acts accordingly
        virtual void handleMessage(Message m) = 0;

        virtual void update(Time dt) = 0;

    protected:
        //Calls GameObject's sendMessage
        void sendMessageToObject(Message m);

        //Calls GameObjectManager's sendMessage
        void sendMessageToWorld(Message m);
}

Component类是纯虚拟的,这样类,针对不同类型的组件可以实现如何处理消息和更新。它还能够发送消息。

现在出现了有关组件如何sendMessageGameObject和中调用函数的问题GameObjectManager。我想出了两种可能的解决方案:

  1. 给它Component的指针GameObject

但是,由于游戏对象位于矢量中,因此指针可能很快变得无效(对于中的矢量也可以这样说GameObject,但希望该问题的解决方案也可以解决该问题)。我可以将游戏对象放在一个数组中,但是然后我必须传入一个任意数字作为大小,而这个数字很容易会不必要地变大并浪费内存。

  1. Component指针一个指针GameObjectManager

但是,我不希望组件能够调用管理器的更新功能。我是从事此项目的唯一人员,但我不想养成编写潜在危险代码的习惯。

如何在保持代码安全和缓存友好的同时解决此问题?

Answers:


6

您的通信模型看起来不错,并且只有您可以安全地存储这些指针,第一种方法才能正常工作。您可以通过为组件存储选择其他数据结构来解决该问题。

A std::vector<T>是一个合理的首选。但是,容器的迭代器无效行为是一个问题。您想要的是一种快速且具有缓存一致性的数据结构,可以迭代,并且在插入或删除项目时还可以保持迭代器的稳定性。

您可以构建这样的数据结构。它由页面的链接列表组成。每个页面具有固定的容量,并将所有项目保存在一个阵列中。计数用于指示该阵列中有多少项处于活动状态。页面还具有一个空闲列表(允许重复使用已清除的条目)和一个跳过列表(允许您在迭代时跳过清除的条目)。

换句话说,概念上类似:

struct Page {
   int count;
   int capacity;           // Optional if every page is a fixed size.
   T * m_storage;
   bool * m_skip;          // Skip list; can be bit-compressed.
   std::stack<int> m_free; // Can be replaced with a specialized stack.

   Page * next;
   Page * prior;           // Optional, allows reverse iteration
};

我将这个数据结构想象成一本书(因为它是您要迭代的页面的集合),但是该结构还有其他各种名称。

马修·本特利(Matthew Bentley)称其为“殖民地”。Matthew的实现使用跳转计数跳转字段(为MediaFire链接表示歉意,但这是Bentley自己托管文档的方式)在这种类型的结构中优于基于布尔的典型跳转列表。Bentley的库是仅标头的,并且易于插入任何C ++项目,因此,我建议您简单地使用它而不是自己滚动。我在这里介绍了很多微妙和优化。

由于此数据结构一旦添加项目就永远不会移动,因此指向该项目的指针和迭代器将保持有效,直到该项目本身被删除(或清除容器本身)为止。因为它存储大块连续分配的项目,所以迭代速度很快,并且大多与缓存保持一致。插入和移除都是合理的。

这并不完美;可能会破坏使用模式的缓存一致性,该使用模式涉及从容器中的有效随机斑点中大量删除,然后在随后的插入回填项目之前对该容器进行迭代。如果您经常处于这种情况下,您将一次跳过潜在的较大内存区域。但是在实践中,我认为此容器是适合您的方案的合理选择。

我将留给其他答案解决的其他方法可能包括基于句柄的方法或插槽图排序的结构(其中,整数“键”与整数“值”具有关联数组,这些值是索引)在后备数组中,它允许您通过“索引”以一些额外的间接访问来遍历向量)。


嗨!有什么资源可以使我更多地了解您在上一段中提到的“殖民地”的替代方法?它们在任何地方实现了吗?我一直在研究这个话题一段时间,我真的很感兴趣。
Rinat Veliakhmedov

5

“缓存友好”是大型游戏的关注点。对我来说,这似乎是过早的优化。


解决此问题而无需“缓存友好”的一种方法是在堆上而不是在堆栈上创建对象:对对象使用new和(智能)指针。这样,您将能够引用您的对象,并且引用不会无效。

对于更友好的缓存解决方案,您可以自己管理对象的取消/分配,并使用这些对象的句柄。

基本上,在程序初始化时,一个对象在堆上保留了一块内存(我们称其为MemMan),然后,当您要创建组件时,您告诉MemMan您需要一个大小为X的组件,将为您保留它,创建一个句柄并在内部保留该句柄对象所在的位置。它会返回该句柄,这是您将保留的唯一有关该对象的信息,永远不会指向其在内存中的位置的指针。

当您需要该组件时,您将要求MemMan访问该对象,它将很乐意这样做。但不要保留对它的引用,因为....

MemMan的工作之一是使对象在内存中彼此靠近。每隔几帧游戏,您就可以告诉MemMan重新排列内存中的对象(或者在创建/删除对象时可以自动完成)。它将更新其“句柄到内存位置”映射。您的句柄将始终有效,但是如果您保留对内存空间的引用指针引用),则只会发现绝望和荒凉。

教科书说,这种管理内存的方式至少具有两个优点:

  1. 较少的高速缓存未命中,因为对象在内存中彼此接近
  2. 它减少了您要对操作系统进行的内存取消/分配调用的次数,据说这需要一些时间。

请记住,使用MemMan的方式以及内部组织内存的方式实际上取决于您如何使用组件。如果要根据它们的类型对它们进行迭代,则希望按类型保留组件,如果要根据它们的游戏对象对它们进行迭代,则需要找到一种方法来确保它们与组件之间的距离接近另一个基于此,依此类推...

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.