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_id
getEntity
std::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_entity
とweak_entity
あなたが生きていると、それが破壊されるまで、ちょうどそれを参照するものの実体を保つハンドルを区別するために型システムを使用することができますので、前者のみ管理参照カウントを持つ型を。
オーバーヘッドが非常に低いことに注意してください。ビット操作は安価です。entity
とにかく後で他のフィールドにアクセスする場合、プールへの追加のルックアップは実際のコストではありません。エンティティが本当にidのみで、他に何もない場合、少し余分なオーバーヘッドが発生する可能性があります。個人的には、エンティティが単なるIDであり、それ以外は何もないECSの考えは、私にとっては学術的なものです。一般的なエンティティに保存するフラグが少なくともいくつかあります。大きなゲームでは、ツールとシリアル化のサポートのために、エンティティのコンポーネントのコレクション(他に何もない場合はインラインリンクリスト)が必要になる可能性があります。
最後の注意として、私は意図的に初期化しませんでしたentity::version
。それは問題ではありません。初期バージョンが何であれ、問題がなければ毎回インクリメントする限り。それが終わりに近づいた2^16
場合、それはちょうどラップアラウンドします。古いIDを有効なままにする方法で折り返してしまう場合は、より大きなバージョン(および必要に応じて64ビットID)に切り替えます。安全のために、entity_ptrをチェックして空の場合は常に消去する必要があります。empty()
変更可能な_world_
and を使用してこれを行うことができますが、_id
スレッドには注意してください。
owning_entity
およびand を使用できますweak_entity
か?