web-dev-qa-db-ja.com

OOP ECS対Pure ECS

第一に、この質問はゲーム開発のトピックに関連していることは承知していますが、実際にはより一般的なソフトウェア生成の問題に帰着するため、ここで質問することにしました。

過去1か月間に、Entity-Component-Systemsについて多くを読みましたが、今ではその概念に非常に慣れています。ただし、明確な「定義」が欠落しているように見える1つの側面があり、異なる記事では根本的に異なるソリューションを提案しています。

これは、ECSがカプセル化を解除する必要があるかどうかの問題です。つまり、そのOOPスタイルECS(コンポーネントは、それらに固有のデータをカプセル化する状態と動作の両方を持つオブジェクト)対純粋なECS(コンポーネントはcスタイルの構造体であり、公開データとシステムのみが機能を提供します)。

フレームワーク/ API /エンジンを開発していることに注意してください。したがって、目標は、誰でも簡単に拡張できることです。これには、新しいタイプのレンダリングまたは衝突コンポーネントの追加などが含まれます。

OOPアプローチの問題

  • コンポーネントは他のコンポーネントのデータにアクセスする必要があります。例えば。 renderコンポーネントのdrawメソッドは、transformコンポーネントの位置にアクセスする必要があります。これにより、コードに依存関係が作成されます。

  • コンポーネントは多態性である可能性があり、さらに複雑さをもたらします。例えば。レンダーコンポーネントの仮想描画メソッドをオーバーライドするSpriteレンダーコンポーネントが存在する可能性があります。

純粋なアプローチの問題

  • ポリモーフィックな動作(レンダリングなど)はどこかに実装する必要があるため、システムに外部委託するだけです。 (例:スプライトレンダーシステムは、レンダーノードを継承するスプライトレンダーノードを作成し、それをレンダーエンジンに追加します)

  • システム間の通信は避けるのが難しい場合があります。例えば。衝突システムには、そこにある具体的なレンダリングコンポーネントから計算される境界ボックスが必要になる場合があります。これは、データを介して通信させることで解決できます。ただし、レンダリングシステムがバウンディングボックスコンポーネントを更新し、衝突システムがそれを使用するため、これによりインスタント更新が削除されます。システムの更新関数を呼び出す順序が定義されていないと、問題が発生する可能性があります。他のシステムがハンドラーをサブスクライブできるイベントをシステムが発生できるようにするイベントシステムがあります。ただし、これはシステムに何をすべきかを指示するためにのみ機能します。

  • 追加のフラグが必要です。たとえば、タイルマップコンポーネントを見てみましょう。サイズ、タイルサイズ、インデックスリストフィールドがあります。タイルマップシステムは、それぞれの頂点配列を処理し、コンポーネントのデータに基づいてテクスチャ座標を割り当てます。ただし、フレームごとにタイルマップ全体を再計算するにはコストがかかります。したがって、システムでそれらを更新するために行われたすべての変更を追跡するために、リストが必要になります。 OOPの方法では、これはタイルマップコンポーネントによってカプセル化できます。たとえば、SetTile()メソッドは、呼び出されるたびに頂点配列を更新します。

純粋なアプローチの美しさはわかりますが、従来のOOPよりも具体的にどのようなメリットがあるのか​​はよくわかりません。コンポーネント間の依存関係は、システムに隠されていますが、依然として存在しています。また、同じ目標を達成するには、さらに多くのクラスが必要になります。これは私には、あまり良い設計のソリューションではないように思えます。

さらに、私はパフォーマンスにそれほど関心がないので、データ指向の設計とキャッシュミスのこの全体的な考えは、私にはそれほど重要ではありません。素敵な建築物が欲しいだけです^^

それでも、私が読んだ記事と議論のほとんどは、2番目のアプローチを提案しています。 なぜ?

アニメーション

最後に、純粋なECSでアニメーションを処理する方法について質問します。現在、アニメーションを、0と1の間の進行状況に基づいてエンティティを操作するファンクタとして定義しています。アニメーションコンポーネントには、アニメーションのリストを含むアニメーターのリストがあります。次に、更新機能で、現在アクティブなアニメーションをエンティティに適用します。

注:

私はこの投稿を読んだところです エンティティコンポーネントシステムアーキテクチャオブジェクトは定義によって指向されていますか? これは、問題を私よりも少しよく説明しています。基本的に同じトピックを扱っていますが、それでも答えはありません理由について純粋なデータアプローチの方が優れています。

11
Adrian Koch

これは難しい問題です。私の特定の経験(YMMV)に基づいて、いくつかの質問に取り組みます。

コンポーネントは他のコンポーネントのデータにアクセスする必要があります。例えば。 renderコンポーネントのdrawメソッドは、transformコンポーネントの位置にアクセスする必要があります。これにより、コードに依存関係が作成されます。

ここでは、カップリング/依存関係の量と複雑さ(程度ではなく)を過小評価しないでください。あなたはこれの違いを見ているかもしれません(そしてこの図はすでにおもちゃのようなレベルに途方もなく単純化されており、実際の例は結合を緩めるためにその間にインターフェースを持っています):

enter image description here

... この:

enter image description here

...またはこれ:

enter image description here

コンポーネントは多態性である可能性があり、さらに複雑さをもたらします。例えば。レンダーコンポーネントの仮想描画メソッドをオーバーライドするSpriteレンダーコンポーネントが存在する可能性があります。

そう? vtableおよび仮想ディスパッチに類似する(またはリテラル)同等物は、オブジェクトがその基礎となる状態/データを隠すのではなく、システムを介して呼び出すことができます。ポリモーフィズムは、類似のvtableまたは関数ポインターがシステムが呼び出す「データ」のような種類の「データ」に変わる場合、「純粋な」ECS実装でまだ非常に実用的で実現可能です。

ポリモーフィックな動作(レンダリングなど)はどこかに実装する必要があるため、システムに外部委託するだけです。 (例:スプライトレンダーシステムは、レンダーノードを継承するスプライトレンダーノードを作成し、それをレンダーエンジンに追加します)

そう?私はこれが皮肉になっていないことを望みます(私は頻繁に非難されてきましたが、私の意図ではありませんが、テキストを通して感情をよりよく伝えたいと思います)。しかし、この場合の「アウトソーシング」ポリモーフィックな動作は、必ずしも追加の負担となるわけではありません。生産性へのコスト。

システム間の通信は避けるのが難しい場合があります。例えば。衝突システムには、そこにある具体的なレンダリングコンポーネントから計算される境界ボックスが必要になる場合があります。

この例は私には特に奇妙に思えます。レンダラーがシーンにデータを出力する理由(私は通常、このコンテキストではレンダラーを読み取り専用と見なします)、またはレンダラーが他のシステムではなくAABBを把握して、レンダラーと衝突/物理(ここで「レンダーコンポーネント」の名前に引っかかるかもしれません)。それでも、この例にこだわりすぎないようにしたいのは、それがあなたがしようとしていることではないということです。それでも、システム間の通信(システムが他のユーザーによって行われた変換に直接依存している中央のECSデータベースへの間接的な読み取り/書き込みの形式であっても)は、必要であれば頻繁に行う必要はありません。これは、評価の順序を前もって決定することの重要性について以下で私が書いたものの一部と矛盾していますが、これは「正確さ」ではなく、ユーザーの応答に対する実際的なニーズがあります(これは必ずしも時間的なカップリングの問題ではなく、フレーム出力を保証するユーザー側の設計の問題です)遅れずに最新の結果)。

システムの更新関数を呼び出す順序が定義されていないと、問題が発生する可能性があります。

これは絶対に定義する必要があります。 ECSは、コードベース内のすべての可能なシステムのシステム処理評価順序を再配置し、フレームとFPSを扱うエンドユーザーにまったく同じ種類の結果を返すための最終的なソリューションではありません。これは、ECSを設計するときに少なくとも前もってある程度予想する必要があることを強くお勧めすることの1つです(ただし、後で変更するための呼吸の余裕がたくさんあるので、順序の最も重要な側面が変更されない場合)システムの呼び出し/評価)。

ただし、フレームごとにタイルマップ全体を再計算するにはコストがかかります。したがって、システムでそれらを更新するために行われたすべての変更を追跡するために、リストが必要になります。 OOPの方法では、これはタイルマップコンポーネントによってカプセル化できます。たとえば、SetTile()メソッドは、呼び出されるたびに頂点配列を更新します。

これはデータ指向の問題であることを除いて、私はこれを完全には理解していませんでした。そして、そのようなパフォーマンスの落とし穴を回避するために、メモ化を含む、ECSでのデータの表現と格納に関する落とし穴はありません(ECSの最大の落とし穴は、特定のコンポーネントタイプの使用可能なインスタンスをクエリするシステムなどに関連する傾向があります一般化されたECSの最適化の最も困難な側面)。ロジックとデータが「純粋な」ECSで分離されているという事実は、OOP表現でキャッシュ/メモ化されていた可能性があるものを突然再計算する必要があることを意味しません。それは問題/無関係です。私が非常に重要なものに目を通さない限り、ポイント。

「純粋な」ECSを使用しても、このデータをタイルマップコンポーネントに格納できます。唯一の大きな違いは、この頂点配列を更新するロジックがどこかのシステムに移動することです。

TileMapCacheのような個別のコンポーネントを作成する場合は、ECSを利用して、エンティティからのこのキャッシュの無効化と削除を簡素化することもできます。その時点で、キャッシュが必要であるがTileMapコンポーネントを持つエンティティでは使用できない場合、それを計算して追加できます。無効化または不要になった場合、そのような無効化と削除のために特別にコードを記述する必要なく、ECSを介して削除できます。

コンポーネント間での依存関係は、システムに隠されていても存在します

「純粋な」担当者のコンポーネント間に依存関係はありません(依存関係がシステムによってここに隠されていると言っても、私はまったく正しいとは思いません)。データは、いわばデータに依存していません。ロジックはロジックに依存します。また、「純粋な」ECSは、システムが動作するために必要なデータとロジックの絶対最小サブセット(多くの場合はなし)に依存するような方法でロジックを作成する傾向にあります。実際のタスクに必要な機能よりはるかに多くの機能。純粋なECS権限を使用している場合、最初に評価する必要があるのは、デカップリングの利点であると同時に、OOPカプセル化および特に情報の非表示について理解するために学んだことすべてに疑問を投げかけることです。

デカップリングとは、具体的には、システムが機能するために必要な情報がどれだけ少ないかを意味します。モーションシステムは、ParticleCharacterなどのはるかに複雑なものについても知る必要はありません(システムの開発者は、そのようなエンティティのアイデアがシステム)。構造体の少数のフロートと同じくらい簡単な位置コンポーネントのような最小限のデータについて知る必要があるだけです。 IMotionのような純粋なインターフェースがそれと一緒に運ぶ傾向があるものよりも、情報と外部依存関係はさらに少なくなります。これは主に、各システムが動作するために必要なこの最小限の知識によるものであり、ECSが非常に予期せぬ設計変更を後から見直すことを許し、至る所でカスケードインターフェースの破損に直面することはありません。

変更が連鎖的な破損を引き起こさないシステムにロジックが厳密にローカライズされていないため、提案する「純粋でない」アプローチはその利点をいくぶん減らします。ロジックは、複数のシステムによってアクセスされるコンポーネントにある程度集中化されます。コンポーネントは、それを使用できるさまざまなシステムすべてのインターフェース要件を満たす必要があり、今ではすべてのシステムがより多くの(依存する)知識を持つ必要があるようですそれが厳密にそのコンポーネントで動作する必要があるよりも情報。

データへの依存関係

ECSについて論争の的となっていることの1つは、抽象インターフェースへの依存関係である可能性のあるものを、生データのみで置き換える傾向があることです。これは一般に、あまり望ましくなく、より緊密な結合形式と見なされます。しかし、ECSが非常に有益なゲームのような種類のドメインでは、多くの場合、システムの中央レベルでそのデータを使用して実行できることを設計するよりも、データ表現を事前に設計して安定性を保つ方が簡単です。これは、コードベースでベテランのベテランの間でも、IMotionのようなCOMスタイルの純粋なインターフェイスアプローチをより多く利用することを痛感しました。

開発者は、この中央インターフェイスに関数を追加、削除、または変更する理由を見つけ続けました。変更は、システム内のすべての場所とともにIMotionを実装したすべてのクラスをすべて破壊する傾向があるため、それぞれに多大なコストとコストがかかりました。 IMotionを使用した。一方、非常に多くの痛みを伴うカスケード変更が発生する間、IMotionを実装したオブジェクトはすべて、フロートの4x4マトリックスを格納するだけであり、インターフェイス全体は、これらのフロートの変換方法とアクセス方法にのみ関係していました。データ表現は最初からずっと安定しており、この集中化されたインターフェースは、予期しない設計のニーズによって変更される傾向があり、そもそも存在していなければ、多くの苦痛を回避できたはずです。

これはすべてグローバル変数のように嫌なことのように聞こえるかもしれませんが、ECSがシステムを介してタイプによって明示的に取得されるコンポーネントにこのデータを編成する方法の性質がそうする一方で、コンパイラーは情報の非表示、アクセスおよび変更する場所などを強制できませんデータは一般に非常に明示的で明白であり、不変量を効果的に維持し、システム間でどのような変換と副作用が発生するかを予測します(実際にはOOPシステムがどのようにフラットなパイプラインになるかを考えると、特定のドメインでは)。

enter image description here

最後に、純粋なECSでアニメーションを処理する方法について質問します。現在、アニメーションを、0と1の間の進行状況に基づいてエンティティを操作するファンクタとして定義しています。アニメーションコンポーネントには、アニメーションのリストを含むアニメーターのリストがあります。次に、更新機能で、現在アクティブなアニメーションをエンティティに適用します。

私たちはみなここで実用主義者です。 gamedevでさえ、おそらく矛盾するアイデア/答えを得るでしょう。最も純粋なECSでさえ、比較的新しい現象であり、先駆的な領域です。そのため、人々は猫の皮を剥ぐ方法について最も強い意見を必ずしも表明していません。私の直感的な反応は、レンダリングシステムが表示するアニメーションコンポーネントのこの種のアニメーションの進行を増分するアニメーションシステムですが、それは特定のアプリケーションとコンテキストの多くのニュアンスを無視しています。

ECSを使用しても、それは特効薬ではありません。新しいシステムを追加したり、いくつかを削除したり、新しいコンポーネントを追加したり、既存のシステムを変更して新しいコンポーネントタイプを取得したりする傾向があります。まだ最初は正しかった。しかし、私の場合の違いは、特定のデザインニーズを事前に予測できない場合、中心的な変更は行わないということです。私はあちこちに行き、新しいニーズに対応するために多くのコードを変更する必要があるカスケード破損の波及効果を取得していません。これはかなりの時間の節約になります。特定のシステムに腰を下ろすと、それに関連するコンポーネント(単なるデータ)以外のことについて何も知る必要がないので、頭がより楽になります。

10
Dragon Energy