エンティティシステム内でのエンティティの割り当て


9

エンティティシステム内でエンティティをどのように割り当て/類似する必要があるのか​​、よくわかりません。私にはさまざまなオプションがありますが、それらのほとんどには短所が関連付けられているようです。すべての場合において、エンティティはID(整数)に似ており、おそらくラッパークラスが関連付けられています。このラッパークラスには、エンティティにコンポーネントを追加/削除するメソッドがあります。

オプションについて言及する前に、ここに私のエンティティシステムの基本構造を示します。

  • エンティティ
    • ゲーム内のオブジェクトを説明するオブジェクト
  • 成分
    • エンティティのデータを格納するために使用されます
  • システム
    • 特定のコンポーネントを持つエンティティが含まれています
    • 特定のコンポーネントでエンティティを更新するために使用されます
  • 世界
    • エンティティシステムのエンティティとシステムが含まれています
    • エンティティを作成/破棄し、システムをそこに追加/削除できます

私が考えた私の選択肢は次のとおりです:

オプション1:

エンティティラッパークラスを保存せず、次のID /削除されたIDのみを保存します。つまり、エンティティは次のように値で返されます。

Entity entity = world.createEntity();

これは、この設計にいくつかの欠陥があることを除いて、ententxによく似ています。

短所

  • エンティティラッパークラスが重複している可能性があります(コピークターを実装する必要があり、システムにエンティティを含める必要があるため)
  • エンティティが破棄された場合、重複するエンティティラッパークラスの値は更新されません

オプション2:

エンティティラッパークラスをオブジェクトプール内に保存します。つまり、エンティティは次のようにポインタ/参照によって返されます。

Entity& e = world.createEntity();

短所

  • 重複するエンティティがある場合、エンティティが破棄されると、同じエンティティオブジェクトが別のエンティティの割り当てに再利用されることがあります。

オプション3:

未加工のIDを使用し、ラッパーエンティティクラスを忘れます。これの落とし穴は、それに必要な構文だと思います。これを実装するのが最も簡単で簡単に思えるので、これを行うことを考えています。構文が原因で、私はそれについてかなり確信が持てません。

つまり、このデザインでコンポーネントを追加するには、次のようになります。

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

これに付け加えて:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

短所

  • 構文
  • 重複するID

回答:


12

IDには、インデックスバージョンを混在させる必要があります。これにより、IDを効率的に再利用し、IDを使用してコンポーネントをすばやく見つけ、「オプション2」の実装をはるかに簡単にすることができます(ただし、オプション3は、いくつかの作業ではるかに使いやすくすることができます)。

struct entity {
  uint16 version;
  /* and other crap that doesn't belong in components */
};

std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */

entity_id createEntity()
{
  uint16 index;
  if (!freelist.empty())
  {
    pool.push_back(entity());
    freelist.push_back(pool.size() - 1);
  }
  index = freelist.pop_back();

  return (pool[id].version << 16) | index;
}

void deleteEntity(entity_id id)
{
   uint16 index = id & 0xFFFF;
   ++pool[index].version;
   freelist.push_back(index);
}

entity* getEntity(entity_id id)
{
  uint16 index = id & 0xFFFF;
  uint16 version = id >> 16;
  if (index < pool.size() && pool[index].version == version)
    return &pool[index];
  else
    return NULL;
 }

これにより、一意のインデックス(すべてのライブオブジェクト間で一意)とバージョンタグ(そのインデックスを占有したすべてのオブジェクトで一意になります)の組み合わせである新しい32ビット整数が割り当てられます。

エンティティを削除するときは、バージョンを増やします。これで、そのidへの参照が浮かんでいる場合、プール内のそのスポットを占有しているエンティティと同じバージョンタグがなくなります。呼び出しgetEntity(またはa isEntityValidか任意のもの)の試みは失敗します。その位置に新しいオブジェクトを割り当てると、古いIDは引き続き失敗します。

このようなものを「オプション2」に使用して、古いエンティティ参照を心配することなく機能することを確認できます。それらは移動する可能性があるので(プール全体を再割り当てして移動する可能性があるため)を保存してはなら、代わりに長期参照にのみ使用することに注意してください。ローカルコードでのみ、より高速にアクセスできるオブジェクトを取得するために使用します。必要に応じて、または同様のものを使用して、ポインターの無効化を回避することもできます。entity*pool.push_back()entity_idgetEntitystd::deque

「オプション3」は完全に有効な選択です。特にへの参照がおそらく必要であり、その参照をエンティティ自体に格納することは必ずしも良いとは限りませんが、必ずしもそうではないので、のworld.foo(e)代わりにを使用しても本質的に問題はありません。e.foo()world

e.foo()構文を維持したい場合は、これを処理する「スマートポインタ」を検討してください。上記であきらめたサンプルコードを基にして、次のようなコードを作成できます。

class entity_ptr {
  world* _world;
  entity_id _id;

public:
  entity_ptr() : _id(0) { }
  entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }

  bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
  void clear() { _world = NULL; _id = 0; }
  entity* get() { assert(!empty()); return _world->getEntity(_id); }
  entity* operator->() { return get(); }
  entity& operator*() { return *get(); }
  // add const method where appropriate
};

これで、一意のIDを使用し、->演算子を使用してentityクラス(およびそのクラスで作成したメソッド)に非常に自然にアクセスできるエンティティへの参照を格納する方法ができました。_worldあなたが好む場合メンバーは、あまりにも、シングルトンまたはグローバルである可能性があります。

コードはentity_ptr他のエンティティ参照の代わりにinを使用するだけです。(あなたがC ++ 11と、使用移動セマンティクスと右辺値参照にすべてのそのコードを更新する場合は、多少、より信頼性の高い)あなたがしたい場合はあなたもあなただけ使用できるようにするクラスへの自動参照カウントを追加することができなかったentity_ptr、どこでも、もはや重く考えます参照と所有権について。それとも、これは私が好む別の作るものであるowning_entityweak_entityあなたが生きていると、それが破壊されるまで、ちょうどそれを参照するものの実体を保つハンドルを区別するために型システムを使用することができますので、前者のみ管理参照カウントを持つ型を。

オーバーヘッドが非常に低いことに注意してください。ビット操作は安価です。entityとにかく後で他のフィールドにアクセスする場合、プールへの追加のルックアップは実際のコストではありません。エンティティが本当にidのみで、他にもない場合、少し余分なオーバーヘッドが発生する可能性があります。個人的には、エンティティが単なるIDであり、それ以外は何もないECSの考えは、私にとっては学術的なものです。一般的なエンティティに保存するフラグが少なくともいくつかあります。大きなゲームでは、ツールとシリアル化のサポートのために、エンティティのコンポーネントのコレクション(他に何もない場合はインラインリンクリスト)が必要になる可能性があります。

最後の注意として、私は意図的に初期化しませんでしたentity::version。それは問題ではありません。初期バージョンが何であれ、問題がなければ毎回インクリメントする限り。それが終わりに近づいた2^16場合、それはちょうどラップアラウンドします。古いIDを有効なままにする方法で折り返してしまう場合は、より大きなバージョン(および必要に応じて64ビットID)に切り替えます。安全のために、entity_ptrをチェックして空の場合は常に消去する必要があります。empty()変更可能な_world_and を使用してこれを行うことができますが、_idスレッドには注意してください。


エンティティ構造内にIDを含めないのはなぜですか?私はかなり混乱しています。また、std :: shared_ptr / weak_ptr for owning_entityおよびand を使用できますweak_entityか?
miguel.martin 2013

必要に応じて、代わりにIDを含めることができます。唯一のポイントは、効率的な検索のためにスロットのインデックスもIDに含まれている一方で、スロット内のエンティティが破棄されるとIDの値が変化することです。あなたは使用することができますshared_ptrし、weak_ptr最も効率的な種類の使用にはありませんが、そうと(彼らはそれを変更するカスタムdeletersを持つことができますが)、彼らが個別に割り当てられたオブジェクトのために意図されていることに注意してください。 weak_ptr特にあなたが望むことをしないかもしれません。すべてweak_ptrがリセットされるまでエンティティが完全に割り当て解除/再利用されるのを防ぎますが、リセットは行われweak_entityません。
Sean Middleditch 2013

ホワイトボードを持っている場合や、ペイントなどでこれを作成するのが面倒ではない場合は、このアプローチを説明する方がはるかに簡単です。:)私は構造を視覚化するとそれが非常に明確になると思います。
Sean Middleditch 2013

gamesfromwithin.com/managing-data-relationshipsこの記事は、あなたが回答で言ったのと同じことをいくつか示しているようですが、これはどういう意味ですか?
miguel.martin 2013

1
私はEntityXの作成者であり、インデックスの再利用がしばらくの間私を悩ませてきました。あなたのコメントに基づいて、バージョンも含めるようにEntityXを更新しました。@SeanMiddleditchに感謝!
アレックトーマス

0

私は現在、同じようなものに取り組んでおり、あなたの数1に最も近いソリューションを使用しています。

私が持っているEntityHandleから返されるインスタンスをWorld。それぞれEntityHandleにへのポインターがありWorld(私の場合、それを呼び出すだけですEntityManager)、のデータ操作/取得メソッドEntityHandleは実際にはへの呼び出しWorldです。たとえばComponent、エンティティにを追加するには、を呼び出しEntityHandle.addComponent(component)、次にを呼び出しますWorld.addComponent(this, component)

これにより、Entityラッパークラスは保存されず、オプション3で得られる構文の余分なオーバーヘッドを回避できます。また、「エンティティが破棄されると、重複するエンティティラッパークラスの値が更新されない」という問題も回避できます。 "すべて同じデータを指しているためです。


別のEntityHandleを同じエンティティに似せて作成し、ハンドルの1つを削除しようとするとどうなりますか?他のハンドルは同じIDを持ちます。つまり、死んだエンティティを「処理」します。
miguel.martin 2013

それは事実であり、他の残りのハンドルは、エンティティを「保持」しなくなったIDを指します。もちろん、エンティティを削除してから、他の場所からエンティティにアクセスしようとする状況は避けてください。World「死んだ」エンティティに関連付けられたデータを取得/操作しようとしたときに、たとえば、例外をスローすることができます。
vijoc 2013

回避するのが最善ですが、現実にはこれが起こります。スクリプトは参照を保持し、「スマート」なゲームオブジェクト(ミサイルを探すなど)は参照を保持します。すべての場合に、古い参照に適切に対処できるシステム、またはどのトラックを追跡して弱点をゼロにするシステムが本当に必要です。参照。
Sean Middleditch 2013

たとえば 、古いIDに新しいエンティティが割り当てられている場合は、「死んだ」エンティティに関連付けられたデータを操作または取得しようとすると、世界が例外をスローする可能性があります。
miguel.martin 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.