同じコンポーネントセットのエンティティを線形メモリにグループ化する


11

基本的なシステム、コンポーネント、エンティティのアプローチから始めます

コンポーネントのタイプに関する情報だけから集合この記事から派生した用語)を作成しましょう。これは、エンティティにコンポーネントを1つずつ追加/削除するのと同じように、実行時に動的に行われますが、タイプ情報のみを対象としているため、より正確に名前を付けましょう。

次に、それらすべての集合を指定するエンティティを作成します。エンティティを作成すると、その組み合わせは不変です。つまり、その場で直接変更することはできませんが、ローカルコピーへの既存のエンティティの署名を(コンテンツとともに)取得し、適切に変更して、新しいエンティティを作成できます。それの。

ここで重要な概念について説明します。エンティティが作成されると、それは常にassemblage bucketというオブジェクトに割り当てられます。つまり、同じ署名のすべてのエンティティが同じコンテナ(例:std :: vector)に置かれます。

現在、システムは関心のあるすべてのバケットを反復処理し、その仕事をしています。

このアプローチにはいくつかの利点があります。

  • コンポーネントは少数(正確にはバケット数)の連続したメモリチャンクに格納されます-これによりメモリの使いやすさが向上し、ゲーム全体の状態をダンプするのが簡単になります
  • システムはコンポーネントを線形的に処理します。つまり、キャッシュの一貫性が向上します。さようなら辞書とランダムメモリジャンプ
  • 新しいエンティティの作成は、アセンブリをバケットにマッピングし、必要なコンポーネントをそのベクトルにプッシュバックするのと同じくらい簡単です
  • エンティティの削除は、std :: moveを1回呼び出して最後の要素を削除された要素と交換するのと同じくらい簡単です。現時点では順序は関係ないためです。

ここに画像の説明を入力してください

完全に異なるシグネチャを持つ多くのエンティティがある場合、キャッシュコヒーレンシの利点はある程度減少しますが、ほとんどのアプリケーションでは発生しないと思います。

ベクトルが再割り当てされると、ポインターの無効化にも問題があります。これは、次のような構造を導入することで解決できます。

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

そのため、ゲームロジックの何らかの理由で、新しく作成されたエンティティを追跡したいときはいつでも、バケット内にentity_watcherを登録し、エンティティを削除中にstd :: moveする必要がある場合は、そのウォッチャーをルックアップして更新しますそれらreal_index_in_vectorを新しい値に。ほとんどの場合、これはエンティティの削除ごとに1回の辞書検索を課します。

このアプローチには他に不利な点はありますか?

なぜ明白なのに、なぜ解決策がどこにも言及されていないのですか

編集:コメントが不十分であるため、「回答に答える」ために質問を編集しています。

静的なクラスの構築を回避するために特別に作成された、プラグ可能なコンポーネントの動的な性質を失います。

私はしません。多分私はそれを十分に明確に説明しなかった:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

これは、既存のエンティティの署名を取得して変更し、新しいエンティティとして再度アップロードするだけの簡単なものです。プラグイン可能な動的な性質?もちろん。ここでは、「アセンブリ」クラスと「バケット」クラスが1つしかないことを強調しておきます。バケットはデータ駆動型で、実行時に最適な量で作成されます。

有効なターゲットが含まれている可能性のあるすべてのバケットを通過する必要があります。外部データ構造がないと、衝突検出も同様に困難になります。

これが、前述の外部データ構造がある理由です。回避策は、次のバケットにジャンプするタイミングを検出するイテレータをSystemクラスに導入するのと同じくらい簡単です。ジャンプはロジックに純粋に透明になります。


また、すべてのコンポーネントをベクターに格納することに関するRandy Gaulの記事を読んで、それらのシステムにそれらを処理させるだけです。そこには2つの大きな問題があります。エンティティのサブセットのみを更新したい場合(たとえば、カリングを考えてみてください)。そのため、コンポーネントはエンティティと再度結合されます。コンポーネントの反復ステップごとに、それが属するエンティティが更新用に選択されているかどうかを確認する必要があります。もう1つの問題は、一部のシステムでは、複数の異なるコンポーネントタイプを処理して、キャッシュコヒーレンシの利点を取り戻す必要があることです。これらの問題に対処する方法はありますか?
tiguchi

回答:


7

基本的に、プールアロケーターと動的クラスを使用して静的オブジェクトシステムを設計しました。

私は、学生時代の「アセンブリ」システムとほぼ同じように機能するオブジェクトシステムを作成しましたが、自分のデザインでは、常に「アセンブリ」を「青写真」または「原型」と呼ぶ傾向があります。アーキテクチャは素朴なオブジェクトシステムよりもお尻の面倒であり、私が比較したより柔軟な設計のいくつかに対して測定可能なパフォーマンス上の利点はありませんでした。ゲームエディタで作業しているときは、オブジェクトを再調整したり再割り当てしたりすることなく動的に変更する機能が非常に重要です。設計者は、コンポーネントをオブジェクト定義にドラッグアンドドロップする必要があります。一部のデザインでは、ランタイムコードでコンポーネントを効率的に変更する必要さえあるかもしれませんが、個人的には嫌いです。エディターでオブジェクト参照をリンクする方法に応じて、

自明ではないケースのほとんどで、キャッシュコヒーレンシが思ったより悪くなるでしょう。たとえば、AIシステムはRenderコンポーネントを気にしませんが、最終的には各エンティティの一部としてコンポーネント上で繰り返しスタックします。繰り返し処理されるオブジェクトは大きくなり、キャッシュラインリクエストは不要なデータを取り込むことになり、リクエストごとに返されるオブジェクト全体が少なくなります)。それはまだナイーブメソッドよりも優れており、ナイーブメソッドのオブジェクト構成は大きなAAAエンジンでも使用されているため、おそらくそれ以上は必要ありませんが、少なくともこれ以上改善できないとは思わないでください。

あなたのアプローチは一部にとって最も理にかなっていますすべてではありませんが、コンポーネント。ECSは非常に嫌いです。ECSは、各コンポーネントを常に個別のコンテナーに配置することを推奨しているためです。これは、物理やグラフィックスにとっては理にかなっていますが、複数のスクリプトコンポーネントや構成可能なAIを許可する場合はまったく意味がありません。コンポーネントシステムを組み込みオブジェクトだけでなく、デザイナーやゲームプレイプログラマーがオブジェクトの動作を構成する方法としても使用できるようにする場合、すべてのAIコンポーネント(相互作用することが多い)またはすべてのスクリプトをグループ化することは理にかなっています。コンポーネント(すべてを1つのバッチで更新するため)。最もパフォーマンスの高いシステムが必要な場合は、コンポーネントの割り当てとストレージスキームを組み合わせて、特定のタイプのコンポーネントに最適なものを決定的に判断する必要があります。


私は言いました:エンティティの署名を変更することはできません。直接インプレースで変更することはできませんが、それでもローカルコピーに既存のアセンブリを取得し、変更して、新しいエンティティとして再度アップロードすることができます。質問で示したように、操作はかなり安価です。繰り返しますが、「バケット」クラスは1つだけです。「Assemblages」/「Signatures」/「好きな名前にしましょう」は、標準的なアプローチのように実行時に動的に作成でき、エンティティを「シグネチャ」として考えることもできます。
Patryk Czachurski 2013

そして、あなたは必ずしも具体化に対処したくないと言った。「新しいエンティティを作成する」とは、ハンドルシステムの動作方法によっては、エンティティへの既存のハンドルをすべて切断することを意味する場合があります。彼らが十分に安いかどうかのあなたの電話。私はそれが対処しなければならないお尻の痛みであることがわかりました。
Sean Middleditch 2013

さて、私はこれについてあなたのポイントを持っています。とにかく、追加/削除が少し高価だったとしても、たまに起こるので、リアルタイムで行われるコンポーネントへのアクセスプロセスを大幅に簡略化する価値があります。したがって、「変更」のオーバーヘッドはごくわずかです。AIの例について、とにかく複数のコンポーネントからのデータを必要とするこれらのいくつかのシステムの価値はまだありませんか?
Patryk Czachurski 2013

私の意見は、AIはあなたのアプローチがより良い場所であるということでしたが、他のコンポーネントにとっては必ずしもそうではありません。
Sean Middleditch 2013

4

これで、C ++オブジェクトが再設計されました。これが明白に感じられる理由は、「エンティティ」という単語を「クラス」に置き換え、「コンポーネント」を「メンバー」に置き換えると、これはミックスインを使用した標準のOOP設計であるためです。

1)静的なクラスの構築を回避するために特別に作成された、プラグ可能なコンポーネントの動的な性質を失います。

2)メモリの一貫性は、1つの場所で複数のデータ型を統合するオブジェクト内ではなく、データ型内で最も重要です。これが、コンポーネント+システムが作成された理由の1つであり、クラス+オブジェクトのメモリの断片化を回避します。

3)この設計もC ++クラススタイルに戻ります。これは、エンティティーをコンポーネント+システム設計で、エンティティーが単なるタグ/ IDであり、内部の仕組みを人間が理解できるようにする場合に、エンティティーを一貫したオブジェクトと見なしているためです。

4)プログラマーとして追跡するのが実際には簡単ではないにしても、コンポーネントがそれ自体の中で複数のコンポーネントをシリアル化する複雑なオブジェクトよりも、コンポーネントがそれ自体をシリアル化するのは同じくらい簡単です。

5)このパスの次の論理的なステップは、システムを削除し、そのコードをエンティティーに直接配置することです。エンティティーには、作業に必要なすべてのデータがあります。私たちは皆、それが意味するものを見ることができます=)


2)キャッシュを完全に理解していないかもしれませんが、たとえば10個のコンポーネントで動作するシステムがあるとします。標準的なアプローチでは、各エンティティの処理とはRAMに10回アクセスすることを意味します。これは、プールが使用されている場合でも、コンポーネントがメモリ内のランダムな場所に分散しているためです。異なるコンポーネントは異なるプールに属しているためです。エンティティ全体を一度にキャッシュし、すべてのコンポーネントを1回のキャッシュミスなしで処理し、ディクショナリルックアップを実行する必要さえないことは「重要」ではないでしょうか。また、1)のポイントをカバーするように編集しました
Patryk Czachurski 2013

@Sean Middleditchは、このキャッシングの内訳について彼の回答に詳しく説明しています。
Patrick Hughes

3)それらは、いかなる方法でも一貫したオブジェクトではありません。Johnが指摘したように、メモリ内でコンポーネントBのすぐ隣にあるコンポーネントAについては、「論理的一貫性」ではなく、「メモリ一貫性」にすぎません。バケットは、作成時にコンポーネントを署名して任意の順序にシャッフルすることもでき、原則は維持されます。4)十分な抽象化があれば、「追跡」することも同様に簡単かもしれません。ここで話しているのは、反復子を備えたストレージスキームだけであり、バイトオフセットマップを使用すると、標準的なアプローチと同じくらい簡単に処理できるようになります。
Patryk Czachurski 2013

5)そして、私はこのアイデアの何もがこの方向を指し示しているとは思いません。私があなたに同意したくないというわけではありません。この議論がどこにつながるのか興味があるだけですが、とにかくそれは一種の「測定」またはよく知られた「時期尚早の最適化」につながるでしょう。:)
Patryk Czachurski 2013

@PatrykCzachurskiですが、システムは10個のコンポーネントで動作しません。
user253751 2018年

3

同じようなエンティティをまとめることは、あなたが考えるほど重要ではありません。そのため、「ユニットだから」以外の正当な理由を考えるのは難しいのです。ただし、これは論理的な一貫性ではなくキャッシュの一貫性のために実際に行っているため、理にかなっている可能性があります。

発生する可能性がある1つの問題は、異なるバケット内のコンポーネント間の相互作用です。たとえば、AIが撃つことができるものを見つけるのは簡単ではありません。たとえば、有効なターゲットが含まれている可能性のあるすべてのバケットを通過する必要があります。外部データ構造がないと、衝突検出も同様に困難になります。

論理的な一貫性を保つためにエンティティーをまとめて整理することを続けるために、エンティティーをまとめておく必要がある唯一の理由は、私の使命における識別の目的です。エンティティタイプAとタイプBのどちらを作成したのかを知る必要があります。これを回避するには、このエンティティをまとめたアセンブリを識別する新しいコンポーネントを追加します。それでも、私はすべてのコンポーネントを一緒に集めて壮大な仕事をしているのではなく、それが何であるかを知る必要があるだけです。ですから、この部分はあまり役に立たないと思います。


私はあなたの答えがよくわかりません。「論理的一貫性」とはどういう意味ですか?インタラクションの難しさについて編集しました。
Patryk Czachurski 2013

「論理的一貫性」の例:Treeエンティティを構成するすべてのコンポーネントを互いに近づけることは「論理的意味」があります。
ジョンマクドナルド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.