21

私が作成している 2D ゲーム (Android 用) では、GameObject が複数の GameComponent オブジェクトを保持するコンポーネント ベースのシステムを使用しています。GameComponents には、入力コンポーネント、レンダリング コンポーネント、弾丸放出コンポーネントなどがあります。現在、GameComponents はそれらを所有し、それを変更できるオブジェクトへの参照を持っていますが、GameObject 自体はコンポーネントのリストを持っているだけで、オブジェクトが更新されたときに更新できる限り、コンポーネントが何であるかは気にしません。

コンポーネントには、GameObject が知る必要のある情報が含まれている場合があります。たとえば、衝突検出の場合、GameObject はそれ自体を衝突検出サブシステムに登録して、別のオブジェクトと衝突したときに通知を受けます。衝突検出サブシステムは、オブジェクトのバウンディング ボックスを認識する必要があります。x と y をオブジェクトに直接格納します (複数のコンポーネントで使用されるため) が、幅と高さは、オブジェクトのビットマップを保持するレンダリング コンポーネントにしかわかりません。その情報を取得するメソッド getBoundingBox または getWidth を GameObject に入れたいと思います。または、一般的に、コンポーネントからオブジェクトに情報を送信したいと考えています。しかし、私の現在の設計では、GameObject はリストにある特定のコンポーネントを認識していません。

この問題を解決するには、いくつかの方法が考えられます。

  1. コンポーネントの完全に一般的なリストを用意する代わりに、GameObject にいくつかの重要なコンポーネント用の特定のフィールドを持たせることができます。たとえば、renderingComponent というメンバー変数を持つことができます。使用するオブジェクトの幅を取得する必要があるときはいつでもrenderingComponent.getWidth()。このソリューションでは、コンポーネントの一般的なリストを引き続き使用できますが、一部のコンポーネントの扱いが異なります。より多くのコンポーネントを照会する必要があるため、いくつかの例外的なフィールドが発生することになるのではないかと心配しています。一部のオブジェクトには、レンダリング コンポーネントさえありません。

  2. 必要な情報を GameObject のメンバーとして保持しますが、コンポーネントがそれを更新できるようにします。したがって、オブジェクトの幅と高さはデフォルトで 0 または -1 ですが、レンダリング コンポーネントは更新ループでそれらを正しい値に設定できます。これはハックのように感じられ、すべてのオブジェクトがそれらを必要としない場合でも、利便性のために多くのものを GameObject クラスにプッシュすることになる可能性があります。

  3. 照会できる情報の種類を示すインターフェースをコンポーネントに実装させます。たとえば、レンダリング コンポーネントは、getWidth や getHeight などのメソッドを含む HasSize インターフェイスを実装します。GameObject が幅を必要とする場合、そのコンポーネントをループして HasSize インターフェイスを実装しているかどうかを確認します ( Java またはC# でinstanceofキーワードを使用)。isこれはより一般的な解決策のように思えますが、欠点の 1 つは、コンポーネントの検索に時間がかかる場合があることです (ただし、ほとんどのオブジェクトには 3 つまたは 4 つのコンポーネントしかありません)。

この質問は特定の問題に関するものではありません。それは私のデザインで頻繁に出てきて、それを処理する最善の方法は何だろうと考えていました。これはゲームであるため、パフォーマンスは多少重要ですが、オブジェクトあたりのコンポーネントの数は一般的に少なくなります (最大 8)。

ショートバージョン

ゲームのコンポーネント ベースのシステムで、一般的なデザインを維持しながら、コンポーネントからオブジェクトに情報を渡す最良の方法は何ですか?

4

4 に答える 4

18

GameDev.net (ゲームオブジェクトは通常「エンティティ」と呼ばれます) で週に 3 ~ 4 回、この質問に関するバリエーションが得られますが、これまでのところ、最善のアプローチについてコンセンサスは得られていません。ただし、いくつかの異なるアプローチが実行可能であることが示されているため、あまり心配する必要はありません。

ただし、通常、問題はコンポーネント間の通信に関するものです。コンポーネントからエンティティに情報を取得することについて心配する人はめったにいません。エンティティが必要な情報を知っている場合、アクセスする必要があるコンポーネントのタイプと、そのコンポーネントでどのプロパティまたはメソッドを呼び出す必要があるかを正確に知っていると思われます。データ。アクティブではなくリアクティブにする必要がある場合は、コールバックを登録するか、コンポーネントにオブザーバー パターンを設定して、コンポーネント内の何かが変更されたときにエンティティに通知し、その時点で値を読み取ります。

完全に汎用的なコンポーネントはほとんど役に立ちません。何らかの既知のインターフェイスを提供する必要があります。そうでなければ、存在する意味がほとんどありません。それ以外の場合は、型指定されていない値の大きな連想配列を取得して、それを処理することもできます。Java、Python、C#、およびその他の C++ よりもわずかに高レベルの言語では、リフレクションを使用して、型やインターフェイスの情報をコンポーネント自体にエンコードすることなく、特定のサブクラスを使用するより一般的な方法を提供できます。

コミュニケーションに関して:

一部の人々は、エンティティには常に既知のコンポーネント タイプのセット (各インスタンスはいくつかの可能なサブクラスの 1 つ) が含まれているため、他のコンポーネントへの直接参照を取得し、そのパブリック インターフェイスを介して読み書きできると仮定しています。

一部の人々は、パブリッシュ/サブスクライブ、シグナル/スロットなどを使用して、コンポーネント間の任意の接続を作成しています。これはもう少し柔軟に見えますが、最終的には、これらの暗黙の依存関係を知っている何かが必要です。(そして、これがコンパイル時にわかっている場合は、なぜ以前のアプローチを使用しないのでしょうか?)

または、すべての共有データをエンティティ自体に配置し、それを各コンポーネントが読み書きできる共有通信領域 (AIの黒板システムにわずかに関連する) として使用することもできます。通常、これには、予期したときに存在しない特定のプロパティに直面して、ある程度の堅牢性が必要です。また、並列処理には向いていませんが、小さな組み込みシステムでは大きな問題になるとは思いません...?

最後に、エンティティがまったく存在しないシステムを持っている人もいます。コンポーネントはサブシステム内に存在し、エンティティの唯一の概念は特定のコンポーネントの ID 値です。レンダリング コンポーネント (レンダリング システム内) とプレーヤー コンポーネント (プレーヤー システム内) が同じ ID を持っている場合は、前者は後者の描画を処理します。しかし、これらのコンポーネントのいずれかを集約する単一のオブジェクトはありません。

于 2010-07-05T14:35:18.773 に答える
12

他の人が言ったように、ここに常に正しい答えがあるわけではありません。ゲームが異なれば、ソリューションも異なります。多数の異なる種類のエンティティを含む大規模で複雑なゲームを構築している場合、コンポーネント間のある種の抽象的なメッセージングを備えた、より分離された汎用アーキテクチャは、取得する保守性のために努力する価値があるかもしれません。同様のエンティティを持つ単純なゲームの場合、その状態をすべて GameObject にプッシュするのが最も理にかなっている場合があります。

境界ボックスをどこかに保存する必要があり、衝突コンポーネントのみがそれを気にする特定のシナリオでは、次のようにします。

  1. 衝突コンポーネント自体に保存します。
  2. 衝突検出コードをコンポーネントで直接動作させます。

そのため、コリジョン エンジンがゲームオブジェクトのコレクションを反復処理してインタラクションを解決する代わりに、CollisionComponents のコレクションを直接反復処理します。衝突が発生すると、それを親のゲームオブジェクトにプッシュするのはコンポーネント次第です。

これにより、いくつかの利点が得られます。

  1. コリジョン固有の状態を GameObject から除外します。
  2. 衝突コンポーネントを持たないゲームオブジェクトを繰り返し処理する必要がなくなります。(視覚効果や装飾などの非インタラクティブなオブジェクトが多数ある場合、これによりかなりの数のサイクルを節約できます。)
  3. オブジェクトとそのコンポーネントの間を歩く燃焼サイクルからあなたを解放します。オブジェクトを反復処理してからgetCollisionComponent()各オブジェクトに対して実行すると、そのポインタの追跡によってキャッシュ ミスが発生する可能性があります。すべてのオブジェクトのすべてのフレームでこれを行うと、多くの CPU が消費される可能性があります。

興味があれば、このパターンの詳細はこちらにありますが、その章の内容のほとんどは既に理解されているようです。

于 2010-07-05T21:12:39.610 に答える
4

イベントバス」をご利用ください。(おそらくコードをそのまま使用することはできませんが、基本的な考え方がわかるはずです)。

基本的に、すべてのオブジェクトが自分自身をリスナーとして登録し、「Xが発生した場合、知りたい」と言うことができる中央リソースを作成します。ゲームで何かが起こったとき、責任のあるオブジェクトはイベントXをイベントバスに送信するだけで、すべての興味深い関係者が気付くでしょう。

[編集]より詳細な議論については、メッセージパッシングを参照してください(これを指摘してくれたsnk_kidに感謝します)。

于 2010-07-05T14:04:50.727 に答える
3

1 つの方法は、コンポーネントのコンテナーを初期化することです。各コンポーネントはサービスを提供でき、他のコンポーネントからのサービスを必要とする場合もあります。プログラミング言語と環境に応じて、この情報を提供する方法を考え出す必要があります。

最も単純な形式では、コンポーネント間に 1 対 1 の接続がありますが、1 対多の接続も必要になります。たとえば、CollectionDetectorには を実装するコンポーネントのリストがありますIBoundingBox

初期化中、コンテナーはコンポーネント間の接続を結び付けます。ランタイム中に追加のコストは発生しません。

これはソリューション 3) に近いものです。コンポーネント間の接続は 1 回だけ配線され、ゲーム ループの反復ごとにチェックされるわけではありません。

Managed Extensibility Framework for .NETは、この問題に対する優れたソリューションです。あなたが Android で開発するつもりであることは承知していますが、このフレームワークから何らかのインスピレーションを得ることができるかもしれません。

于 2010-07-05T14:22:16.457 に答える