C ++ Entity-Component-Systemsのコンポーネントに適切にアクセスするにはどうすればよいですか?


18

(私が説明しているのは、この設計に基づいています:エンティティシステムフレームワークとは何ですか?、下にスクロールすると見つかります)

C ++でエンティティコンポーネントシステムを作成するときに問題が発生します。コンポーネントクラスがあります。

class Component { /* ... */ };

これは実際には、他のコンポーネントを作成するためのインターフェースです。したがって、カスタムコンポーネントを作成するには、インターフェイスを実装し、ゲーム内で使用されるデータを追加するだけです。

class SampleComponent : public Component { int foo, float bar ... };

これらのコンポーネントはEntityクラス内に格納され、Entityの各インスタンスに一意のIDが付与されます。

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

コンポーネントは、コンポーネントの名前をハッシュすることでエンティティに追加されます(これはおそらくそれほど素晴らしい考えではありません)。カスタムコンポーネントを追加すると、コンポーネントタイプ(ベースクラス)として保存されます。

一方、今では、内部にNodeインターフェースを使用するSystemインターフェースがあります。Nodeクラスは、単一のエンティティのコンポーネントの一部を格納するために使用されます(システムはエンティティのすべてのコンポーネントの使用に関心がないため)。システムがupdate()必要な場合、システムは異なるエンティティから作成されたノードを反復処理するだけです。そう:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

問題は、エンティティをSampleSystemに渡すことでSampleNodesを構築するとしましょう。次に、SampleNodeは、エンティティにSampleSystemが使用する必要なコンポーネントがあるかどうかを「チェック」します。エンティティ内の目的のコンポーネントにアクセスする必要があるときに問題が発生します。コンポーネントはComponent(基本クラス)コレクションに格納されているため、コンポーネントにアクセスして新しいノードにコピーできません。私は一時的にComponent派生型にキャストすることで問題を解決しましたが、これを行うより良い方法があるかどうかを知りたかったのです。これがすでに持っているものを再設計することを意味するかどうかを理解しています。ありがとう。

回答:


23

あなたが保存しようとしている場合はComponent、すべて一緒にコレクションの中のを、あなたは、コレクションに保存されているタイプとして、共通の基本クラスを使用しなければならない、とあなたがアクセスしようとするので、あなたは正しい型にキャストする必要がありComponent、コレクションで秒。typeidただし、テンプレートと関数を巧妙に使用することで、間違った派生クラスにキャストしようとする問題を解消できます。

次のように宣言されたマップでは:

std::unordered_map<const std::type_info* , Component *> components;

次のようなaddComponent関数

components[&typeid(*component)] = component;

およびgetComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

ミスキャストは発生しません。これはtypeid、コンポーネントのランタイム型(最も派生した型)の型情報へのポインターを返すためです。コンポーネントはその型情報をキーとして保存されるため、型の不一致が原因でキャストが問題を引き起こすことはありません。それはタイプが成分由来かなければならないとして、また、テンプレートの種類にコンパイル時の型チェックを取得するstatic_cast<T*>と型が異なるだろうunordered_map

ただし、共通のコレクションに異なるタイプのコンポーネントを保存する必要はありません。Entityを含むという考えを放棄し、Component代わりに各Componentストアを持っている場合Entity(実際には、おそらく単なる整数IDになります)、各派生コンポーネントタイプをとしてではなく、派生タイプの独自のコレクションに格納できます共通の基本型、およびそのIDを介してComponents を見つけEntityます。

この2番目の実装は、最初の実装よりも少し直感的ではありませんが、システムのユーザーが気にする必要のないインターフェイスの背後に実装の詳細として隠されている可能性があります。2番目は実際に使用していないため、どちらが良いかについてはコメントしませんが、static_castを使用することは、最初の実装が提供するほど強力な型の保証を伴う問題としては見ていません。プラットフォームや哲学的信念に応じて問題となる場合とそうでない場合があるRTTIが必要なことに注意してください。


3
私はもう6年近くC ++を使用していますが、毎週新しいトリックを学んでいます。
knight666

回答ありがとうございます。最初に最初の方法を使用してみますが、後からもう1つを使用する方法を考えます。しかし、addComponent()メソッドもテンプレートメソッドである必要はありませんか?を定義するとaddComponent(Component* c)、追加するサブコンポーネントはすべてComponentポインターに格納され、typeid常にComponent基本クラスを参照します。
フェデリコ

2
Typeidは、ポインターが基本クラスであっても、ポイントされているオブジェクトの実際のタイプを提供します
Chewy Gumball

私は噛み応えのある答えが本当に好きだったので、mingw32でそれを実装してみました。typeidがすべてのタイプとしてコンポーネントを返すため、addComponent()がすべてをコンポーネントとして格納するfede ricoが言及した問題に遭遇しました。ここで誰かが、ポインタが基本クラスを指す場合でも、typeidが指すオブジェクトの実際の型を与えるべきであると述べましたが、コンパイラなどに基づいて変わる可能性があると思います。私はWindows 7でg ++ std = c ++ 11 mingw32を使用していました。最終的にgetComponent()をテンプレートに変更し、その型をth
shwosephに保存しました。

これはコンパイラ固有ではありません。おそらくtypeid関数の引数として正しい式を持っていなかったでしょう。
チューイーガムボール

17

Chewyには問題はありませんが、C ++ 11を使用している場合は、使用できる新しい型がいくつかあります。

const std::type_info*マップでキーとして使用する代わりに、のラッパーであるstd::type_indexcppreference.comを参照)を使用できますstd::type_info。なぜそれを使用するのですか?std::type_index実際との関係格納std::type_infoポインタとしては、それは心配するあなたのための1つの少ないのポインタです。

実際にC ++ 11を使用している場合は、Componentスマートポインター内に参照を保存することをお勧めします。したがって、マップは次のようになります。

std::map<std::type_index, std::shared_ptr<Component> > components

新しいエントリを追加するには次のようにします。

components[std::type_index(typeid(*component))] = component

where componentはタイプstd::shared_ptr<Component>です。特定のタイプへの参照を取得すると、次のComponentようになります。

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

の使用にも注意してください static_pointer_cast代わりのstatic_cast


1
私は実際に自分のプロジェクトでこの種のアプローチを使用しています。
vijoc

これは、C ++ 11標準を参照として使用してC ++を学習しているため、実際には非常に便利です。しかし、私が気づいたことの1つは、Webで見つかったすべてのエンティティコンポーネントシステムが何らかのを使用していることですcast。これ、またはキャストなしの同様のシステム設計を実装することは不可能だと考え始めています。
フェデリコ

@Fede Component単一のコンテナにポインタを保存するには、必ず派生型にキャストする必要があります。ただし、Chewyが指摘したように、キャストを必要としない他のオプションも利用できます。私自身は、このタイプのキャストが比較的安全であるため、デザインに「悪い」ものは何もありません。
vijoc

@vijocそれらは、導入する可能性のあるメモリの一貫性の問題のために、時々悪いと考えられます。
-akaltar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.