コンポーネントベースのゲームの設計


16

私はシューティングゲーム(1942、クラシック2Dグラフィックスなど)を書いていますが、コンポーネントベースのアプローチを使用したいと思います。これまでのところ、次の設計について考えました。

  1. 各ゲーム要素(飛行船、発射物、パワーアップ、敵)はエンティティです

  2. 各エンティティは、実行時に追加または削除できるコンポーネントのセットです。例には、Position、Sprite、Health、IA、Damage、BoundingBoxなどがあります。

飛行船、発射物、敵、パワーアップはゲームクラスではないという考え方です。エンティティは、所有するコンポーネントによってのみ定義されます(時間の経過とともに変化する可能性があります)。そのため、プレイヤーの飛行船は、スプライト、位置、ヘルス、および入力コンポーネントから始まります。パワーアップには、Sprite、Position、BoundingBoxがあります。等々。

メインループは、ゲームの「物理」、つまりコンポーネントの相互作用を管理します。

foreach(entity (let it be entity1) with a Damage component)
    foreach(entity (let it be entity2) with a Health component)
    if(the entity1.BoundingBox collides with entity2.BoundingBox)
    {
        entity2.Health.decrease(entity1.Damage.amount());
    }

foreach(entity with a IA component)
    entity.IA.update(); 

foreach(entity with a Sprite component)
    draw(entity.Sprite.surface()); 

...

コンポーネントは、メインC ++アプリケーションにハードコーディングされています。エンティティはXMLファイル(luaまたはpythonファイルのIAパーツ)で定義できます。

メインループはエンティティをあまり気にしません。コンポーネントを管理するだけです。ソフトウェア設計では、次のことを許可する必要があります。

  1. コンポーネントを指定して、それが属するエンティティを取得します

  2. エンティティを指定すると、タイプ「type」のコンポーネントを取得します

  3. すべてのエンティティについて、何かをする

  4. すべてのエンティティのコンポーネントについて、何かを実行します(例:シリアル化)

私は次のことを考えていました:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };

// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
   int id; // entity id
   boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
   template <class C> bool has_component() { return components.at<C>() != 0; }
   template <class C> C* get_component() { return components.at<C>(); }
   template <class C> void add_component(C* c) { components.at<C>() = c; }
   template <class C> void remove_component(C* c) { components.at<C>() = 0; }
   void serialize(filestream, op) { /* Serialize all componets*/ }
...
};

std::list<Entity*> entity_list;

この設計では、#1、#2、#3(boost :: fusion :: mapアルゴリズムに感謝)と#4を取得できます。また、すべてがO(1)です(正確ではありませんが、それでも非常に高速です)。

より一般的なアプローチもあります。

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };

class Entity
{
   int id; // entity id
   std::vector<Component*> components;
   bool has_component() { return components[i] != 0; }
   template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};

もう1つの方法は、Entityクラスを削除することです。各コンポーネントタイプは独自のリストにあります。スプライトリスト、ヘルスリスト、ダメージリストなどがあります。これらはエンティティIDにより同じロジックエンティティに属していることがわかります。これはもっと簡単ですが、IAコンポーネントは基本的に他のすべてのエンティティのコンポーネントにアクセスする必要があり、各ステップで他のコンポーネントのリストを検索する必要があります。

どのアプローチが良いと思いますか?boost :: fusion mapは、そのように使用するのに適していますか?


2
なぜ下票なのか?この質問の何が問題になっていますか?
エミリアーノ

回答:


6

コンポーネントベースの設計とデータ指向の設計が密接に関連していることがわかりました。コンポーネントの同種リストを持ち、ファーストクラスのエンティティオブジェクトを削除する(コンポーネント自体のエンティティIDを選択する)のは「遅い」と言いますが、実際のコードをプロファイリングしていないため、ここでもそこにもありません両方のアプローチを実装して、その結論に到達します。実際、データ指向設計のさまざまな利点(並列化、キャッシュ使用率、モジュール性など)により、コンポーネントの均質化と従来の重い仮想化の回避がより高速になることをほぼ保証できます。

このアプローチがすべてに理想的だとは言いませんが、基本的にはすべてのフレームで同じ変換を実行する必要があるデータのコレクションであるコンポーネントシステムは、単にデータ指向であると叫びます。コンポーネントが異なるタイプの他のコンポーネントと通信する必要がある場合がありますが、これはいずれにせよ必要な悪事になるでしょう。ただし、メッセージキューやfutureなどのすべてのコンポーネントが並行して処理される極端な場合でも、この問題を解決する方法があるため、設計を推進すべきではありません。

コンポーネントベースのシステムに関連するデータ指向のデザインをGoogleが担当しているのは、このトピックが多く取り上げられ、かなりの議論と逸話的なデータが出回っているからです。


「データ指向」とはどういう意味ですか?
エミリアーノ

そこGoogleで多くの情報があるが、ここでは、コンポーネントのシステムに関連する議論が続く高レベルの概要を提供しなければならないポップアップというまともな記事では、次のとおりです。gamesfromwithin.com/data-oriented-designgamedev。 net / topic /…
スカイラーヨーク

DODについて完全に理解することはできないと思うので、DODについて述べたすべてのことに同意することはできません。 OOPのアプローチ、問題は、これら2つの方法を組み合わせて、パフォーマンスとコーディングの容易さの両方で最大の利益を得る方法です。構造では、すべてのエンティティがいくつかのコンポーネントを共有していないが、DODを使用して簡単に解決できる場合、パフォーマンスの問題があることを示唆しています。
Ali1S232

これは私の質問に直接答えませんが、非常に有益です。私は大学時代にデータフローについて何かを思い出しました。これがこれまでのベストアンサーであり、「勝ちます」。
エミリアーノ

-1

このようなコードを書く場合は、このアプローチを使用します(あなたにとって重要な場合はブーストを使用しません)いくつかのコンポーネントを共有していないため、コンポーネントを見つけるには時間がかかります。それ以外には、私ができる他の問題はありません:

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

このアプローチでは、すべてのコンポーネントはエンティティのベースであるため、コンポーネントのポインタはエンティティでもあります。あなたが求める2番目のことは、いくつかのエンティティのコンポーネントへの直接アクセスを持つことです。私が使用するエンティティのいずれかで損傷にアクセスする必要があるdynamic_cast<damage*>(entity)->value場合、entity損傷コンポーネントがある場合は値を返します。entityコンポーネントに損傷があるかどうかわからない場合、キャストが有効でない場合はif (dynamic_cast<damage*> (entity))戻り値dynamic_castが常にNULLであり、有効な場合は要求された型と同じポインターであることを簡単に確認できます。だから、entitiesいくつかを持っているすべてのもので何かをするためには、component以下のようにすることができます

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

他に質問があれば、喜んでお答えします。


なぜ私は反対票を得たのですか?私のソリューションの何が問題になっていますか?
Ali1S232

3
コンポーネントはゲームクラスから分離されていないため、ソリューションは実際にはコンポーネントベースのソリューションではありません。インスタンスはすべて、HAS A関係(構成)ではなく、IS A関係(継承)に依存しています。構成方法(エンティティは複数のコンポーネントに対応します)で実行すると、継承モデルよりも多くの利点が得られます(通常、コンポーネントを使用する理由です)。あなたのソリューションは、コンポーネントベースのソリューションの利点を何も与えず、いくつかの癖(多重継承など)を導入します。データの局所性、個別のコンポーネントの更新はありません。コンポーネントのランタイム変更はありません。
無効

まず最初に、すべてのコンポーネントインスタンスが1つのエンティティのみに関連している構造を求めますbool isActive。基本コンポーネントクラスにを追加するだけでコンポーネントをアクティブまたは非アクティブにできます。そこにあなたがentetiesを定義する際にまだ使用可能コンポーネントの導入のために必要とされるが、私は問題としてそれを考慮していない、まだあなたが持っているseprate componnent更新(のように気にいらない覚えてdynamic_cast<componnet*>(entity)->update()
Ali1S232

彼がデータを共有できるコンポーネントを持ちたいと思うとき、まだ問題があることに同意しますが、彼が要求したことを考えれば、それは問題ではないと思います、そして再びその問題にもいくつかのトリックがあります説明したいです。
Ali1S232

この方法で実装することは可能ですが、良いアイデアだとは思いません。設計者は、考えられるすべてのコンポーネントを継承する1つのuberクラスがない限り、オブジェクト自体を作成できません。また、1つのコンポーネントだけでupdateを呼び出すことはできますが、メモリ内レイアウトは適切ではありません。合成モデルでは、同じタイプのすべてのコンポーネントインスタンスをメモリ内に保持し、キャッシュミスなしで繰り返し処理できます。また、RTTIにも依存しています。RTTIは、パフォーマンス上の理由により通常ゲームではオフになっています。適切にソートされたオブジェクトレイアウトは、ほとんどを修正します。
ボイド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.