コンポーネントベースのエンティティシステムでメッセージ処理を適切に実装する方法は?


30

エンティティシステムのバリアントを実装しています。

  • Entityクラス IDより少しであることが結合コンポーネント一緒に

  • 「コンポーネントロジック」がなく、データのみを持つコンポーネントクラスの

  • 多数のシステムクラス(別名「サブシステム」、「マネージャー」)。これらはすべてのエンティティロジック処理を行います。ほとんどの基本的な場合、システムは、関心のあるエンティティのリストを反復処理し、各エンティティに対してアクションを実行するだけです。

  • MessageChannelクラスオブジェクトのすべてのゲームシステムで共有されています。各システムは、特定のタイプのメッセージをサブスクライブしてリッスンし、チャネルを使用して他のシステムにメッセージをブロードキャストすることもできます

システムメッセージ処理の最初のバリアントは、次のようなものでした。

  1. 各ゲームシステムで順次更新を実行する
  2. システムがコンポーネントに対して何かを実行し、そのアクションが他のシステムにとって重要である場合、システムは適切なメッセージを送信します(たとえば、システム呼び出し

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    エンティティが移動されるたびに)

  3. 特定のメッセージをサブスクライブした各システムは、そのメッセージ処理メソッドを取得します

  4. システムがイベントを処理しており、イベント処理ロジックで別のメッセージをブロードキャストする必要がある場合、メッセージはすぐにブロードキャストされ、メッセージ処理メソッドの別のチェーンが呼び出されます

このバリアントは、衝突検出システムの最適化を開始するまでは問題ありませんでした(エンティティの数が増えると、本当に遅くなりました)。最初は、単純なブルートフォースアルゴリズムを使用して各エンティティペアを繰り返します。次に、特定のセルの領域内にあるエンティティを格納するセルのグリッドを持つ「空間インデックス」を追加しました。これにより、隣接セルのエンティティのみをチェックできます。

エンティティが移動するたびに、衝突システムはエンティティが新しい位置にあるものと衝突しているかどうかを確認します。そうである場合、衝突が検出されます。衝突するエンティティが両方とも「物理オブジェクト」である場合(両方にRigidBodyコンポーネントがあり、同じスペースを占有しないように互いを押しのけようとしている場合)、専用の剛体分離システムがエンティティを移動するように移動システムに要求しますそれらを分離する特定の位置。これにより、移動システムは変更されたエンティティの位置を通知するメッセージを送信します。衝突検出システムは、空間インデックスを更新する必要があるため、反応するように設計されています。

場合によっては、セルの内容(C#のEntityオブジェクトの汎用リスト)が繰り返し処理されている間に変更され、イテレーターによって例外がスローされるため、問題が発生します。

だから... 衝突をチェックしている間に衝突システムが中断されるのを防ぐにはどうすればよいですか?

もちろん、セルの内容が正しく繰り返されることを保証する「巧妙な」/「トリッキーな」ロジックを追加することもできますが、問題は衝突システム自体にあるのではないと思います(他のシステムにも同様の問題がありました)メッセージはシステムからシステムへ移動するときに処理されます。私が必要とするのは、特定のイベント処理メソッドが中断することなく仕事を確実に行うための何らかの方法です。

私が試したもの:

  • 着信メッセージキュー。あるシステムがメッセージをブロードキャストするたびに、そのメッセージは関心のあるシステムのメッセージキューに追加されます。これらのメッセージは、システム更新が各フレームで呼び出されると処理されます。問題:システムAがシステムBのキューにメッセージを追加する場合、システムBがシステムAよりも後(同じゲームフレーム内)に更新される場合、それはうまく機能します。それ以外の場合、メッセージが次のゲームフレームを処理します(一部のシステムでは望ましくありません)
  • 発信メッセージキュー。システムがイベントを処理している間、システムがブロードキャストするメッセージは送信メッセージキューに追加されます。メッセージは、システムの更新が処理されるのを待つ必要はありません。最初のメッセージハンドラが処理を完了した後、「すぐに」処理されます。メッセージの処理によって他のメッセージがブロードキャストされる場合、それらも発信キューに追加されるため、すべてのメッセージが同じフレームで処理されます。問題:エンティティライフタイムシステム(システムでエンティティライフタイム管理を実装)がエンティティを作成した場合、いくつかのシステムAとBに通知します。システムAがメッセージを処理している間、メッセージチェーンが発生し、最終的に作成されたエンティティが破棄されます(たとえば、弾丸エンティティが障害物と衝突する場所で作成され、弾丸が自己破壊します)。メッセージチェーンが解決されている間、システムBはエンティティ作成メッセージを取得しません。したがって、システムBがエンティティ破壊メッセージにも関心がある場合、システムBはそれを取得し、「チェーン」の解決が完了した後にのみ、最初のエンティティ作成メッセージを取得します。これにより、破棄メッセージは無視され、作成メッセージは「受け入れられます」、

編集-質問への回答、コメント:

  • 衝突システムがセルの内容を繰り返し処理している間、誰がセルの内容を変更しますか?

衝突システムが一部のエンティティとその近隣で衝突チェックを行っている間、衝突が検出される可能性があり、エンティティシステムは他のシステムがすぐに反応するメッセージを送信します。メッセージに対する反応により、他のメッセージが作成され、すぐに処理される場合があります。そのため、他のシステムは、以前の衝突チェックがまだ終了していない場合でも、衝突システムがすぐに処理する必要があるメッセージを作成する場合があります(たとえば、衝突システムが空間インデックスを更新する必要があるため、エンティティを移動します)。

  • グローバルな送信メッセージキューを使用できませんか?

最近、単一のグローバルキューを試しました。新しい問題を引き起こします。問題:タンクエンティティを壁エンティティに移動します(タンクはキーボードで制御されます)。それからタンクの方向を変えることにしました。タンクと壁を各フレームに分離するために、CollidingRigidBodySeparationSystemはタンクを壁から可能な限り最小の距離だけ移動します。分離方向は、戦車の移動方向の反対方向でなければなりません(ゲームの描画が開始されると、戦車は壁に移動したことがないように見えるはずです)。しかし、方向はNEW方向とは逆になり、タンクを壁の最初とは異なる側に移動します。問題が発生する理由:これがメッセージの処理方法です(簡略化されたコード):

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

コードは次のように流れます(最初のゲームフレームではないと仮定しましょう)。

  1. システムは TimePassedMessageの処理を開始します
  2. InputHandingSystemはキーの押下をエンティティアクションに変換します(この場合、左矢印はMoveWestアクションに変わります)。エンティティアクションはActionExecutorコンポーネントに保存されます
  3. ActionExecutionSystemは、エンティティアクションに対応して、MovementDirectionChangeRequestedMessageをメッセージキューの最後に追加します
  4. MovementSystemは、Velocityコンポーネントデータに基づいてエンティティの位置を移動し、PositionChangedMessageメッセージをキューの最後に追加します。移動は、前のフレームの移動方向/速度を使用して行われます(北に向かってみましょう)
  5. システムは TimePassedMessageの処理を停止します
  6. システムは MovementDirectionChangeRequestedMessageの処理を開始します
  7. MovementSystemは、要求に応じてエンティティの速度/移動方向を変更します
  8. システムは MovementDirectionChangeRequestedMessageの処理を停止します
  9. システムは PositionChangedMessageの処理を開始します
  10. CollisionDetectionSystemは、エンティティが移動したために別のエンティティにぶつかったことを検出します(タンクが壁の内側に入りました)。CollisionOccuredMessageをキューに追加します
  11. システムは PositionChangedMessageの処理を停止します
  12. システムは CollisionOccuredMessageの処理を開始します
  13. CollidingRigidBodySeparationSystemは、タンクと壁を分離することで衝突に反応します。壁は静止しているため、タンクのみが移動します。戦車の移動方向は、戦車がどこから来たかを示す指標として使用されます。反対方向にオフセットされています

BUG:戦車がこのフレームを移動したとき、前のフレームからの移動方向を使用して移動しましたが、分離されたときは、このフレームからの移動方向が使用されていました。それはそれがどのように機能すべきかではありません!

このバグを防ぐには、古い移動方向をどこかに保存する必要があります。この特定のバグを修正するためだけにコンポーネントに追加することもできますが、このケースはメッセージを処理する根本的に間違った方法を示していませんか?分離システムが使用する移動方向を考慮する必要があるのはなぜですか?この問題をエレガントに解決するにはどうすればよいですか?

  • gamadu.com/artemisを読んで、Aspectsで何をしたかを確認することをお勧めします。

実際、私はかなり長い間、アルテミスに精通しています。ソースコードを調べたり、フォーラムを読んだりします。しかし、「アスペクト」が言及されているのはごく少数の場所でしか見ていません。理解できる限り、それらは基本的に「システム」を意味します。しかし、Artemisが私の問題のいくつかをどのように踏んでいるかはわかりません。メッセージも使用しません。

  • 参照:「エンティティ通信:メッセージキューvsパブリッシュ/サブスクライブvsシグナル/スロット」

エンティティシステムに関するgamedev.stackexchangeの質問はすべて読んでいます。これは私が直面している問題を議論していないようです。何か不足していますか?

  • 2つのケースを別々に処理します。グリッドの更新は衝突システムの一部であるため、移動メッセージに依存する必要はありません。

どういう意味かわかりません。CollisionDetectionSystemの古い実装では、更新時に衝突をチェックするだけでした(TimePassedMessageが処理されたとき)が、パフォーマンスのためにチェックを最小限に抑える必要がありました。そこで、エンティティが移動したときに衝突チェックに切り替えました(ゲーム内のほとんどのエンティティは静的です)。


はっきりしないことがあります。衝突システムがセルの内容を繰り返し処理している間、誰がセルの内容を変更しますか?
ポールマンタ

グローバルな送信メッセージキューを使用できませんか?したがって、システムが完了するたびにそこにあるすべてのメッセージが送信されます。これにはシステムの自己破壊も含まれます。
ロイT.

この複雑なデザインを維持したい場合は、@ RoyTに従う必要があります。のアドバイス、それはあなたのシーケンスの問題を処理する唯一の方法です(複雑な時間ベースのメッセージングなし)。gamadu.com/artemisを読んで、Aspectsで何をしたかを確認することをお勧めします。
パトリックヒューズ


2
CTPをダウンロードしてコードをコンパイルし、ILSpyを使用してC#に結果をリバースエンジニアリングすることで、Axumがどのようにそれを実現したかを学ぶことができます。メッセージパッシングは、アクターモデル言語の重要な機能であり、Microsoftが彼らが何をしているかを知っていると確信しています。
ジョナサンディキンソン

回答:


12

おそらく、God / Blobオブジェクトのアンチパターンについて聞いたことがあるでしょう。まああなたの問題は、神/ブロブのループです。メッセージパッシングシステムをいじると、せいぜいBand-Aidソリューションが提供され、最悪の場合は完全に時間の無駄になります。実際、あなたの問題はゲーム開発とはまったく関係ありません。コレクションを何度か繰り返し処理しながらコレクションを変更しようとすると、解決策は常に同じです:サブディバイド、サブディバイド、サブディバイド。

私はあなたの質問の言い回しを理解しているので、衝突システムを更新する方法は現在、次のように広く見えます。

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

このように書かれているので、ループには1つの責任があるはずなのに、ループには3つの責任があることがわかります。問題を解決するには、現在のループを3つの異なるアルゴリズムパスを表す3つの独立したループに分割します。

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

元のループを3つのサブループに分割することにより、現在反復しているコレクションを変更しようとすることはなくなります。また、元のループよりも多くの作業を行っているわけではないことに注意してください。実際、同じ操作を何回も連続して実行することで、ある程度のキャッシュが得られる可能性があります。

さらなる利点もあります。これは、コードに並列処理を導入できるようになったことです。結合ループアプローチは本質的にシリアルです(基本的に、同時変更の例外が伝えていることです!)、各ループの反復は潜在的にコリジョンワールドの読み取りと書き込みの両方を行うためです。ただし、上記の3つのサブループは、すべて読み取りまたは書き込みのいずれかであり、両方ではありません。少なくとも、起こりうるすべての衝突をチェックする最初のパスは恥ずかしいほど平行になりました。コードの記述方法によっては、2番目と3番目のパスも同じようになります。


私はこれに完全に同意します。私は自分のゲームでこの非常によく似たアプローチを使用していますが、これは長期的には報われると信じています。これが衝突システム(またはマネージャー)の動作方法です(メッセージングシステムをまったく持たないことは実際に考えられます)。
エミリアーノ

11

コンポーネントベースのエンティティシステムでメッセージ処理を適切に実装する方法は?

同期と非同期の2種類のメッセージが必要だと思います。同期メッセージはすぐに処理され、非同期メッセージは同じスタックフレームでは処理されません(ただし、同じゲームフレームで処理される場合があります)。通常、「メッセージクラスごと」に基づいて決定されます。たとえば、「すべてのEnemyDiedメッセージは非同期です」。

これらの方法のいずれかを使用すると、いくつかのイベントがはるか簡単に処理されます。たとえば、私の経験では、ObjectGetsDeletedNow-イベントは、ObjectWillBeDeletedAtEndOfFrameよりもはるかにセクシーではなく、コールバックの実装がはるかに困難です。繰り返しになりますが、「veto」のようなメッセージハンドラー(シールド効果がDamageEventを変更するなど、実行中に特定のアクションをキャンセルまたは変更できるコード)は、非同期環境では簡単ではありませんが、同期呼び出し。

非同期は、場合によってはより効率的な場合があります(たとえば、オブジェクトが後で削除されたときに一部のイベントハンドラーをスキップできます)。特にイベントのパラメーターを計算するのにコストがかかる場合は、同期がより効率的である場合があり、既に計算された値の代わりに特定のパラメーターを取得するためにコールバック関数を渡す方が好きです(とにかくこの特定のパラメーターに誰も興味がない場合)。

同期のみのメッセージシステムに関する別の一般的な問題については、既に述べました。同期メッセージシステムに関する私の経験では、一般的なエラーと悲しみのほとんどは、これらのリストを繰り返し処理する際のリストの変更です。

考えてみてください:同期(何らかのアクションの後遺症をすべて即座に処理する)とメッセージシステム(受信者を送信者から切り離すことで、送信者がアクションに誰が反応しているかわからない)の性質上、簡単にできないそのようなループを見つけます。私が言っているのは、この種の自己修正の繰り返しをたくさん処理する準備をしておくことです。その一種の「設計による」。;-)

衝突のチェック中に衝突システムが中断されるのを防ぐにはどうすればよいですか?

衝突検出の特定の問題については、衝突イベントを非同期にすることで十分である可能性があるため、衝突マネージャーが終了し、その後1つのバッチとして(またはフレーム内の後で)実​​行されるまでキューに入れられます。これがソリューションの「着信キュー」です。

問題:システムAがシステムBのキューにメッセージを追加する場合、システムBがシステムAよりも後(同じゲームフレーム内)に更新される場合、うまく機能します。それ以外の場合、メッセージが次のゲームフレームを処理します(一部のシステムでは望ましくありません)

簡単:

while(!queue.empty()){queue.pop()。handle(); }

メッセージがなくなるまで、キューを繰り返し実行するだけです。(「エンドレスループ」を叫んでいる場合、次のフレームに遅延する場合、「メッセージスパム」としてこの問題が発生する可能性が最も高いことを忘れないでください。あなたがそれのように感じるなら;))


非同期メッセージが正確に「いつ」処理されるかについては話していないことに注意してください。私の意見では、衝突検出モジュールが終了後にメッセージをフラッシュできるようにすることはまったく問題ありません。あなたはまた、または「ただ反復しながら、それを変更することができるような方法で反復を実装する」のいくつかの気の利いたやり方「ループの最後まで遅延同期メッセージ」と考えることができ
イミ

5

実際にECSのデータ指向設計の性質を利用しようとしている場合は、これを行う最もDODの方法を検討することをお勧めします。

BitSquidブログ、特にイベントに関する部分をご覧ください。ECSとうまく噛み合うシステムが提示されます。ECSのシステムがコンポーネントごとにあるのと同じように、すべてのイベントをきれいなメッセージタイプごとのキューにバッファします。後で更新されたシステムは、特定のメッセージタイプのキューを効率的に反復処理してそれらを処理できます。または、単に無視します。どっちでも。

たとえば、CollisionSystemは衝突イベントでいっぱいのバッファーを生成します。衝突後に実行される他のシステムは、リストを反復処理し、必要に応じてそれらを処理できます。

ECS設計のデータ指向の並列性を維持し、メッセージ登録などの複雑さを排除します。特定のタイプのイベントを実際に処理するシステムだけが、そのタイプのキューに対して反復処理を行い、メッセージキューに対して単純なシングルパス反復処理を実行するだけで、可能な限り効率的です。

コンポーネントを各システムで一貫した順序で保持する場合(たとえば、すべてのコンポーネントをエンティティIDまたはそのようなもので並べる)、それらを反復処理し、対応するコンポーネントを検索するための最も効率的な順序でメッセージが生成されるという素晴らしい利点も得られます処理システム。つまり、エンティティ1、2、および3がある場合、メッセージはその順序で生成され、メッセージの処理中に実行されるコンポーネントルックアップは厳密にアドレスの昇順(最速)になります。


1
+1、しかし、このアプローチには欠点がないとは信じられません。これにより、システム間の相互依存関係をハードコードする必要はありませんか?それとも、これらの相互依存関係は、何らかの方法でハードコーディングされることを意図していますか?
パトリックチャチャルスキ

2
@Daedalus:ゲームロジックが正しいロジックを実行するために物理学の更新を必要とする場合、どのようにその依存関係を持たないのですか?pubsubモデルであっても、他のシステムによってのみ生成されるそのようなメッセージタイプを明示的にサブスクライブする必要があります。依存関係を回避するのは難しく、ほとんどの場合、適切なレイヤーを見つけるだけです。グラフィックスと物理例えば、独立している、しかし性を保証は物理シミュレーションの更新が等グラフィック、に反映されて補間することを、より高いレベルの接着剤層が存在することになる
ショーンMiddleditch

これは受け入れられた答えであるはずです。これを行う簡単な方法は、衝突の発生後に物事を行うことに関心のあるすべてのシステムによって処理されるCollisionResolvableなどの新しいタイプのコンポーネントを作成することです。これはドレイクの命題にうまく適合しますが、細分化ループごとにシステムがあります。
user8363
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.