答えは、常に配列またはstd :: vectorを使用することです。リンクリストやstd :: mapなどの型は、通常、ゲームでは絶対に恐ろしいものであり、ゲームオブジェクトのコレクションなどの場合も含まれます。
オブジェクト自体(オブジェクトへのポインタではない)を配列/ベクトルに保存する必要があります。
連続したメモリが必要です。あなたは本当にそれが欲しいです。不連続メモリ内のデータを反復処理すると、一般に多くのキャッシュミスが発生し、コンパイラとCPUが効果的なキャッシュプリフェッチを実行できなくなります。これだけでパフォーマンスが低下する可能性があります。
また、メモリの割り当てと割り当て解除を回避する必要があります。高速のメモリアロケータを使用しても、非常に低速です。各フレームで数百のメモリ割り当てを削除するだけで、ゲームで10倍のFPSバンプが発生するのを見ました。そんなに悪くないように思えますが、そうかもしれません。
最後に、ゲームオブジェクトの管理に関心のあるほとんどのデータ構造は、ツリーやリストを使用するよりもはるかに効率的に配列またはベクターに実装できます。
たとえば、ゲームオブジェクトを削除するには、スワップとポップを使用できます。次のようなもので簡単に実装できます。
std::swap(objects[index], objects.back());
objects.pop_back();
また、オブジェクトを削除済みとしてマークし、次に新しいオブジェクトを作成する必要があるときにそのインデックスを空きリストに入れることもできますが、スワップとポップを行う方が優れています。ループ自体を除いて分岐することなく、すべてのライブオブジェクトに対して単純なforループを実行できます。弾丸物理学の統合などでは、これによりパフォーマンスが大幅に向上します。
さらに重要なことは、スロットマップ構造を使用している安定した一意のテーブルルックアップの単純なペアでオブジェクトを検索できることです。
ゲームオブジェクトのメイン配列にはインデックスがあります。このインデックスだけで非常に効率的に検索できます(マップやハッシュテーブルよりもはるかに高速です)。ただし、オブジェクトを削除するときのスワップとポップのため、インデックスは安定していません。
スロットマップには2層のインダイレクションが必要ですが、どちらも定数インデックスを使用した単純な配列検索です。彼らは速いです。本当に速い。
基本的な考え方は、メインオブジェクトリスト、インダイレクションリスト、インダイレクションリスト用のフリーリストの3つの配列があるということです。メインオブジェクトリストには実際のオブジェクトが含まれ、各オブジェクトは独自の一意のIDを知っています。一意のIDは、インデックスとバージョンタグで構成されます。間接リストは、単にメインオブジェクトリストへのインデックスの配列です。フリーリストは、間接リストへのインデックスのスタックです。
メインリストでオブジェクトを作成すると、間接リストで未使用のエントリが見つかります(空きリストを使用)。間接リストのエントリは、メインリストの未使用のエントリを指します。その場所でオブジェクトを初期化し、選択した間接リストエントリのインデックスとメインリスト要素の既存のバージョンタグに1を加えた一意のIDを設定します。
オブジェクトを破棄するときは、通常どおりスワップアンドポップを行いますが、バージョン番号も増やします。次に、間接リストインデックス(オブジェクトの一意のIDの一部)も空きリストに追加します。スワップアンドポップの一部としてオブジェクトを移動する場合、間接リストのエントリも新しい場所に更新します。
擬似コードの例:
Object:
int index
int version
other data
SlotMap:
Object objects[]
int slots[]
int freelist[]
int count
Get(id):
index = indirection[id.index]
if objects[index].version = id.version:
return &objects[index]
else:
return null
CreateObject():
index = freelist.pop()
objects[count].index = id
objects[count].version += 1
indirection[index] = count
Object* object = &objects[count].object
object.initialize()
count += 1
return object
Remove(id):
index = indirection[id.index]
if objects[index].version = id.version:
objects[index].version += 1
objects[count - 1].version += 1
swap(objects[index].data, objects[count - 1].data)
インダイレクションレイヤーを使用すると、圧縮中に移動できるリソース(メインオブジェクトリスト)の安定した識別子(エントリが移動しないインダイレクションレイヤーへのインデックス)を持つことができます。
バージョンタグを使用すると、削除される可能性のあるオブジェクトにIDを保存できます。たとえば、ID(10,1)があります。インデックス10のオブジェクトが削除されます(たとえば、弾丸がオブジェクトに衝突して破棄されます)。メインオブジェクトリスト内のメモリのその場所にあるオブジェクトは、バージョン番号がバンプされて(10,2)になります。古いIDから(10,1)を再度検索しようとすると、ルックアップはインデックス10を介してそのオブジェクトを返しますが、バージョン番号が変更されたことを確認できるため、IDは無効になります。
これは、オブジェクトがメモリ内を移動できる安定したIDを使用して保持できる絶対最速のデータ構造です。これは、データの局所性とキャッシュの一貫性にとって重要です。これは、可能なハッシュテーブルの実装よりも高速です。ハッシュテーブルは、少なくともハッシュを計算する必要があり(テーブルルックアップよりも多くの指示)、ハッシュチェーン(std :: unordered_mapの恐ろしい場合のリンクリスト、またはハッシュテーブルのバカではない実装)、各キーで値の比較を行う必要があります(バージョンタグチェックよりも高価ではありませんが、可能性は低くなります)。非常に優れたハッシュテーブル(STLは、ゲームオブジェクトリスト用にゲームとは異なるさまざまなユースケースに最適化するハッシュテーブルを義務付けているため、STLの実装ではありません)は、1つのインダイレクションを節約できます。
基本アルゴリズムにはさまざまな改善を加えることができます。たとえば、メインオブジェクトリストにstd :: dequeのようなものを使用します。間接的な1つの追加レイヤー。ただし、スロットマップから取得した一時的なポインターを無効にすることなく、オブジェクトを完全なリストに挿入できます。
また、オブジェクトのメモリアドレス(this-objects)からインデックスを計算できるため、オブジェクト内にインデックスを保存することも避けられます。オブジェクトを削除する場合にのみ必要です。インデックス)パラメータとして。
謝罪; それが最も明確な説明だとは思わない。遅く、コードサンプルよりも多くの時間を費やすことなく説明するのは困難です。