C ++でのエンティティコンポーネントシステム間のリンクに関するアドバイス


10

エンティティコンポーネントシステムに関するいくつかのドキュメントを読んだ後、私は私のものを実装することにしました。これまでのところ、エンティティとシステムマネージャ(システム)を含むWorldクラス、コンポーネントをstd :: mapとして含むエンティティクラス、およびいくつかのシステムがあります。エンティティをワールドのstd :: vectorとして保持しています。今のところ問題ありません。私を混乱させるのは、エンティティの反復です。それについて明確な心を持つことができないため、その部分を実装することはまだできません。すべてのシステムが、関心のあるエンティティのローカルリストを保持する必要がありますか?または、Worldクラスのエンティティを反復処理し、ネストされたループを作成してシステムを反復処理し、エンティティがシステムに関係するコンポーネントを持っているかどうかを確認する必要がありますか?というのは :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

しかし、ビットマスクシステムは、スクリプト言語を組み込む場合の柔軟性をある程度ブロックすると思います。または、各システムのローカルリストがあると、クラスのメモリ使用量が増加します。私はひどく混乱しています。


ビットマスクアプローチがスクリプトバインディングを妨害するのはなぜでしょうか。余談ですが、エンティティとシステムのコピーを回避するために、for-eachループで参照(可能な場合はconst)を使用してください。
Benjamin Kloster 2013

intなどのビットマスクを使用すると、32の異なるコンポーネントしか保持できません。32を超えるコンポーネントがあることを示唆していませんが、持っている場合はどうなりますか?別のintまたは64ビットintを作成する必要があります。動的にはなりません。
デニス

実行時に動的にするかどうかに応じて、std :: bitsetまたはstd :: vector <bool>を使用できます。
Benjamin Kloster 2013

回答:


7

各システムのローカルリストがあると、クラスのメモリ使用量が増加します。

それは伝統的な時空のトレードオフですです。

すべてのエンティティを繰り返し処理してその署名を確認することはコードに直接記述されますが、システムの数が増えるにつれて非効率になる可能性があります-何千もの無関係なエンティティの中からおそらく関心のある単一のエンティティを探す特殊なシステム(入力する)を想像してください。

とは言っても、このアプローチはあなたの目標に応じてまだ十分かもしれません。

ただし、速度が心配な場合は、他にも考慮すべき解決策があります。

すべてのシステムが、関心のあるエンティティのローカルリストを保持する必要がありますか?

丁度。これは適切なパフォーマンスを提供する標準的なアプローチであり、実装はかなり簡単です。私の考えでは、メモリのオーバーヘッドはごくわずかです-ポインタの格納について話しています。

これらの「関心のあるリスト」を維持する方法は、今ではそれほど明白ではないかもしれません。データコンテナーについては、std::vector<entity*> targetsシステムの内部クラスで十分です。今私がやっていることはこれです:

  • エンティティは作成時には空であり、どのシステムにも属していません。
  • エンティティにコンポーネントを追加するときはいつでも:

    • 現在のビット署名を取得します
    • コンポーネントのサイズを適切なチャンクサイズの世界のプールにマップし(個人的にはboost :: poolを使用)、そこにコンポーネントを割り当てる
    • エンティティの新しいビット署名を取得します(これは単に「現在のビット署名」と新しいコンポーネントです)
    • 世界のすべてのシステムを反復処理し、その署名システムがあるかどういないエンティティの現在の署名と一致してない新しい署名と一致し、それは私たちがそこに私たちの事業体へのポインタを一backべきで明らかになりました。

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

エンティティの削除は完全に類似しています。唯一の違いは、システムが現在の署名と一致する場合(つまり、エンティティが存在したことを意味します)と新しい署名と一致しない場合(エンティティが存在しないことを意味します)です。 )。

ベクトルから削除することがO(n)であるため、std :: listの使用を検討している可能性があります。中央から削除するたびに大きなデータチャンクをシフトする必要があることは言及していません。実際、あなたはする必要はありません-このレベルでは注文を処理する必要がないので、std :: removeを呼び出して、すべての削除でO(n)検索を実行するだけでよいという事実に応えることができます削除されるエンティティ。

std :: listはO(1)削除を提供しますが、反対側では、追加のメモリオーバーヘッドが少しあります。また、ほとんどの場合、エンティティを処理して削除しないことを忘れないでください。これは確かにstd :: vectorを使用すると高速に実行されます。

パフォーマンスが非常に重要な場合は、別のデータアクセスパターンを検討することできますが、いずれかの方法で「対象となるリスト」を維持します。Entity System APIを十分に抽象化したままにしておけば、フレームレートが原因でシステムのエンティティ処理メソッドが低下しても問題にはならないことに注意してください。今のところ、コーディングしやすい方法を選択してください-のみその後、プロファイルを作成し、必要に応じて改善します。


5

各システムが自身に関連付けられたコンポーネントを所有し、エンティティがそれらを参照するだけの場合を検討する価値のあるアプローチがあります。基本的に、(簡略化された)Entityクラスは次のようになります。

class Entity {
  std::map<ComponentType, Component*> components;
};

RigidBody接続されているコンポーネントを言うと、システムにEntityそれを要求しますPhysics。システムはコンポーネントを作成し、エンティティにそれへのポインタを保持させます。システムは次のようになります。

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

さて、これは最初は少し直観に反するように見えるかもしれませんが、利点はコンポーネントエンティティシステムがその状態を更新する方法にあります。多くの場合、システムを反復処理し、関連するコンポーネントを更新するように要求します

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

システムが所有するすべてのコンポーネントを連続メモリに置くことの利点は、システムがすべてのコンポーネントを反復処理して更新するときに、基本的には

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

更新する必要のあるコンポーネントがない可能性のあるすべてのエンティティを反復処理する必要はありません。また、コンポーネントがすべて連続して格納されるため、非常に優れたキャッシュパフォーマンスが得られる可能性もあります。これは、この方法の最大の利点ではありませんが、1つです。多くの場合、一度に何百、何千ものコンポーネントが存在し、可能な限りパフォーマンスを向上させることもできます。

その時点で、あなたWorldだけがシステムをループし、updateエンティティを繰り返す必要なしにそれらを呼び出します。システムの責任がより明確になるので、(imho)より良い設計です。

もちろん、そのようなデザインは無数にあるので、ゲームのニーズを慎重に評価し、最も適切なものを選択する必要がありますが、ここでわかるように、違いをもたらすことができるのは小さなデザインの詳細です。


良い答え、ありがとう。ただし、コンポーネントには関数(update()など)はなく、データのみがあります。システムはそのデータを処理します。だからあなたの例によれば、コンポーネントクラスの仮想アップデートと各コンポーネントのエンティティのポインタを追加する必要がありますか?
デニス2013

@denizそれはすべてあなたのデザインに依存します。コンポーネントにメソッドがなくデータのみがある場合でも、システムはそれらを反復して必要なアクションを実行できます。エンティティへのリンクについては、はい、所有者エンティティへのポインタをコンポーネント自体に格納するか、システムにコンポーネントハンドルとエンティティ間のマップを維持させることができます。ただし、通常は、コンポーネントをできるだけ自己完結型にする必要があります。親エンティティについてまったく知らないコンポーネントが理想的です。その方向でのコミュニケーションが必要な場合は、イベントなどを優先します。
pwny 2013

効率的になると言うなら、私はあなたのパターンを使います。
デニス2013

@deniz実際にコードをプロファイリングして、特定のエンジンに対して何が機能し、何が機能しないかを特定するために、頻繁にプロファイルを作成してください:)
pwny

わかりました:)私はちょっとストレステストをします
デニス2013

1

私の意見では、優れたアーキテクチャーはエンティティーにコンポーネント・レイヤーを作成し、このコンポーネント・レイヤーで各システムの管理を分離することです。たとえば、ロジックシステムには、エンティティに影響するいくつかのロジックコンポーネントがあり、エンティティのすべてのコンポーネントに共有される共通の属性を格納します。

その後、各システムのオブジェクトを異なるポイントまたは特定の順序で管理する場合は、各システムでアクティブなコンポーネントのリストを作成することをお勧めします。システムで作成および管理できるポインターのすべてのリストは、ロードされたリソース1つ未満です。

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