タイトルは意図的に双曲的であり、それはパターンに対する私の未経験かもしれませんが、ここに私の推論があります:
エンティティを実装する「通常の」またはほぼ間違いなく簡単な方法は、エンティティをオブジェクトとして実装し、一般的な動作をサブクラス化することです。これは、「EvilTree
はTree
またはEnemy
のサブクラスですか?」という古典的な問題につながります。多重継承を許可すると、ひし形の問題が発生します。代わりに、Tree
とEnemy
を組み合わせた機能をプルアップして、階層をさらに上位にしてGodクラスにするか、Tree
クラスとEntity
クラスの動作を意図的に省略して(極端な場合はインターフェースにする)、EvilTree
がそれを実装できるようにすることができますそれ自体-SomewhatEvilTree
がある場合、コードの重複につながります。
エンティティコンポーネントシステムは、Tree
オブジェクトとEnemy
オブジェクトを別のコンポーネント(Position
、Health
、AI
など)に分割することでこの問題を解決し、AISystem
などのシステムを実装して、AIの判断に従ってエンティティの位置を変更します。これまでのところ良好ですが、EvilTree
がパワーアップを取得してダメージを与えることができるとしたらどうでしょうか?最初に、CollisionSystem
とDamageSystem
が必要です(おそらく既に持っています)。 CollisionSystem
は、DamageSystem
と通信する必要があります。2つのものが衝突するたびに、CollisionSystem
がDamageSystem
にメッセージを送信するため、ヘルスを減算できます。ダメージはパワーアップの影響も受けるため、どこかに保存する必要があります。エンティティにアタッチする新しいPowerupComponent
を作成しますか?しかし、DamageSystem
は、何も知らないことを知っている必要があります-結局のところ、パワーアップを取得できないダメージを与えるものもあります(例:Spike
)。 PowerupSystem
がStatComponent
を変更することを許可しますか。これは this answer と同様のダメージ計算にも使用されますか?しかし、現在は2つのシステムが同じデータにアクセスしています。ゲームがより複雑になると、コンポーネントが多くのシステム間で共有される無形の依存関係グラフになります。この時点で、グローバル静的変数を使用して、すべてのボイラープレートを取り除くことができます。
これを解決する効果的な方法はありますか?私が持っていた1つのアイデアは、コンポーネントに特定の機能を持たせることでした。 StatComponent
attack()
を指定します。これはデフォルトで整数を返すだけですが、電源投入時に構成できます。
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
これは、attack
を複数のシステムからアクセスされるコンポーネントに保存する必要があるという問題を解決しませんが、少なくとも、十分にサポートする言語があれば、関数を適切に入力できます。
// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup
// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage
// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity
このようにして、少なくともシステムによって追加されたさまざまな機能の正しい順序を保証します。どちらにしても、ここでは関数型反応プログラミングに急速に近づいているようです。そのため、最初からそれを使用するべきではなかったかどうかを自問します(FRPを調べただけなので、ここでは間違っているかもしれません)。 ECSは複雑なクラス階層よりも改善されていると思いますが、それが理想的だとは思いません。
これを回避する方法はありますか? ECSをより明確に分離するために欠けている機能/パターンはありますか? FRPはこの問題により厳密に適していますか?これらの問題は、私がプログラミングしようとしていることの固有の複雑さに起因しているのでしょうか。つまり、FRPにも同様の問題がありますか?
ECSはデータの非表示を完全に破壊します。これはパターンのトレードオフです。
ECSは、デカップリング時にexcellentです。優れたECSを使用すると、移動システムは、存在するエンティティタイプや他のシステムがこれらのコンポーネントにアクセスすることを気にする必要なく、速度と位置コンポーネントを持つエンティティで動作することを宣言できます。これは、ゲームオブジェクトに特定のインターフェイスを実装させることとデカップリングパワーにおいて少なくとも同等です。
同じコンポーネントにアクセスする2つのシステムは機能であり、問題ではありません。それは完全に期待されており、システムを結合することはありません。システムに暗黙的な依存関係グラフがあることは事実ですが、それらの依存関係はモデル化された世界に固有のものです。ダメージシステムがパワーアップシステムに暗黙的に依存するべきではないと言うことは、パワーアップがダメージに影響しないと主張することであり、それはおそらく間違っています。ただし、依存関係は存在しますが、システムは結合ではありません。通信はstatコンポーネントを介して行われ、完全に暗黙的であるため、ダメージシステムに影響を与えることなく、ゲームからパワーアップシステムを削除できます。
これらの依存関係の解決とシステムの注文は、DIシステムでの依存関係の解決が機能するのと同様に、単一の中央ロケーションで実行できます。はい、複雑なゲームはシステムの複雑なグラフになりますが、この複雑さは本質的にあり、少なくとも含まれています。
システムが複数のコンポーネントにアクセスする必要があるという事実を回避する方法はほとんどありません。 VelocitySystemのようなものが機能するためには、おそらくVelocityComponentおよびPositionComponentにアクセスする必要があります。一方、RenderingSystemもこのデータにアクセスする必要があります。何をする場合でも、ある時点で、レンダリングシステムはオブジェクトのレンダリング先を認識し、VelocitySystemはオブジェクトの移動先を認識する必要があります。
これに必要なのは、依存関係のexplicitnessです。各システムは、読み取るデータと書き込むデータについてexplicitである必要があります。システムが特定のコンポーネントをフェッチしたい場合、これを実行できる必要があります明示的にのみ。最も単純な形式では、必要な各タイプのコンポーネント(RenderSystemはRenderComponentsとPositionComponentsを必要とするなど)を引数として持ち、変更されたもの(たとえば、RenderComponentsのみ)を返します。
このようにして、システムによって追加されたさまざまな機能の正しい順序を少なくとも保証します
そんなデザインでご注文頂けます。 ECSの場合、システムが順序やそのようなものから独立していなければならないということはありません。
FRPはこの問題により厳密に適していますか?これらの問題は、私がプログラミングしようとしていることの固有の複雑さに起因しているのでしょうか。つまり、FRPにも同様の問題がありますか?
このエンティティコンポーネントシステムの設計とFRPの使用は、相互に排他的ではありません。実際、システムは状態を持たず、単にデータ変換(コンポーネント)を実行するだけであると見なすことができます。
FRPは、一部の操作を実行するために必要な情報を使用する必要があるという問題を解決しません。