エンティティシステムについて絶賛されている2つの主な利点は、1)複雑な継承階層に絡む必要がないため、新しい種類のエンティティを簡単に構築できること、および2)キャッシュ効率です。
(1)は、ES / ECSだけでなく、コンポーネントベースの設計の利点であることに注意してください。「システム」部分を持たない多くの方法でコンポーネントを使用でき、それらはうまく機能します(そして、多くのインディーゲームとAAAゲームの両方がそのようなアーキテクチャを使用しています)。
標準のUnityオブジェクトモデル(使用GameObject
およびMonoBehaviour
オブジェクト)はECSではなく、コンポーネントベースの設計です。もちろん、新しいUnity ECS機能は実際のECSです。
システムは複数のコンポーネントで動作する必要があります。つまり、レンダリングシステムと物理システムの両方が変換コンポーネントにアクセスする必要があります。
一部のECSは、エンティティIDでコンポーネントコンテナーをソートします。つまり、各グループの対応するコンポーネントは同じ順序になります。
これは、グラフィックスコンポーネントを線形に反復している場合、対応する変換コンポーネントも線形に反復していることを意味します。いくつかの変換をスキップしている可能性があります(レンダリングしない物理トリガーボリュームがあるなどの理由で)が、メモリ内で常に前方にスキップしているため(通常、特に大きな距離ではないため)効率が向上します。
これは、HPCの推奨されるアプローチである構造の配列(SOA)に似ています。CPUとキャッシュは、単一の線形配列を処理できるのとほぼ同じように、複数の線形配列を処理でき、ランダムメモリアクセスを処理するよりもはるかに優れています。
Unity ECSを含むいくつかのECS実装で使用される別の戦略は、対応するエンティティのアーキタイプに基づいてコンポーネントを割り当てることです。つまり、コンポーネントの正確セットを持つすべてのエンティティは、( 、PhysicsBody
)Transform
異なるコンポーネントを持つエンティティとは別に割り当てられます(たとえばPhysicsBody
、Transform
、および Renderable
)。
そのような設計のシステムは、最初に要件に一致するすべてのアーキタイプ(コンポーネントの必要なセットを含む)を見つけ、そのアーキタイプのリストを繰り返し、一致する各アーキタイプ内に格納されたコンポーネントを繰り返します。これにより、アーキタイプ内で完全に線形かつ真のO(1)コンポーネントアクセスが可能になり、システムは非常に低いオーバーヘッドで互換性のあるエンティティを見つけることができます(潜在的に数十万のエンティティを検索するのではなく、アーキタイプの小さなリストを検索することにより)。
コンポーネントに、他のコンポーネントへのポインター、またはコンポーネントへのポインターを格納するエンティティへのポインターを保存させることができます。
同じエンティティ上の他のコンポーネントを参照するコンポーネントは、何も保存する必要はありません。他のエンティティ上のコンポーネントを参照するには、エンティティIDを保存するだけです。
単一のエンティティに対してコンポーネントが複数回存在することが許可されており、特定のインスタンスを参照する必要がある場合、他のエンティティのIDとそのエンティティのコンポーネントインデックスを保存します。ただし、特にこれらの操作の効率が低下するため、多くのECS実装ではこのケースを許可していません。
すべてのコンポーネント配列が「n」の大きさであることを確認できます。「n」はシステム内に存在するエンティティの数です
ポインターではなくハンドル(インデックス+生成マーカーなど)を使用すると、オブジェクト参照を壊す心配なく配列のサイズを変更できます。
std::deque
何らかの理由でポインタを許可したい場合や、問題を測定した場合は、多くの一般的な実装に似た「チャンク配列」アプローチ(配列の配列)を使用できます(ただし、実装の哀れなほど小さいチャンクサイズはありません)配列のサイズ変更のパフォーマンス。
第二に、これはすべてのエンティティがリストでフレーム/ティックごとに線形に処理されることを前提としていますが、実際にはそうではありません
エンティティに依存します。はい、多くのユースケースでは、そうではありません。実際、これが、コンポーネントベースの設計(良い)とエンティティシステム(特定の形式のCBD)の違いを非常に強調している理由です。
一部のコンポーネントは確かに直線的に簡単に処理できます。通常、「ツリーが重い」ユースケースでも、密集したアレイを使用することでパフォーマンスが確実に向上します(ほとんどの場合、典型的なゲームのAIエージェントのように、最大で数百のNを含むケース)。
一部の開発者は、データ指向の線形に割り当てられたデータ構造を使用することのパフォーマンスの利点が、「スマートな」ツリーベースの構造を使用することのパフォーマンスの利点を上回ることも発見しました。もちろん、すべてはゲームと特定のユースケースに依存します。
セクター/ポータルレンダラーまたはoctreeを使用してオクルージョンカリングを実行するとします。エンティティをセクター/ノード内に連続して保存できる場合もありますが、好きかどうかに関係なく飛び回ることになります。
アレイがどれだけ役立つのか驚くでしょう。「どこでも」よりもはるかに小さなメモリ領域でジャンプしているのに、ジャンプしたとしてもキャッシュ内の何かになってしまう可能性がずっと高くなります。特定のサイズ以下のツリーでは、すべてをキャッシュにプリフェッチでき、そのツリーでキャッシュミスが発生することはありません。
また、密集した配列に収まるように構築されたツリー構造もあります。たとえば、octreeでは、ヒープのような構造(子の前に親、隣同士に兄弟)を使用して、ツリーを「ドリルダウン」しても、常に前方に反復するようにできます。 CPUはメモリアクセス/キャッシュルックアップを最適化します。
これは重要なポイントです。x86 CPUは複雑な獣です。CPUは、マシンコードでマイクロコードオプティマイザーを効果的に実行し、小さなマイクロコードに分割して命令を並べ替え、メモリアクセスパターンなどを予測します。 CPUまたはキャッシュの仕組み。
次に、他のシステムがあります。これは、他の何らかの順序で格納されたエンティティを好む場合があります。
それらを複数回保存できます。配列を最小限の詳細まで削除すると、この方法で実際にメモリを節約できることがわかります(64ビットポインターを削除し、より小さいインデックスを使用できるため)。
個別の配列を維持する代わりにエンティティ配列をインターリーブすることもできますが、それでもメモリを無駄にしています
これは、良好なキャッシュ使用量とは正反対です。変換とグラフィックスのデータだけが必要な場合、物理学とAI、入力とデバッグなどの他のすべてのデータの取り込みに時間を費やすのはなぜですか?
これは通常、ECSとモノリシックゲームオブジェクトを優先する点です(ただし、他のコンポーネントベースのアーキテクチャと比較した場合、実際には適用できません)。
価値があるものとして、私が知っているほとんどの「生産グレード」のECS実装は、インターリーブストレージを使用します。前述の一般的なArchetypeのアプローチ(Unity ECSなどで使用)は、Archetypeに関連付けられたコンポーネントにインターリーブストレージを使用するように非常に明示的に構築されています。
AIは、エンティティのレンダリングに使用される変換またはアニメーションの状態に影響を与えない場合、意味がありません。
AIが変換データに直線的に効率的にアクセスできないからといって、他のシステムがそのデータレイアウトの最適化を効果的に使用できないというわけではありません。ゲームロジックシステムが通常行うようなアドホックな方法でゲームロジックシステムが行うことを止めることなく、パックされた配列を使用してデータを変換できます。
また、コードキャッシュを忘れています。ECSのシステムアプローチを使用する場合(より単純なコンポーネントアーキテクチャとは異なり)、同じ小さなループループを実行し、仮想関数テーブルを前後に行き来するランダムUpdate
関数の種類にジャンプしてジャンプしないように保証します。あなたのバイナリ。そのため、AIの場合、さまざまなAIコンポーネント(ビヘイビアを構成できるように複数のコンポーネントがあるのは確かです!)を別々のバケットに保持し、各リストを個別に処理して、コードキャッシュの使用を最適化する必要があります。
遅延イベントキュー(システムはイベントのリストを生成しますが、システムがすべてのエンティティの処理を完了するまでディスパッチしません)を使用すると、イベントを保持しながらコードキャッシュを適切に使用できます。
各システムがフレームのどのイベントキューを読み取るかを知っているアプローチを使用すると、イベントの読み取りを高速化することもできます。または、少なくともなしでよりも高速です。
パフォーマンスは絶対的なものではありません。優れたデータ指向設計のパフォーマンス上の利点を確認するために、最後の単一のキャッシュミスをすべて排除する必要はありません。
ECSアーキテクチャおよびデータ指向のデザインパターンで多くのゲームシステムをより良く機能させるための研究がまだ活発に行われています。近年、SIMDで行った驚くべきこと(JSONパーサーなど)のいくつかと同様に、古典的なゲームアーキテクチャには直観的ではないが、多くの利点(速度、マルチスレッド、テスト容易性など)。
または、誰もが使用しているが誰も話していないハイブリッドアプローチがあるかもしれません
これは、特にECSアーキテクチャに懐疑的な人々にとって、私が過去に提唱したものです。パフォーマンスが重要なコンポーネントには、優れたデータ指向のアプローチを使用してください。シンプルさが開発時間を短縮する、よりシンプルなアーキテクチャを使用します。ECSが提案するように、すべてのコンポーネントを厳密にコンポーネント化の過剰定義に陥らせないでください。ECSのようなアプローチが理にかなっている場合は簡単に使用できるようにコンポーネントアーキテクチャを開発し、ECSのようなアプローチが理にかなっていない(またはツリー構造よりも理にかなっていない)単純なコンポーネント構造を使用する。
私は個人的には、ECSの真の力への比較的最近の改宗者です。私にとっては、決定要因はECSについてめったに言及されていませんでした:ゲームシステムとロジックのテストを書くことは、私が過去に取り組んだ緊密に結合されたロジックを搭載したコンポーネントベースのデザインと比較してほとんど些細なことです。ECSアーキテクチャはすべてのロジックをシステムに配置し、コンポーネントを消費してコンポーネントの更新を生成するだけなので、システムの動作をテストするためのコンポーネントの「模擬」セットを構築するのは非常に簡単です。ほとんどのゲームロジックはシステム内にのみ存在する必要があるため、事実上、すべてのシステムをテストすると、ゲームロジックのコードカバレッジがかなり高くなります。システムは、テストよりも複雑さやパフォーマンスへの影響がはるかに少ないテストのために、モックの依存関係(GPUインターフェイスなど)を使用できます。
余談ですが、多くの人がECSのことを本当に理解せずにECSについて話していることに気付くかもしれません。多くのゲーム開発者が「ECS」を「Components」と同等であり、「Entity System」の部分を完全に無視していることを示す、古典的なUnityはECSと呼ばれます。インターネット上のECSには、実際のECSではなく、コンポーネントベースのデザインを支持している人が大勢いるときに、多くの愛が山積しています。この時点で、それを議論することはほとんど無意味です。ECSは元の意味から一般的な用語に破損しているため、「ECS」は「データ指向のECS」と同じ意味ではないことを受け入れてください。:/