コンポーネント間通信を安全かつキャッシュフレンドリーなコンポーネントストレージでサポートするにはどうすればよいですか?


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クラスは純粋に仮想なので、さまざまなタイプのコンポーネントのクラスがメッセージの処理方法と更新方法を実装できます。メッセージを送信することもできます。

さて問題は、コンポーネントを呼び出すことができますどのように起こるsendMessageの関数をGameObjectGameObjectManager。私は2つの可能な解決策を思いつきました:

  1. Componentそのへのポインタを与えるGameObject

ただし、ゲームオブジェクトはベクター内にあるため、ポインターがすぐに無効になる可能性があります(のベクターについても同じことが言えますが、GameObjectうまくいけば、この問題を解決することでベクターも解決できます)。ゲームオブジェクトを配列に配置することもできましたが、その場合はサイズに任意の数を渡す必要があり、サイズが不必要に高くなり、メモリを浪費する可能性があります。

  1. Componentへのポインタを与えますGameObjectManager

ただし、コンポーネントがマネージャーの更新関数を呼び出せるようにしたくありません。私はこのプロジェクトに取り組んでいる唯一の人ですが、潜在的に危険なコードを書く習慣をつけたくありません。

コードを安全でキャッシュに優しい状態に保ちながら、この問題を解決するにはどうすればよいですか?

回答:


6

あなたの通信モデルは問題ないようで、これらのポインターを安全に格納できさえすれば、オプション1は大丈夫です。コンポーネントの格納に別のデータ構造を選択することで、この問題を解決できます。

A std::vector<T>は合理的な最初の選択でした。ただし、コンテナのイテレータの無効化動作に問題があります。何が欲しいのは反復処理するために高速でデータ構造とキャッシュコヒーレントである、どの項目を挿入または削除するときにも、イテレータの安定性を維持します。

このようなデータ構造を構築できます。ページのリンクリストで構成されます。各ページには固定容量があり、すべてのアイテムを1つの配列に保持します。カウントは、その配列内のアクティブなアイテムの数を示すために使用されます。ページには、フリーリスト(クリアされたエントリの再利用を可能にする)とスキップリスト(反復中にクリアされたエントリをスキップできるようにする)あります。

つまり、概念的には次のようなものです。

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

私はunimaginativelyこのデータ構造を呼び出すブック(それはあなたがが反復ページの集合だから)が、構造は、様々な他の名前を持っています。

マシュー・ベントレーはそれを「コロニー」と呼んでいます。Matthewの実装はジャンプカウントスキップフィールドを使用します(MediaFireリンクについては謝罪しますが、これはBentley自身がドキュメントをホストする方法です)。これは、これらの種類の構造におけるより典型的なブールベースのスキップリストより優れています。Bentleyのライブラリはヘッダーのみであり、任意のC ++プロジェクトに簡単にドロップできるので、単純にそれを使用して独自のものをローリングすることをお勧めします。私がここで説明している微妙な点や最適化はたくさんあります。

このデータ構造は一度追加されたアイテムを移動することはないため、そのアイテムへのポインターとイテレーターは、そのアイテム自体が削除される(またはコンテナー自体がクリアされる)まで有効です。連続して割り当てられたアイテムのチャンクを格納するため、反復は高速で、ほとんどがキャッシュコヒーレントです。挿入と削除はどちらも合理的です。

それは完璧ではありません。コンテナー内の実質的にランダムなスポットから大幅に削除し、その後の挿入でアイテムがバックフィルされる前にそのコンテナーを反復するという使用パターンでは、キャッシュの一貫性を損なう可能性があります。そのシナリオが頻繁に発生する場合は、潜在的に大きなメモリ領域を一度にスキップします。ただし、実際には、このコンテナはシナリオにとって妥当な選択だと思います。

カバーする他の回答のために残します他のアプローチには、ハンドルベースのアプローチまたはスロットマップの種類の構造が含まれる場合があります(整数の「キー」から整数の「値」への連想配列があり、値はインデックスですバッキング配列内。これにより、いくつかの追加の間接参照を使用した「インデックス」によるアクセスによって、引き続きベクトルを反復できます。


こんにちは!前の段落で述べた「コロニー」の代替案について詳しく知ることができるリソースはありますか?それらはどこに実装されていますか?私はしばらくこのトピックを研究してきましたが、本当に興味があります。
Rinat Veliakhmedov

5

「キャッシュフレンドリー」であることは、ビッグゲームが抱える課題です。これは時期尚早の最適化のようです。


「キャッシュフレンドリー」ではなくこれを解決する1つの方法は、スタックではなくヒープ上にオブジェクトを作成することnewです。オブジェクトのと(スマート)ポインターを使用します。このようにして、オブジェクトを参照することができ、オブジェクトの参照が無効になることはありません。

よりキャッシュフレンドリなソリューションでは、オブジェクトの割り当て解除/割り当てを自分で管理し、これらのオブジェクトへのハンドルを使用できます。

基本的に、プログラムの初期化時に、オブジェクトはヒープ上にメモリのチャンクを予約し(MemManと呼ぶことにします)、コンポーネントを作成するときに、サイズXのコンポーネントが必要であることをMemManに伝えます。それを予約し、ハンドルを作成して、割り当てがそのハンドルのオブジェクトである場所に内部的に保持します。それはハンドルを返し、オブジェクトに関して保持する唯一のものはメモリ内のその場所へのポインタではありません

コンポーネントが必要な場合は、このオブジェクトにアクセスするようにMemManに要求します。ただし、参照を保持しないでください。

MemManの仕事の1つは、オブジェクトをメモリ内で互いに近くに保つことです。いくつかのゲームフレームごとに、メモリ内のオブジェクトを再配置するようにMemManに指示できます(またはオブジェクトを作成/削除するときに自動的に再配置できます)。ハンドルとメモリの場所のマップを更新します。ハンドルは常に有効ですが、メモリ空間への参照(ポインタまたは参照)を保持している場合は、絶望と荒廃しか見つかりません。

教科書によると、このメモリ管理方法には少なくとも2つの利点があります。

  1. オブジェクトがメモリ内で互いに近接しているため、キャッシュミスが少なく、
  2. これは、OSに対して行うメモリのデアロケーション/割り当ての呼び出しの数を減らします。これには時間がかかると言われいます。

MemManの使用方法とメモリの内部構成方法は、コンポーネントの使用方法に大きく依存することに注意してください。タイプに基づいてそれらを反復する場合は、コンポーネントをタイプごとに保持する必要があります。ゲームオブジェクトに基づいてそれらを反復する場合は、それらが近くにあることを確認する方法を見つける必要があります。それに基づいた別のものなど...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.