OOP ECSとPure ECS


11

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

過去1か月間に、Entity-Component-Systemsについてたくさん読んだので、今はその概念にとても慣れています。ただし、明確な「定義」が欠落しているように見える側面が1つあり、記事によって根本的に異なる解決策が提案されています。

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

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

OOPアプローチの問題

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

  • コンポーネントはポリモーフィックになる可能性があり、さらに複雑さをもたらします。たとえば、レンダーコンポーネントの仮想描画メソッドをオーバーライドするスプライトレンダーコンポーネントがある場合があります。

純粋なアプローチの問題

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

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

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

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

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

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

アニメーション

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

注意:

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


1
おそらくシンプルだが深刻な質問です。ECSの長所と短所を知っていますか。それは主に「理由」を説明しています。
Caramiriel

まあ、私は多重継承ectを通じて死のダイヤを回避するために、継承ではなくコンポーネントを使用することの利点を理解しています。コンポーネントを使用すると、実行時に動作を操作することもできます。そしてそれらはモジュール式です。私が理解していないのは、なぜデータと関数を分割する必要があるのか​​です。私の現在の実装はgithub github.com/AdrianKoch3010/MarsBaseProjectにあります
Adrian Koch

ええと、ECSで十分な経験を積んでいないので、完全な答えを追加することはできません。ただし、コンポジションはDoDを回避するためだけに使用されるのではありません。OOアプローチを使用して生成するのが難しい(固有の)エンティティを実行時に作成することもできます。とは言っても、データ/手順を分割することで、データを推測しやすくなります。シリアル化、状態の保存、元に戻す/やり直しなどを簡単に実装できます。データについて推論するのは簡単なので、データを最適化するのも簡単です。ほとんどの場合、エンティティをバッチ(マルチスレッド)に分割するか、エンティティを他のハードウェアにオフロードして、その可能性を最大限に引き出すことができます。
Caramiriel

「レンダーコンポーネントの仮想描画メソッドをオーバーライドするスプライトレンダーコンポーネントがある可能性があります。」あなたがそれをする/必要とするならば、私はそれがもはやECS ではないと主張します。
wondra 2018

回答:


10

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

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

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

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

... この:

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

...またはこれ:

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

コンポーネントはポリモーフィックになる可能性があり、さらに複雑さをもたらします。たとえば、レンダーコンポーネントの仮想描画メソッドをオーバーライドするスプライトレンダーコンポーネントがある場合があります。

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

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

そう?私はこれが皮肉として脱落しないことを望みます(私は頻繁に非難されてきましたが、私の意図ではありませんが、テキストを通じて感情をよりよく伝えることができればと思います)。生産性へのコスト。

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

この例は私には特に奇妙に思えます。レンダラーがシーンにデータを出力する理由(私は通常、このコンテキストではレンダラーを読み取り専用と見なします)、またはレンダラーが他のシステムではなくAABBを把握して、レンダラーと衝突/物理(ここで「レンダーコンポーネント」の名前に引っかかるかもしれません)。それでも、この例にこだわりすぎないようにしたいのは、それがあなたが意図していることではないということです。それでも、システム間の通信(システムが他のユーザーによって行われた変換に直接依存している中央のECSデータベースへの間接的な読み取り/書き込みの形式であっても)は、必要であれば頻繁に行う必要はありません。それ'

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

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

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

これはデータ指向の問題であることを除いて、私はこれを完全には理解していませんでした。また、このようなパフォーマンスの落とし穴を回避するために、メモ化を含むECSでのデータの表現と保存に関する落とし穴はありません(ECSの最大の落とし穴は、一般化されたECSの最適化の最も困難な側面)。ロジックとデータが「純粋な」ECSで分離されているという事実は、そうでなければOOP表現でキャッシュ/メモ化できたであろうものを突然再計算しなければならないという意味ではありません。これは、私が非常に重要なことをざっと見ない限り、重要な/無関係なポイントです。

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

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

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

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

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

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

データへの依存

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

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

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

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.