ターンベースのゲームでエンティティコンポーネントのゲーム状態を進行させる方法は?


9

これまでのところ、私が使用したエンティティコンポーネントシステムは、ほとんどがJavaのアルテミスのように機能しています。

  • コンポーネント内のすべてのデータ
  • ステートレスな独立したシステム(少なくとも初期化時に入力を必要としない程度)は、特定のシステムが関心のあるコンポーネントのみを含む各エンティティを反復します。
  • すべてのシステムはエンティティを1ティック処理し、その後すべてをやり直します。

今、私はこれをターンベースのゲームに初めて適用しようとしています。ゲームを進める前に、相互に設定された順序で発生しなければならない大量のイベントと応答があります。例:

プレイヤーAは剣からダメージを受けます。これに応じて、Aの鎧はキックインし、受けるダメージを減らします。Aが弱まる結果、Aの移動速度も低下します。

  • 受けたダメージは相互作用全体を開始するものです
  • ダメージがプレイヤーに適用される前に、鎧を計算して、受けたダメージに適用する必要があります
  • 移動速度低下は、最終的なダメージ量に依存するため、実際にダメージが与えられるまでは適用できません。

イベントは他のイベントをトリガーすることもできます。鎧を使用して刀のダメージを軽減すると、刀が粉砕され(これはダメージの軽減が完了する前に行う必要があります)、次に、それに応じて追加のイベントが発生し、本質的にはイベントの再帰的な評価が発生します。

全体として、これはいくつかの問題につながるようです:

  1. 多くの無駄な処理サイクル:ほとんどのシステム(レンダリングのように常に実行されるもののために保存)は、「自分の番」が機能しない場合に実行する価値のあることは何もなく、ほとんどの時間はゲームの開始を待機しています。有効な作業状態。これにより、そのようなすべてのシステムに、ゲームに追加される状態が増えるにつれてサイズが大きくなり続けるチェックが散らばっています。
  2. システムがゲームに存在するエンティティを処理できるかどうかを確認するには、他の無関係なエンティティ/システムの状態を監視する何らかの方法が必要です(ダメージの処理を担当するシステムは、鎧が適用されているかどうかを知る必要があります)。これは、複数の責任でシステムを混乱させるか、または各処理サイクルの後にエンティティコレクションをスキャンし、何かをする準備ができていることを通知することによって一連のリスナーと通信する以外に目的のない追加のシステムの必要性を生み出します。

上記の2つのポイントは、システムが同じエンティティセットで動作し、コンポーネントのフラグを使用して状態が変化することを前提としています。

それを解決する別の方法は、単一のシステムがゲームの状態を進行させるために動作する結果として、コンポーネントを追加/削除する(または完全に新しいエンティティを作成する)ことです。これは、システムが実際に一致するエンティティを持っている場合はいつでも、処理が許可されていることを認識していることを意味します。

ただし、これにより、システムは後続のシステムのトリガーを担当し、単一のシステムの相互作用の結果としてバグが表示されないため、プログラムの動作を推論することが難しくなります。新しいシステムを追加することは、他のシステムにどのように影響するかを正確に把握しないと実装できないため(そして以前のシステムを変更して、新しいシステムが関心のある状態をトリガーする必要がある場合があるため)、さらに難しくなります。単一のタスクで。

これは私と一緒に暮らす必要があるでしょうか?私が見たすべてのECSの例はすべてリアルタイムであり、そのような場合にこのゲームごとの1回の繰り返しがどのように機能するかを確認するのは非常に簡単です。そして、私はまだレンダリングにそれを必要とします、何かが起こるたびにそれ自身のほとんどの側面を一時停止するシステムには本当に不適当に思えます。

これに適したゲームの状態を進めるためのデザインパターンはありますか?それとも、すべてのロジックをループの外に移動し、代わりに必要なときにのみトリガーする必要がありますか?


イベントの発生をポーリングする必要はありません。イベントは、発生したときにのみ発生します。Artemisはシステムが互いに通信することを許可していませんか?
Sidar 2013年

それは可能ですが、メソッドを使用してそれらを結合することによってのみ。
Aeris130 2013年

回答:


3

ここでのアドバイスは、コンポーネントシステムを使用したRPGプロジェクトでの過去の経験に基づいています。スパゲッティコードだったので、ゲームサイドコードでの作業は嫌いだったと言います。だから私はここで答えの多くを提供するのではなく、単に視点を提供します:

プレイヤーへの剣のダメージを処理するためにあなたが説明するロジック...それは1つのシステムがそのすべてを担当する必要があるようです。

どこかに、HandleWeaponHit()関数があります。プレーヤーエンティティのArmorComponentにアクセスして、関連するアーマーを取得します。攻撃している武器エンティティのWeaponComponentにアクセスして、おそらく武器を粉砕します。最終的なダメージを計算した後、プレイヤーはMovementComponentに触れて速度を下げます。

無駄な処理サイクルについて... HandleWeaponHit()は(剣のヒットを検出すると)必要なときにのみトリガーされます。

多分私がしようとしている点は、ブレークポイントを配置してヒットできるコード内の場所が確実に必要であり、剣のヒットが発生したときに実行されるはずのすべてのロジックをステップスルーすることです。つまり、ロジックは、複数のシステムのtick()関数全体に散在するべきではありません。


このようにすると、より多くの動作が追加されるので、hit()関数は風船になります。剣が視線内のターゲット(任意のターゲット)に当たるたびに笑いながら倒れる敵がいるとしましょう。HandleWeaponHitはそれを実際にトリガーする必要がありますか?
Aeris130 2013年

1
あなたはしっかりと絡み合った戦闘シーケンスを持っているので、はい、ヒットは効果を誘発する責任があります。すべてを小さなシステムに分解する必要はありません。この1つのシステムがこれを処理できるようにします。これは、実際に「戦闘システム」であり、処理するためです。戦闘...
Patrick Hughes

3

それは1年前の質問ですが、ECSを勉強している間、私は自分の自家製のゲームで同じ問題に直面しています。うまくいけば、それは議論または少なくともいくつかのコメントで終わるでしょう。

ECSの概念に違反しているかどうかはわかりませんが、次の場合はどうなりますか。

  • EventBusを追加して、システムがイベントオブジェクトを発行/サブスクライブできるようにします(実際には純粋なデータですが、私が推測するコンポーネントではありません)。
  • 中間状態ごとにコンポーネントを作成する

例:

  • UserInputSystemは、[DamageDealerEntity、DamageReceiverEntity、Skill / Weapon used info]で攻撃イベントを発生させます
  • CombatSystemがサブスクライブし、DamageReceiverの回避確率を計算します。回避が失敗すると、同じパラメーターでダメージイベントがトリガーされます
  • DamageSystemはそのようなイベントにサブスクライブされているため、トリガーされます
  • DamageSystemは、Strength、BaseWeaponダメージ、そのタイプなどを使用し、[DamageDealerEntity、FinalOutgoingDamage、DamageType]を使用して新しいIncomingDamageComponent に書き込み、それをダメージレシーバーのエンティティ/エンティティにアタッチします
  • DamageSystemはOutgoingDamageCalculatedを起動します
  • ArmorSystemはそれによってトリガーされ、レシーバーエンティティを取得するか、エンティティ内のこのIncomingDamageアスペクトで検索してIncomingDamageComponent(最後の1つはおそらくスプレッドのある複数の攻撃に適しています)を取得し、それに適用される鎧とダメージを計算します。必要に応じて、刀破砕のイベントをトリガーします
  • ArmorSystemsは、各エンティティのIncomingDamageComponentを削除し、それをDamageReceivedComponentに置き換えます。これは、HPに影響を与える最終的な計算された数値と、傷からの速度低下に影響します。
  • ArmorSystemsはIncomingDamageCalculatedイベントを送信します
  • 速度システムがサブスクライブされ、速度を再計算します
  • HealthSystemが購読され、実際のHPが減少します
  • どういうわけかクリーンアップ

長所:

  • システムは相互にトリガーし、複雑なチェーンイベントの中間データを提供します
  • EventBusによる分離

短所:

  • 私は物事を渡す2つの方法を混合しているように感じます。イベントパラメータと一時コンポーネントです。弱いところかもしれません。理論的には、物事を均一に保つために、システムがエンティティのコンポーネント内の暗黙のパラメータをアスペクトごとに見つけることができるように、データのない列挙イベントのみを発生させることができます。
  • 潜在的に関心のあるすべてのSystemsHaveがIncomingDamageCalculatedを処理したかどうかを確認する方法がわからないため、クリーンアップして次のターンを実行できます。たぶん、CombatSystemで何らかのチェックを返します...

2

ヤコブレフのように、私が最終的に解決した解決策を投稿しました。

基本的に、そのロジックを順番にたどるのは非常に直感的であることがわかったため、イベントシステムを使用することになりました。システムは、ターンベースのロジック(プレイヤー、モンスター、およびそれらが操作できるすべてのもの)に準拠したゲーム内ユニットを担当し、レンダリングや入力ポーリングなどのリアルタイムタスクは別の場所に配置されていました。

システムは、イベントとエンティティを入力として受け取るonEventメソッドを実装し、エンティティがイベントを受信したことを通知します。すべてのシステムは、特定のコンポーネントセットを持つイベントとエンティティにもサブスクライブします。システムで使用できる唯一の対話点は、エンティティーにイベントを送信したり、特定のエンティティーからコンポーネントを取得したりするために使用されるエンティティーマネージャーシングルトンです。

エンティティー・マネージャーは、送信先のエンティティーと結合されたイベントを受信すると、そのイベントをキューの最後に配置します。キューにイベントがある間、一番前のイベントが取得され、イベントにサブスクライブしていて、イベントを受信するエンティティのコンポーネントセットに関心があるすべてのシステムに送信されます。これらのシステムは、エンティティのコンポーネントを処理したり、追加のイベントをマネージャーに送信したりできます。

例:プレイヤーがダメージを受けるため、プレイヤーエンティティにダメージイベントが送信されます。DamageSystemは、ヘルスコンポーネントを持つ任意のエンティティに送信されるダメージイベントをサブスクライブし、エンティティコンポーネントのヘルスをイベントで指定された量だけ減らすonEvent(entity、event)メソッドを持っています。

これにより、鎧コンポーネントを持つエンティティに送信される損傷イベントにサブスクライブする鎧システムを簡単に挿入できます。そのonEventメソッドは、コンポーネントの鎧の量によってイベントのダメージを減らします。これは、システムがイベントを受け取る順序を指定すると、ゲームロジックに影響を与えることを意味します。これは、鎧システムが機能するためには、ダメージシステムの前にダメージイベントを処理する必要があるためです。

ただし、システムが受信エンティティの外に出なければならない場合もあります。Eric Undersanderへの私の応答を続けるには、ゲームマップにアクセスし、損傷を受けているエンティティのx空間内でFallsDownLaughingComponentを持つエンティティを探し、FallDownLaughingEventをそれらに送信するシステムを追加するのは簡単です。このシステムは、ダメージシステムの後にイベントを受け取るようにスケジュールする必要があります。その時点でダメージイベントがキャンセルされなかった場合、ダメージは処理されました。

発生した問題の1つは、一部の応答が追加の応答を生成する可能性がある場合に、応答イベントが送信順に処理されるようにする方法です。例:

プレーヤーが移動し、移動イベントがプレーヤーエンティティに送信され、移動システムによってピックアップされます。

待機中:移動

移動が許可されている場合、システムはプレイヤーの位置を調整します。そうでない場合(プレーヤーが障害物に移動しようとした場合)、イベントはキャンセルされたものとしてマークされ、エンティティマネージャーは後続のシステムに送信する代わりにイベントを破棄します。イベントに関心のあるシステムのリストの最後にあるのがTurnFinishedSystemです。これは、プレーヤーが自分の番をキャラクターの移動に費やしたこと、および自分の番が終わったことを確認します。これにより、TurnOverイベントがプレーヤーエンティティに送信され、キューに配置されます。

待機中:ターンオーバー

プレーヤーがトラップを踏んだため、ダメージが発生したとしましょう。TrapSystemはTurnFinishedSystemの前に移動メッセージを取得するため、ダメージイベントが最初に送信されます。キューは次のようになります。

待機中:ダメージ、ターンオーバー

これまでのところ問題ありません。ダメージイベントが処理され、ターンが終了します。ただし、損傷への対応として追加のイベントが送信された場合はどうなりますか?イベントキューは次のようになります。

待機中:Damage、TurnOver、ResponseToDamage

つまり、ダメージへの対応が処理される前に、ターンは終了します。

これを解決するために、私はイベントを送信する2つの方法を使用することになりました:send(event、entity)およびrespond(event、eventToRespondTo、entity)。

すべてのイベントは、以前のイベントの記録を応答チェーンに保持し、respond()メソッドが使用されると、応答されるイベント(およびその応答チェーン内のすべてのイベント)は、使用されるイベントのチェーンの先頭で終了します。と応答します。初期移動イベントにはそのようなイベントはありません。後続のダメージ応答のリストには、移動イベントがあります。

その上で、可変長配列は複数のイベントキューを格納するために使用されます。イベントがマネージャーによって受信されるたびに、イベントは、応答チェーン内のイベントの量と一致する配列内のインデックスでキューに追加されます。したがって、最初の移動イベントは[0]でキューに追加され、ダメージとターンオーバーイベントは、両方が移動への応答として送信されたため、[1]で別のキューに追加されます。

損傷イベントへの応答が送信されると、それらのイベントには、損傷イベント自体と移動の両方が含まれ、それらをインデックス[2]のキューに入れます。インデックス[n]のキューにイベントがある限り、それらのイベントは[n-1]に進む前に処理されます。これにより、次の処理順序が得られます。

移動->ダメージ[1]-> ResponseToDamage [2]-> [2]が空->ターンオーバー[1]-> [1]が空-> [0]が空

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