エンティティ通信はどのように機能しますか?


115

2つのユーザーケースがあります。

  1. どのようにメッセージをentity_A送信しtake-damageますentity_Bか?
  2. のHPをどのようにentity_A照会しますentity_Bか?

これまでに遭遇したことがあります:

  • メッセージキュー
    1. entity_Atake-damageメッセージを作成し、それをentity_Bのメッセージキューに投稿します。
    2. entity_Aquery-hpメッセージを作成してに投稿しentity_Bます。 entity_B代わりに、response-hpメッセージを作成してに投稿しentity_Aます。
  • パブリッシュ/サブスクライブ
    1. entity_Btake-damageメッセージをサブスクライブします(おそらく、何らかのプリエンプティブフィルタリングを使用して、関連するメッセージのみが配信されます)。 を参照entity_Aするtake-damageメッセージを生成しますentity_B
    2. entity_Aupdate-hpメッセージをサブスクライブします(フィルタリングされる可能性があります)。すべてのフレームがメッセージをentity_Bブロードキャストしupdate-hpます。
  • 信号/スロット
    1. ???
    2. entity_Aupdate-hpスロットをentity_Bupdate-hp信号に接続します。

もっと良いものはありますか?これらの通信方式がゲームエンジンのエンティティシステムにどのように結び付くのかを正しく理解していますか?

回答:


67

良い質問!あなたが尋ねた特定の質問に進む前に、シンプルさの力を過小評価しないでください。Tenpnは正しい。これらのアプローチでやろうとしているのは、関数呼び出しを延期するか、呼び出し元を呼び出し先から切り離すエレガントな方法を見つけることだけです。これらの問題のいくつかを軽減する驚くほど直感的な方法としてコルーチンをお勧めできますが、それは少し話題から外れています。場合によっては、関数を呼び出して、エンティティAがエンティティBに直接結合されているという事実をそのまま使用したほうがよい場合があります。

とはいえ、私はシグナル/スロットモデルと単純なメッセージパッシングを組み合わせて使用​​し、満足しています。私は非常にタイトなスケジュールを持っていたかなり成功したiPhoneタイトルのためにC ++とLuaでそれを使用しました。

シグナル/スロットの場合、エンティティAにエンティティBがしたことに応答して何かをさせたい場合(たとえば、何かが死んだときにドアのロックを解除する)、エンティティAがエンティティBの死亡イベントに直接サブスクライブする場合があります。または、エンティティAがエンティティグループのそれぞれにサブスクライブし、発生した各イベントでカウンターをインクリメントし、それらのNが死亡した後にドアをロック解除する可能性があります。また、「エンティティのグループ」と「それらのN」は、通常、レベルデータで定義されたデザイナーです。(余談ですが、WaitForMultiple( "Dying"、entA、entB、entC); door.Unlock();など、コルーチンが本当に輝くことができる領域の1つです。

しかし、C ++コードと密接に関連する反応、または本質的にはかないゲームイベント(損傷の処理、武器のリロード、デバッグ、プレイヤー主導のロケーションベースのAIフィードバック)に関しては、面倒になる可能性があります。これは、メッセージの受け渡しがギャップを埋めることができる場所です。基本的に、「このエリアのすべてのエンティティに3秒でダメージを与えるように伝える」、「物理学を完成させて誰が撃ったかを把握したら、このスクリプト関数を実行するように指示する」などです。パブリッシュ/サブスクライブまたはシグナル/スロットを使用してうまく行う方法を理解することは困難です。

これは簡単に過剰になります(tenpnの例に対して)。また、多くのアクションがある場合は、非効率的な肥大化になる可能性があります。しかし、欠点にも関わらず、この「メッセージとイベント」アプローチは、スクリプト化されたゲームコード(Luaなど)と非常によく一致します。スクリプトコードは、C ++コードをまったく気にせずに、独自のメッセージとイベントを定義して対応できます。また、スクリプトコードは、レベルの変更、サウンドの再生、TakeDamageメッセージが与えるダメージの量を武器に設定させるなど、C ++コードをトリガーするメッセージを簡単に送信できます。luabindを常にあざける必要がなかったので、時間を大幅に節約できました。そして、あまり多くはなかったので、すべてのluabindコードを1か所に保管することができました。適切に結合すると、

また、ユースケース#2での私の経験では、別の方向のイベントとして処理する方が良いということです。エンティティのヘルスが何であるかを尋ねる代わりに、ヘルスが大幅に変化するたびにイベントを発生させ、メッセージを送信します。

ところで、インターフェイスに関しては、EventHost、EventClient、MessageClientの3つをすべて実装することになりました。EventHostsはスロットを作成し、EventClientsはスロットにサブスクライブ/接続し、MessageClientsはデリゲートをメッセージに関連付けます。MessageClientのデリゲートターゲットは、必ずしも関連付けを所有するオブジェクトと同じである必要はないことに注意してください。つまり、MessageClientsは、メッセージを他のオブジェクトに転送するためだけに存在できます。FWIW、ホスト/クライアントの比phorは一種の不適切です。Source / Sinkはより良い概念かもしれません。

申し訳ありませんが、私はちょっとそこを取り乱しました。それが私の最初の答えです:)私はそれが理にかなったことを望みます。


答えてくれてありがとう。素晴らしい洞察。メッセージパッシングを設計している理由は、Luaが原因です。新しいC ++コードなしで新しい武器を作成できるようにしたいと思います。それで、あなたの考えは私の私の質問に答えました。
deft_code

コルーチンについては、私もコルーチンを強く信じていますが、C ++でコルーチンを使用することはありません。luaコードでコルーチンを使用してブロッキング呼び出し(たとえば、死を待つ)を処理するという漠然とした希望がありました。努力する価値はありましたか?私は、c ++のコルーチンに対する強い欲求に盲目にされるのではないかと心配しています。
deft_code

最後に、iphoneゲームは何でしたか?使用したエンティティシステムに関する詳細情報を入手できますか?
deft_code

2
エンティティシステムはほとんどがC ++でした。そのため、たとえば、Impの動作を処理するImpクラスがありました。Luaは、起動時またはメッセージを介してImpのパラメーターを変更できます。Luaの目標は、厳しいスケジュールに収まることであり、Luaコードのデバッグには非常に時間がかかります。Luaを使用してスクリプトレベル(どのエンティティがどこに行くか、トリガーを押したときに発生するイベント)をスクリプト化しました。だから、Luaでは、SpawnEnt( "Imp")のようなことを言うでしょう。ここで、Impは手動で登録されたファクトリの関連付けです。常に1つのグローバルプールのエンティティに生成されます。素敵でシンプル。多くのsmart_ptrとweak_ptr を使用しました。
バッフル

1
BananaRaffle:これはあなたの答えの正確な要約だと思いますか:「投稿したソリューションの3つはすべて、他のソリューションと同じように使用できます。1つの完璧なソリューションを探してはいけません。 」
イプスクイックグル

76
// in entity_a's code:
entity_b->takeDamage();

あなたはコマーシャルゲームがそれをどのように行うか尋ねました。;)


8
反対票?真剣に、それが通常のやり方です!エンティティシステムは優れていますが、初期のマイルストーンに到達する助けにはなりません。
tenpn

私はFlashゲームをプロとして作り、これが私がそれをする方法です。enemy.damage(10)を呼び出してから、パブリックgetterから必要な情報を検索します。
イアン

7
これは、市販のゲームエンジンが真剣に行う方法です。彼は冗談ではありません。Target.NotifyTakeDamage(DamageType、DamageAmount、DamageDealerなど)は、通常の方法です。
AAグラパス

3
商用ゲームも「損傷」のスペルを間違えていますか?:-P-
リケット

15
はい、それらは、とりわけミススペルダメージを与えます。:)
LearnCocos2D

17

より深刻な答え:

黒板がよく使われるのを見てきました。単純なバージョンは、エンティティのHPのようなもので更新されたストラットにすぎず、エンティティはクエリを実行できます。

黒板は、このエンティティの世界観(Bの黒板にそのHPを尋ねる)、またはエンティティの世界観(Aが黒板に問い合わせてAのHPを確認する)のいずれかです。

フレーム内の同期点でのみ黒板を更新する場合は、後から任意のスレッドからそれらを読み取ることができるため、マルチスレッドの実装は非常に簡単になります。

より高度な黒板は、文字列を値にマッピングするハッシュテーブルのようなものです。これは保守性が高くなりますが、明らかに実行時のコストがかかります。

黒板は伝統的に一方向の通信にすぎません-損傷によるディッシングを処理しません。


私は今まで黒板モデルについて聞いたことがありませんでした。
deft_code

また、イベントキューやパブリッシュ/サブスクライブモデルと同じように、依存関係を減らすのにも適しています。
tenpn

2
これは、「理想的な」E / C / Sシステムが「機能する」方法の標準的な「定義」でもあります。コンポーネントは黒板を構成します。システムはそれに作用するコードです。(もちろん、long long int純粋なECSシステムでは、エンティティはsまたは同様のものです。)
BRPocock

6

私はこの問題を少し研究し、素晴らしい解決策を見ました。

基本的にはサブシステムがすべてです。これは、tenpnが言及した黒板のアイデアに似ています。

エンティティはコンポーネントで構成されていますが、プロパティバッグにすぎません。エンティティ自体に動作は実装されていません。

たとえば、エンティティにはHealthコンポーネントとDamageコンポーネントがあります。

次に、MessageManagerと3つのサブシステム、ActionSystem、DamageSystem、HealthSystemがあります。ある時点で、ActionSystemはゲームワールドで計算を行い、イベントを生成します。

HIT, source=entity_A target=entity_B power=5

このイベントは、MessageManagerに公開されます。ある時点で、MessageManagerは保留中のメッセージを調べて、DamageSystemがHITメッセージをサブスクライブしたことを検出します。これで、MessageManagerはHITメッセージをDamageSystemに配信します。DamageSystemは、Damageコンポーネントを持つエンティティのリストを調べ、ヒットパワーまたは両方のエンティティのその他の状態などに応じてダメージポイントを計算し、イベントを発行します。

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystemはDAMAGEメッセージをサブスクライブしており、MessageManagerがDAMAGEメッセージをHealthSystemに発行すると、HealthSystemはエンティティentity_Aとentity_Bの両方にHealthコンポーネントでアクセスできるため、HealthSystemが計算を実行できますMessageManagerへ)。

このようなゲームエンジンでは、メッセージの形式がすべてのコンポーネントとサブシステム間の唯一のカップリングです。サブシステムとエンティティは完全に独立しており、互いに気付きません。

実際のゲームエンジンがこのアイデアを実装しているかどうかはわかりませんが、かなり堅実でクリーンなようです。いつか自分の趣味レベルのゲームエンジンに自分で実装したいと思っています。


これは、承認済みの回答IMOよりもはるかに優れた回答です。分離、保守、拡張可能(そしての冗談のようなカップリング災害でもありませんentity_b->takeDamage();
ダニーヤロスラフ

4

次のようなグローバルメッセージキューを用意してください。

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

と:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

そして、ゲームループ/イベント処理の最後に:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

これがコマンドパターンだと思います。そして、派生物が何かを定義して実行する、Execute()純粋な仮想in Eventです。だからここに:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

ゲームがシングルプレーヤーの場合は、ターゲットオブジェクトメソッドを使用します(tenpnの推奨どおり)。

マルチプレイヤー(正確にはマルチクライアント)である(またはサポートしたい)場合は、コマンドキューを使用します。

  • クライアント1でAがBにダメージを与えた場合、ダメージイベントをキューに入れるだけです。
  • ネットワーク経由でコマンドキューを同期する
  • 両側でキューに入れられたコマンドを処理します。

2
不正行為を避けることに真剣に取り組んでいる場合、AはクライアントのBをまったく傷つけません。Aを所有するクライアントは「attack B」コマンドをサーバーに送信します。これは、tenpnが言ったことを正確に実行します。サーバーは、その状態を関連するすべてのクライアントと同期します。

@Joe:はい、考慮すべき有効なポイントがあるサーバーがあれば、重いサーバー負荷を回避するためにクライアントを信頼することは問題ありません(コンソール上など)。
アンドレアス

2

私は言うだろう:ダメージからの即時のフィードバックを明示的に必要としない限り、どちらも使用しないでください。

損傷を受けるエンティティ/コンポーネント/何でも、イベントをローカルイベントキューまたは損傷イベントを保持する同等レベルのシステムにプッシュする必要があります。

エンティティaからイベントを要求し、それをエンティティbに渡す両方のエンティティへのアクセスを備えたオーバーレイシステムが必要です。どこからでも何でも使用してイベントをいつでも渡すことができる一般的なイベントシステムを作成しないことにより、コードを常にデバッグしやすくし、パフォーマンスを測定しやすくし、理解しやすく読みやすくする明示的なデータフローを作成します一般的に、より適切に設計されたシステムにつながります。


1

電話をかけるだけです。query-hpに続くrequest-hpを実行しないでください。そのモデルに従えば、あなたは怪我の世界に入ります。

Mono Continuationsもご覧ください。NPCにとって理想的だと思います。


1

では、同じupdate()サイクルでプレーヤーAとBがお互いにヒットしようとするとどうなりますか?プレーヤー1のUpdate()が、サイクル1のプレーヤーBのUpdate()の前に発生したと仮定します(または、ティック、またはそれを呼び出すもの)。私が考えることができる2つのシナリオがあります:

  1. メッセージによる即時処理:

    • プレーヤーA.Update()は、プレーヤーがBをヒットすることを望んでいることを確認し、プレーヤーBは損傷を通知するメッセージを取得します。
    • プレイヤーB.HandleMessage()はプレイヤーBのヒットポイントを更新します(彼は死にます)
    • プレーヤーB.Update()は、プレーヤーBが死亡したことを確認します。プレーヤーAを攻撃することはできません。

これは不公平です。プレイヤーAとBはお互いにヒットするはずです。そのエンティティ/ゲームオブジェクトが後でupdate()を取得したという理由だけで、プレイヤーBはAにヒットする前に死亡しました。

  1. メッセージのキューイング

    • プレーヤーA.Update()は、プレーヤーがBをヒットすることを望み、プレーヤーBが損傷を通知するメッセージを取得し、それをキューに格納します
    • プレーヤーA.Update()はキューをチェックしますが、空です
    • プレイヤーB.Update()は最初に動きをチェックし、プレイヤーBもダメージを与えてプレイヤーAにメッセージを送信します
    • プレーヤーB.Update()もキュー内のメッセージを処理し、プレーヤーAからのダメージを処理します
    • 新しいサイクル(2):プレーヤーAはヘルスポーションを飲みたいため、プレーヤーA.Update()が呼び出され、移動が処理されます
    • プレーヤーA.Update()はメッセージキューをチェックし、プレーヤーBからのダメージを処理します

繰り返しますが、これは不公平です。プレイヤーAは、同じターン/サイクル/ティックでヒットポイントを取ることになっています!


4
あなたは本当に質問に答えているわけではありませんが、あなたの答えは素晴らしい質問になると思います。先に進んで、このような「不公平な」優先順位付けを解決する方法を尋ねてみませんか
bummzack

ほとんどのゲームはこの不公平を気にしているとは思えません。簡単な回避策の1つは、更新時にエンティティリストを順方向と逆方向に繰り返すことです。
キロタン

2つの呼び出しを使用するので、すべてのエンティティに対してUpdate()を呼び出し、ループの後、もう一度繰り返してのようなものを呼び出しますpEntity->Flush( pMessages );。entity_Aが新しいイベントを生成すると、そのフレーム内のentity_Bによって読み取られず(ポーションを受け取る機会もあります)、その後、ダメージを受け、その後、キューの最後になるポーションヒーリングのメッセージを処理します。 。ポーションメッセージはキューの最後にあるので、プレイヤーBはまだ死にます:P
パブロアリエル

フレームレベルでは、ほとんどのゲーム実装は単に不公平だと思います。カイロタンが言ったように。
v.oddou 14

この問題は非常に簡単に解決できます。メッセージハンドラーなどでお互いにダメージを与えるだけです。間違いなく、メッセージハンドラー内でプレーヤーにデッドのフラグを立てるべきではありません。「Update()」では、単に「if(hp <= 0)die();」を実行します。(たとえば、「Update()」の先頭)。そうすれば、両方が同時にお互いを殺すことができます。また:プレイヤーに直接ダメージを与えるのではなく、弾丸などの中間オブジェクトを介してダメージを与えることがよくあります。
タラ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.