回答:
私がRPGスタイルのマップでやったことの1つは、あなたが入ることができる家、ダンジョンなどである4つの主要な構造(マップ、エリア、ゾーン、タイル)を持っています。
タイルは明らかにタイルです。
ゾーン、チャンク、または何でも、X x Yタイルの領域です。これには2D配列があります。
エリアはゾーンのコレクションです。各エリアは異なるゾーンサイズを持つことができます-世帯は32x32ゾーンを使用できますが、家は1つの10x20ゾーンを持つことができます。これらは辞書に保存されています(ゾーン(-3、-2)を作成できるため)。
マップはエリアのコレクションであり、すべてが互いにリンクされています。
これにより、1つの巨大なマップを作成するよりも柔軟性を高めることができると感じました。
タイルマップの特定のケースで私が何をするかを説明します。これは、世界がオンデマンドで生成されているものの、変更を保存する必要がある場合に効果的な無限マップ用です。
いくつかのセルサイズ「N」を定義し、世界を正方形/立方体「NxN」または「NxNxN」に分割します
セルには一意のキーがあります。私はハッシュ化または直接フォーマットされた文字列を使用して鉱山を生成します: "%i、%i、%i"、x、y、z(x、y、zはセルの始点のワールド座標をNで除算したものです)
NxNタイルまたはNxNxNタイルがあることがわかっているので、タイルインデックスを配列に格納するのは簡単です。また、タイルタイプが占めるビット数もわかります。線形配列を使用するだけです。セルの読み込みと保存/解放も扱いやすくなります。
アクセサーは、単にセルのキーを生成する必要があります(それがロード/生成されたことを確認し、それへのポインターを取得するため)。次に、サブインデックスを使用して、そのセル内を調べます。その特定のポイントでタイル値を見つけるため。
セルをそのキーで抽出し、現在は全体のセルを一度に処理するため、現在マップ/辞書を使用しています(タイルごとの辞書ルックアップを実行することでヒットがどれほどひどいのか知りたくありません)。
別の点として、私はモブ/プレイヤーをセルデータに保持しません。積極的に動的なものはそれ自身のシステムを必要とします。
代替案は必要ないため、おそらく質問はされませんでした。私にとっては:
Tile tiles[MAP_HEIGHT][MAP_WIDTH];
サイズが固定の場合。
std::vector<Tile> tiles(MAP_HEIGHT*MAP_WIDTH);
さもないと。
それはゲームのスタイルと正直にマップに依存します。比較的小さな長方形のタイルマップの場合、おそらく2D配列をそのまま使用します。マップが非常に不規則な形(多数の空のギャップ)である場合、O(1)インデックスを提供するリンクリストのラッパーがおそらく私の選択でしょう。
整数のインデックス付き配列は、2147483647 ^ 2の2次元配列を提供します。これはかなり大きいですが、メモリにロードするものを超えています。マップが大縮尺である場合、もう1つ注意すべき点は、マップをチャンクに分割することです。各チャンクは固定サイズで、必要に応じてメモリを低く保つためにロード/アンロードできるタイルのサブ配列が含まれています。
それがターンベースの戦略ゲームのようなグリッド上の単純なタイルゲームの場合は、次のようになります。
struct Tile
{
// Stores the first entity (enemy, NPC, item, etc) on the tile.
int first_entity;
...
};
struct Entity
{
// Stores the next entity on the same tile or
// the next free entity index to reclaim if
// this entity has been freed/removed.
int next;
...
};
struct Row
{
// Stores all the tiles on the row.
vector<Tile> tiles;
// Stores all the entities on the row.
vector<Entity> entities;
// Points to the first free entity index
// to reclaim on insertion.
int first_free;
};
struct Map
{
// Stores all the rows in the map.
vector<Row> rows;
};
一部の人々は、なぜマップの行ごとに別々のベクトルを格納するのを選ぶのか疑問に思うかもしれません。特定のタイルの上に立っているエンティティをトラバースするときに、空間的な局所性を改善するためです。行ごとに個別のベクトルを格納する場合、その行のすべてのエンティティはL1またはL2に収まる可能性がありますが、マップ全体のすべてのエンティティに1つのエンティティコンテナを格納した場合、L3に収まらない可能性もあります。これは、たとえば、タイルごとに個別のベクトルを格納する場合と比較すると、かなり安価になる傾向があります。
たとえばのタイルを取得するには、次の(102, 72)
ようにします。
Row& row = map.rows[72];
Tile& tile = row.tiles[102];
タイル上のエンティティをトラバースするには、次のようにします。
int entity = tile.first_entity;
while (entity != -1)
{
// Do something with the entity on the tile.
...
// Advance to the next entity on the tile.
entity = row.entities[entity].next;
}
当然のことながら、「行ごとの個別のコンテナ」タイプの実装が最も効果的ですが、タイルアクセスパターンは、次の行に移動する前に、行の対象となるすべての列を処理する必要があります。次と再び。
エンティティをタイルに挿入すると、次のようになります。
int Map::insert_entity(Entity ent, int col_idx, int row_idx)
{
Row& row = rows[row_idx];
int ent_idx = row.first_free;
if (ent_idx != -1)
{
row.first_free = row.entities[ent_idx].next;
row.entities[ent_idx] = ent;
}
else
{
ent_idx = static_cast<int>(row.entities.size());
row.entities.push_back(ent);
}
Tile& tile = row.tiles[col_idx];
row.entities[ent_idx].next = tile.first_entity;
tile.first_entity = ent_idx;
return ent_idx;
}
...と削除:
void Map::remove_entity(int ent_idx, int col_idx, int row_idx)
{
Row& row = rows[row_idx];
Tile& tile = row.tiles[col_idx];
if (tile.first_entity = ent_idx)
tile.first_entity = row.entities[ent_idx].next;
row.entities[ent_idx].next = row.first_free;
row.first_free = ent_idx;
}
私がこの解決策を気に入っている主な理由は、あまりにも多くのベクトル(例:タイルごとに1つのベクトル:大きなマップには多すぎる)を保存しないことですが、特定のタイルのエンティティを反復することでメモリアドレス全体に壮大なストライドが生じることは少なくありませんスペースとlotsaのキャッシュミス。行ごとに1つのエンティティベクトルが適切なバランスをとります。
これは、建物、敵、アイテム、宝箱、プレイヤーがタイルの上に立っていること、およびゲームロジックに費やされる時間の多くが、それらのタイルの上に立っているエンティティにアクセスし、エンティティが何であるかを確認していることを前提としています。特定のタイル。それ以外の場合は、タイルにアクセスするだけで最も効率的であるため、すべてのタイルに対して1つのベクトルを使用する1D配列アプローチを使用します。次に、以下をtiles[row*num_cols+col]
使用してタイルを取得できます。疑わしい場合は、1次元配列を使用します。これにより、ネストされたループなしで単純な順次順序でトラバースできるため、全体を割り当てるために1つのヒープ割り当てのみが必要になります。
一般に、行ごとに個別の動的配列を使用すると、グリッドがその内部に要素を格納している場合に、キャッシュミスを大幅に削減できます。もちろん、そうでなく、グリッドがピクセルを含む画像のようなものである場合、行ごとに個別の動的配列を使用しても意味がありません。このようにグリッドのようなものを最適化した最近のベンチマークとして(すべてに1つの巨大な配列を使用する前に、vtuneで多くのキャッシュミスを確認した後、行ごとに個別の動的配列を格納するように最適化しました):
前:
--------------------------------------------
- test_grid
--------------------------------------------
time passed for 'insert': {1.799000 secs}
mem use after 'insert': 479,508,224 bytes
8560 cells, 1000000 rects
finished test_grid: {1.919000 secs}
後:
--------------------------------------------
- test_grid
--------------------------------------------
time passed for 'insert': {0.310000 secs}
mem use after 'insert': 410,546,720 bytes
8560 cells, 1000000 rects
finished test_grid: {0.361000 secs}
そして、私は上記と同じ種類の戦略を使用しました。おまけとして、エンティティを格納するベクトルがマップ全体ではなく行ごとに1つを格納する場合、より厳密に適合する傾向があるため、メモリ使用量が減少することもわかります。
グリッドに100万のエンティティを挿入する上記のテストは、最適化後でも長時間と多くのメモリを消費しているように見える場合があります。これは、挿入する各エンティティが多くのタイルを取り、エンティティあたり平均約100タイル(平均サイズ10x10)であるためです。したがって、100万のエンティティのそれぞれを平均100のグリッドタイルに挿入します。これにより、わずか100万のエンティティではなく1億のエンティティを挿入するようになります。それは病理学的なケースのストレステストです。それぞれ1タイルを占める100万のエンティティを挿入するだけの場合、ミリ秒単位で挿入でき、約16メガバイトのメモリを使用します。
私の場合、ゲームではなくVFXで作業するため、病理学的ケースを効率的にする必要があることもよくあります。アーティストに「このエンジンでコンテンツをこのようにする」とは言えません。VFXの目的は、アーティストが自由にコンテンツを作成できるようにすることです。その後、お気に入りのエンジンにエクスポートする前に最適化しますが、最適化されていないものに対処する必要があります。つまり、オクトリーがシーン全体に広がる巨大な三角形を効率的に処理する必要があるなど、病理学的なケースを効率的に処理する必要があることがよくあります。アーティストはそのようなコンテンツを頻繁に作成します(予想よりもはるかに頻繁に)。とにかく、上記のテストは決して発生してはならないことをテストしているため、100万のエンティティを挿入するのに3分の1秒近くかかりますが、私の場合、これらの「発生してはならない」ことが常に発生します。だから、病理学的なケースは私にとって珍しいケースではありません、
副次的なボーナスとして、これにより、ロックなしのマルチスレッドを使用して、複数の行のエンティティを同時に並行して挿入および削除することもできます。同時に同じ行にデータを挿入/削除します。
私は、世界がメッシュである3Dエンジンをいじっています。
以前作成した2Dマップとタイルセットをインポートしています。
そして、私はそれを3Dメッシュに変換し、すべてのタイルを1つのテクスチャに配置します。
これはかなり速く描画します。
タイルのコンセプトを維持する必要がある場合は、マップをw * hの1D配列で保持することもできますが、私の答えは、2Dを超えて自由に移動できることです。
また、GPU描画の使用中にパフォーマンスの問題が発生した場合、グラフィック表現を単一のテクスチャとして(高さが変化する場合はメッシュを使用して)維持すると、各タイルを個別に描画する場合に比べて速度が大幅に向上します。