コンポーネントを更新する時期/場所


10

通常の継承の重いゲームエンジンの代わりに、よりコンポーネントベースのアプローチをいじっています。ただし、コンポーネントに機能させる場所を正当化するのに苦労します。

コンポーネントのリストを持つ単純なエンティティがあるとします。もちろん、エンティティはこれらのコンポーネントが何であるかを知りません。画面上のエンティティに位置を与えるコンポーネントが存在する場合もあれば、画面上にエンティティを描画するためのコンポーネントが存在する場合もあります。

これらのコンポーネントが機能するためには、フレームごとに更新する必要があります。これを行う最も簡単な方法は、シーンツリーをウォークスルーし、エンティティごとに各コンポーネントを更新することです。ただし、コンポーネントによっては、もう少し管理が必要になる場合があります。たとえば、エンティティを衝突可能にするコンポーネントは、すべての衝突可能コンポーネントを監視できる何かによって管理される必要があります。エンティティを描画可能にするコンポーネントは、他のすべての描画可能コンポーネントを監視して、描画順序などを把握する必要があります。

だから私の質問は、どこでコンポーネントを更新するのですか、それらをマネージャーに届けるクリーンな方法は何ですか?

コンポーネントタイプごとにシングルトンマネージャーオブジェクトを使用することを考えましたが、シングルトンを使用するという通常の欠点があります。これを緩和する方法は、依存関係の注入を使用することですが、この問題に対して過剰に聞こえます。また、シーンツリーをウォークスルーして、ある種のオブザーバーパターンを使用してさまざまなコンポーネントをリストにまとめることもできますが、すべてのフレームを実行するのは少し無駄です。


1
システムを何らかの方法で使用していますか?
Asakeron 2013

コンポーネントシステムは、これを行う通常の方法です。私は個人的に、すべてのエンティティでupdateを呼び出すだけです。これは、すべてのコンポーネントでupdateを呼び出し、いくつかの「特別な」ケースがあります(静的な衝突検出用の空間マネージャーなど)。
ashes999 2013

コンポーネントシステム?今まで聞いたことがありません。私はグーグルを開始しますが、お勧めのリンクを歓迎します。
ロイT.

1
エンティティシステムはMMOG開発の未来であり、優れたリソースです。そして、正直に言うと、私は常にこれらのアーキテクチャ名に戸惑っています。提案されたアプローチとの違いは、コンポーネントはデータのみを保持し、システムはそれを処理することです。この答えも非常に関連があります。
Asakeron 2013

1
私はこの件について、一種の蛇行するブログ投稿をここに書き
11

回答:


15

マイクアクトンの3つの大きな嘘を読むことから始めることをお勧めします。私は真剣です、これはあなたがコードをデザインする方法を変えるでしょう:http : //cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

どちらに違反しますか?

嘘#3-コードはデータよりも重要

あなたは依存性注入について話しました。これは一部(および一部)のインスタンスで役立つ場合がありますが、特にゲーム開発で使用する場合は、常に大きな大きな警報音が鳴るはずです。どうして?それは、しばしば不必要な抽象化だからです。そして、間違った場所での抽象化は恐ろしいことです。だからあなたはゲームを持っています。ゲームには、さまざまなコンポーネントのマネージャーがあります。コンポーネントはすべて定義されています。したがって、マネージャーを「持っている」メインゲームループコードのどこかにクラスを作成します。お気に入り:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

各マネージャークラス(getBulletManager())を取得するいくつかのゲッター関数を与えます。おそらく、このクラス自体がシングルトンであるか、1つのクラスから到達可能です(とにかく中央のゲームシングルトンがどこかにあるはずです)。明確に定義されたハードコードされたデータと動作には何も問題はありません。

マネージャーを使用したい他のクラスがそのキーを使用して取得できるキーを使用してマネージャーを登録できるManagerManagerを作成しないでください。それは素晴らしいシステムであり、非常に柔軟ですが、ここでゲームについて話しているところです。どのシステムがゲームに含まれているかが正確にわかります。なぜあなたがしないようなふりをするのですか?これは、データよりもコードの方が重要だと考える人のためのシステムだからです。彼らは「コードは柔軟性があり、データはそれを満たすだけだ」と言うでしょう。しかし、コードは単なるデータです。私が説明したシステムは、はるかに簡単で、信頼性が高く、保守が簡単で、はるかに柔軟です(たとえば、1つのマネージャーの動作が他のマネージャーと異なる場合、システム全体をやり直すのではなく、数行変更するだけで済みます)。

嘘#2-コードは世界のモデルを中心に設計する必要があります

これで、ゲームの世界にエンティティができました。エンティティには、その動作を定義するいくつかのコンポーネントがあります。したがって、Componentオブジェクトのリストと、各ComponentのUpdate()関数を呼び出すUpdate()関数を使用してEntityクラスを作成します。正しい?

いいえ:)それは世界のモデルを中心に設計しています。あなたはゲームに弾丸を持っているので、弾丸クラスを追加します。次に、各弾丸を更新して次の弾丸に進みます。これはあなたのパフォーマンスを完全に殺します、そして、それはあなたにあらゆる場所で重複したコードと恐ろしい複雑なコードベースを与え、同様のコードの論理的な構造化をしません。(伝統的なOO設計がなぜ悪いのか、またはデータ指向設計を調べる理由の詳細については、ここで私の回答を確認してください)

OOバイアスのない状況を見てみましょう。私たちは以下を望んでいます。エンティティまたはオブジェクトのクラスを作成する必要がないことに注意してください。

  • エンティティがたくさんあります
  • エンティティは、エンティティの動作を定義するいくつかのコンポーネントで構成されています
  • ゲームの各コンポーネントをフレームごとに、できれば制御された方法で更新したい
  • コンポーネントを一緒に属しているものとして識別することを除いて、エンティティ自体が行う必要があることは何もありません。いくつかのコンポーネントのリンク/ IDです。

そして状況を見てみましょう。お使いのコンポーネントシステムはの行動に更新されますすべてのゲーム内のオブジェクトのすべてのフレームを。これは間違いなくあなたのエンジンの重要なシステムです。ここではパフォーマンスが重要です!

コンピュータアーキテクチャまたはデータ指向設計のどちらかをよく知っている場合は、メモリを密にパックし、コード実行をグループ化することで、最高のパフォーマンスを実現する方法を知っています。ABCABCABCのようにコードA、B、Cのスニペットを実行すると、AAABBBCCCのように実行した場合と同じパフォーマンスは得られません。これは、命令とデータキャッシュがより効率的に使用されるためだけでなく、すべての "A"を次々に実行する場合、最適化の余地がたくさんあるためです。重複するコードの削除、使用されるデータの事前計算すべての「A」など

したがって、すべてのコンポーネントを更新する場合は、更新関数を使用してそれらをクラス/オブジェクトにしないでください。各エンティティの各コンポーネントに対してその更新関数を呼び出さないでください。それが「ABCABCABC」ソリューションです。同一のコンポーネントの更新をすべてグループ化しましょう。次に、すべてのAコンポーネントを更新し、次にBなどを更新します。これを行うには何が必要ですか?

まず、コンポーネントマネージャーが必要です。ゲーム内のすべてのタイプのコンポーネントについて、マネージャークラスが必要です。そのタイプのすべてのコンポーネントを更新する更新関数があります。そのタイプの新しいコンポーネントを追加する作成関数と、指定されたコンポーネントを破棄する削除関数があります。そのコンポーネントに固有のデータを取得および設定する他のヘルパー関数がある場合があります(例:モデルコンポーネントの3Dモデルの設定)。マネージャーはある意味で外の世界へのブラックボックスであることに注意してください。各コンポーネントのデータがどのように保存されているかはわかりません。各コンポーネントがどのように更新されるかはわかりません。コンポーネントが適切に動作する限り、問題はありません。

次に、エンティティが必要です。これをクラスにすることもできますが、それはほとんど必要ありません。エンティティは、一意の整数IDまたはハッシュされた文字列(整数でもかまいません)にすぎません。エンティティのコンポーネントを作成するときに、IDを引数としてManagerに渡します。コンポーネントを削除する場合は、IDを再度渡します。エンティティを単にIDにする代わりに、エンティティにもう少しデータを追加することにはいくつかの利点があるかもしれませんが、要件に記載したように、すべてのエンティティの動作はコンポーネント自体によって定義されるため、これらはヘルパー関数にすぎません。それはあなたのエンジンですが、あなたにとって意味のあることをしてください。

必要なのはエンティティマネージャーです。このクラスは、IDのみのソリューションを使用する場合に一意のIDを生成するか、Entityオブジェクトの作成/管理に使用できます。また、必要に応じて、ゲーム内のすべてのエンティティのリストを保持することもできます。エンティティマネージャーは、コンポーネントシステムの中心的なクラスであり、ゲーム内のすべてのComponentManagerへの参照を格納し、それらの更新関数を正しい順序で呼び出します。ゲームループが実行する必要があるすべての方法でEntityManager.update()を呼び出すだけで、システム全体が他のエンジンからうまく分離されます。

これが全体像です。コンポーネントマネージャがどのように機能するかを見てみましょう。必要なものは次のとおりです。

  • create(entityID)が呼び出されたときにコンポーネントデータを作成する
  • remove(entityID)が呼び出されたときにコンポーネントデータを削除する
  • update()が呼び出されたときにすべての(適用可能な)コンポーネントデータを更新します(つまり、すべてのコンポーネントが各フレームを更新する必要があるわけではありません)

最後の1つは、コンポーネントの動作/ロジックを定義する場所であり、作成するコンポーネントのタイプに完全に依存します。AnimationComponentは、現在のフレームに基づいてアニメーションデータを更新します。DragableComponentは、マウスでドラッグされているコンポーネントのみを更新します。PhysicsComponentは、物理システムのデータを更新します。それでも、同じタイプのすべてのコンポーネントを一度に更新するので、各コンポーネントがいつでも呼び出すことができる更新関数を持つ個別のオブジェクトである場合は不可能な最適化を実行できます。

コンポーネントデータを保持するXxxComponentクラスの作成をまだ要求していないことに注意してください。それはあなた次第です。データ指向のデザインがお気に入りですか。次に、変数ごとに個別の配列でデータを構造化します。オブジェクト指向デザインがお気に入りですか。(私はそれをお勧めしません、それでも多くの場所であなたのパフォーマンスを殺します)次に、各コンポーネントのデータを保持するXxxComponentオブジェクトを作成します。

マネージャーの素晴らしいところはカプセル化です。現在、カプセル化は、プログラミングの世界で最も恐ろしく誤用されている哲学の1つです。これは、使用方法です。マネージャーだけが、どのコンポーネントデータがどこに保存されているか、コンポーネントのロジックがどのように機能するかを知っています。データを取得/設定するいくつかの関数がありますが、それだけです。マネージャー全体とその基礎となるクラスを書き換えることができ、パブリックインターフェースを変更しないと、誰も気付かないでしょう。物理エンジンを変更しましたか?単にPhysicsComponentManagerを書き換えれば完了です。

そして最後にもう1つ、コンポーネント間の通信とデータ共有があります。現在、これはトリッキーであり、万能のソリューションはありません。たとえば衝突コンポーネントが位置コンポーネント(つまり、PositionManager.getPosition(entityID))から位置を取得できるように、マネージャーでget / set関数を作成できます。イベントシステムを使用できます。いくつかの共有データをエンティティに格納できます(私の考えでは最も醜いソリューション)。メッセージングシステムを使用できます(これはよく使用されます)。または、複数のシステムの組み合わせを使用してください!私はこれらの各システムに進む時間や経験はありませんが、googleとstackoverflow検索はあなたの友達です。


この答えはとても興味深いと思います。質問は1つだけです(あなたまたは誰かが私に応答してくれることを願っています)。DODコンポーネントベースのシステムでエンティティをどのように排除しますか?Artemisでさえ、Entityをクラスとして使用しています。
Wolfrevo Kcats 2013年

1
それを排除するとはどういう意味ですか?Entityクラスのないエンティティシステムを意味しますか?Artemisにエンティティがある理由は、ArtemisではEntityクラスが独自のコンポーネントを管理するためです。私が提案したシステムでは、ComponentManagerクラスがコンポーネントを管理します。したがって、Entityクラスを必要とする代わりに、一意の整数IDを持つことができます。したがって、positionコンポーネントを持つエンティティ254があるとします。位置を変更する場合は、idパラメータを254として、PositionCompMgr.setPosition(int id、Vector3 newPos)を呼び出すことができます。
2013年

しかし、IDをどのように管理しますか?エンティティからコンポーネントを削除して、後で他のコンポーネントに割り当てる場合はどうなりますか?エンティティを削除して新しいエンティティを追加したい場合はどうなりますか?1つのコンポーネントを2つ以上のエンティティ間で共有したい場合はどうなりますか?私はこれに本当に興味があります。
Wolfrevo Kcats 2013年

1
EntityManagerを使用して、新しいIDを配布できます。また、事前定義されたテンプレートに基づいて完全なエンティティを作成するために使用することもできます(たとえば、「EnemyNinja」を作成して、新しいIDを生成し、レンダリング可能、衝突、AIなどの敵忍者を構成するすべてのコンポーネントを作成します。 、など)。また、すべてのComponentManager削除関数を自動的に呼び出すremoveEntity関数を含めることもできます。ComponentManagerは、指定されたエンティティのコンポーネントデータがあるかどうかを確認し、ある場合はそのデータを削除します。
Mart

1
コンポーネントをあるエンティティから別のエンティティに移動しますか?各ComponentManagerに関数swapComponentOwner(int oldEntity、int newEntity)を追加するだけです。データはすべてComponentManagerにあり、必要なのは、それが属する所有者を変更する関数だけです。各ComponentManagerには、どのデータがどのエンティティIDに属するかを格納するためのインデックスまたはマップのようなものがあります。エンティティIDを古いIDから新しいIDに変更するだけです。考えたシステムでコンポーネントを簡単に共有できるかどうかはわかりませんが、どれほど難しいのでしょうか。インデックステーブルの1つのエンティティID <-> Component Dataリンクの代わりに、複数あります。
Mart

3

これらのコンポーネントが機能するためには、フレームごとに更新する必要があります。これを行う最も簡単な方法は、シーンツリーをウォークスルーし、エンティティごとに各コンポーネントを更新することです。

これは、コンポーネントの更新に対する典型的な素朴なアプローチです(そして、それが機能する場合、素朴であることに必ずしも問題はありません)。実際に触れた大きな問題の1つです。コンポーネントのインターフェース(たとえばIComponent)を介して操作しているため、更新した内容については何もわかりません。また、エンティティ内のコンポーネントの順序について何も知らない可能性があります。

  1. 異なるタイプのコンポーネントを頻繁に更新する可能性があります(参照のコードの局所性が本質的に低い)
  2. このシステムは同時更新には適していません。データの依存関係を特定できず、更新を無関係なオブジェクトのローカルグループに分割できないためです。

コンポーネントタイプごとにシングルトンマネージャーオブジェクトを使用することを考えましたが、シングルトンを使用するという通常の欠点があります。これを緩和する方法は、依存関係の注入を使用することですが、この問題に対して過剰に聞こえます。

シングルトンはここでは実際には必要ありません。そのため、シングルトンはあなたが述べた欠点を持っているので、避ける必要があります。依存性注入は過剰ではありません-概念の中心は、オブジェクトが必要とするものをそのオブジェクトに、理想的にはコンストラクターで渡すことです。これには(Ninjectのような)ヘビー級のDIフレームワークは必要ありません-どこかでコンストラクターに追加のパラメーターを渡すだけです。

レンダラーは基本的なシステムであり、おそらくゲーム内の視覚的なもの(スプライトまたはモデルなど)に対応するレンダリング可能なオブジェクトの束の寿命の作成と管理をサポートします。同様に、物理エンジンは、物理シミュレーション(剛体)内を動き回ることができるエンティティを表すものに対して、生涯制御を持っている可能性があります。これらの関連システムのそれぞれは、ある程度の能力でそれらのオブジェクトを所有し、それらを更新する責任があります。

ゲームエンティティ構成システムで使用するコンポーネントは、それらの下位レベルシステムのインスタンスのラッパーでなければなりません。位置コンポーネントはリジッドボディをラップするだけでよく、ビジュアルコンポーネントはレンダリング可能なスプライトやモデルなどをラップするだけです。

次に、下位レベルのオブジェクトを所有するシステム自体がそれらの更新を担当し、一括して、必要に応じてその更新をマルチスレッド化できるように更新を行うことができます。メインゲームループは、これらのシステムが更新される大まかな順序を制御します(最初に物理、次にレンダラーなど)。ライフタイムやサブシステムが提供するインスタンスの更新を制御できないサブシステムがある場合は、単純なラッパーを構築して、そのシステムに関連するすべてのコンポーネントの更新も一括で処理し、配置する場所を決定できます。システムの残りの更新に関連して更新します(これは「スクリプト」コンポーネントで頻繁に発生します)。

詳細を知りたい場合は、この方法をアウトボードコンポーネントアプローチと呼ぶこともあります。

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