Apple AppStoreやGoogle Playアプリストアなどのアプリケーション(ネイティブまたはWeb)を構築するとき、Model-View-Controllerアーキテクチャを使用することは非常に一般的であることを知っています。
ただし、ゲームエンジンで一般的なComponent-Entity-Systemアーキテクチャを使用してアプリケーションを作成することは合理的ですか?
Apple AppStoreやGoogle Playアプリストアなどのアプリケーション(ネイティブまたはWeb)を構築するとき、Model-View-Controllerアーキテクチャを使用することは非常に一般的であることを知っています。
ただし、ゲームエンジンで一般的なComponent-Entity-Systemアーキテクチャを使用してアプリケーションを作成することは合理的ですか?
回答:
ただし、ゲームエンジンで一般的なComponent-Entity-Systemアーキテクチャを使用してアプリケーションを作成することは合理的ですか?
絶対に。私はビジュアルFXで仕事をしており、この分野のさまざまなシステム、そのアーキテクチャ(CAD / CAMを含む)、SDKに飢えている、そして無限に思えるアーキテクチャ上の決定の長所と短所を感じさせる論文を研究しました。最も微妙なものでさえ、常に微妙な影響を与えるとは限りません。
VFXは、レンダリングされた結果を表示するビューポートを持つ「シーン」という1つの中心的な概念があるという点で、ゲームにかなり似ています。また、物理コンテキストが発生している可能性のあるアニメーションコンテキスト、パーティクルエミッターがパーティクルをスポーンし、メッシュがアニメーション化およびレンダリングされる、モーションアニメーションなど、最終的にそれらをレンダリングするアニメーションコンテキストで絶えず回転している多くの中央ループ処理が行われる傾向があります最後にユーザーにすべて。
少なくとも非常に複雑なゲームエンジンに似たもう1つの概念は、デザイナーが独自の軽量プログラミング(スクリプトおよびノード)を行う能力など、シーンを柔軟に設計できる「デザイナー」アスペクトの必要性でした。
長年にわたって、ECSが最適であることがわかりました。もちろん、それは主観性と完全に離婚することは決してありませんが、私はそれが最も少ない問題を与えるように強く見えたと言うでしょう。それは、私たちが常に苦労していたより多くの大きな問題を解決しましたが、見返りにいくつかの新しいマイナーな問題だけを与えました。
従来のOOPアプローチは、実装要件ではなく設計要件をしっかりと把握している場合に非常に強力になります。よりフラットな複数のインターフェイスアプローチまたはよりネストされた階層型ABCアプローチのいずれを使用する場合でも、設計を強化し、変更をより簡単かつ安全にしながら、変更をより困難にする傾向があります。単一のバージョンを超える製品には常に不安定性が必要であるため、OOPアプローチでは、安定性(変更の難しさと変更理由の欠如)を設計レベルにゆがめ、不安定性(変更の容易さと変更理由)を偏らせる傾向があります実装レベルまで。
ただし、進化するユーザーエンド要件に対しては、設計と実装の両方を頻繁に変更する必要がある場合があります。植物と動物の両方である必要があり、構築した概念モデル全体を完全に無効にしなければならない類推的な生物に対するユーザー側の強いニーズのような奇妙なものを見つけるかもしれません。通常のオブジェクト指向のアプローチでは、ここであなたを保護することはできません。また、そのような予期せぬ概念を壊すような変更をさらに困難にすることがあります。パフォーマンスが非常に重要な領域が関係する場合、設計変更の理由はさらに増大します。
複数の詳細なインターフェースを組み合わせてオブジェクトの適合インターフェースを形成することは、クライアントコードの安定化には大いに役立ちますが、サブタイプの安定化には役立ちません。たとえば、システムの一部でのみ使用される1つのインターフェイスを、そのインターフェイスを実装する1000の異なるサブタイプで使用できます。その場合、複雑なサブタイプ(それらが果たすべき非常に多くの異なるインターフェースの責任を持っているため複雑)を維持することは、インターフェースを介してそれらを使用するコードではなく悪夢になります。OOPは複雑さをオブジェクトレベルに転送する傾向がありますが、ECSはそれをクライアント(「システム」)レベルに転送します。これは、システムが非常に少なく、適合「オブジェクト」(「エンティティ」)がたくさんある場合に理想的です。
クラスはそのデータをプライベートに所有するため、不変式をすべて独自に維持できます。それにもかかわらず、オブジェクトが相互作用する場合、実際には維持するのが依然として困難な「粗い」不変条件があります。複雑なシステム全体が有効な状態になるには、個々の不変式が適切に維持されている場合でも、オブジェクトの複雑なグラフを考慮する必要があります。従来のOOPスタイルのアプローチは、詳細な不変式の維持に役立ちますが、オブジェクトがシステムの小さなファセットに焦点を合わせている場合、実際には、広くて粗い不変式を維持することを困難にします。
そこで、こうした種類のレゴブロック構築ECSアプローチまたはバリアントが非常に役立ちます。また、システムの設計が通常のオブジェクトよりも粗い場合、システムの鳥瞰図でこれらの種類の粗い不変式を維持することが容易になります。小さなオブジェクトの相互作用の多くは、1キロメートルの紙をカバーする依存関係グラフを備えた小さなタスクに焦点を合わせた小さなオブジェクトではなく、1つの広いタスクに焦点を合わせた1つの大きなシステムに変わります。
それでも、ECSについて学ぶには、ゲーム業界で自分の分野の外を見る必要がありましたが、私は常にデータ指向の考え方の1つでした。また、おもしろいことに、私は自分でECSに向かって進んでおり、繰り返して、より良いデザインを考え出そうとしています。しかし、私はそれを最後までやり遂げず、非常に重要な詳細を見落としました。これは、「システム」部分の形式化と、生データに至るまでコンポーネントを押しつぶすことです。
ECSにどのように落ち着き、以前の設計の反復ですべての問題を解決したのかを説明します。ここでの答えが非常に強力な「はい」である理由、ECSはゲーム業界をはるかに超えて適用できる可能性があることを正確に強調するのに役立つと思います。
私がVFX業界で取り組んだ最初のアーキテクチャには、私が入社してから10年以上経った長い遺産がありました。それはブルートフォースの粗いCコーディングでした(私はCが好きなので、Cの傾斜ではありませんが、ここで使用されている方法は本当に粗野でした)。ミニチュアで単純化しすぎたスライスは、次のような依存関係に似ていました。
これは、システムのごく一部を非常に単純化した図です。ダイアグラム内のこれらの各クライアント(「レンダリング」、「物理」、「モーション」)は、次のようにタイプフィールドをチェックする「ジェネリック」オブジェクトを取得します。
void transform(struct Object* obj, const float mat[16])
{
switch (obj->type)
{
case camera:
// cast to camera and do something with camera fields
break;
case light:
// cast to light and do something with light fields
break;
...
}
}
もちろん、これよりも著しくく、より複雑なコードを使用します。多くの場合、追加の関数がこれらのスイッチケースから呼び出され、繰り返し再帰的にスイッチを実行します。この図とコードはほとんどECS-liteのように見えるかもしれませんが、強いエンティティ成分の区別(「ありませんでしたです、このオブジェクトはカメラ?」、 『このオブジェクトがないではない提供(運動を?』)、および『システム』の無い定式化は、ネストされた関数の束だけが場所を行き来し、責任を混乱させます)。その場合、ほとんどすべてが複雑でしたが、どの機能も災害が起こるのを待っている可能性がありました。
ここでのテスト手順では、他のタイプのアイテムとは別の種類のアイテムとは別のメッシュのようなものをチェックする必要がありました。ここでのコーディングのブルートフォースの性質(多くの場合、コピーと貼り付けが多い)そうでない場合、まったく同じロジックが、あるアイテムタイプから次のアイテムタイプに失敗する可能性が非常に高くなります。システムを拡張して新しいタイプのアイテムを処理しようとしても、既存のタイプのアイテムを処理するだけで苦労するのは非常に困難であったため、ユーザーエンドのニーズが強く表現されていたとしても、かなり絶望的でした。
いくつかの長所:
短所:
VFX業界のほとんどは、私が収集したものからこのスタイルのアーキテクチャを使用し、設計の決定に関するドキュメントを読み、ソフトウェア開発キットを一glしています。
ABIレベルのCOMではない場合があります(これらのアーキテクチャの一部は、同じコンパイラを使用して記述されたプラグインのみを持つことができます)が、コンポーネントがサポートするインターフェイスを確認するためにオブジェクトに対して行われるインターフェイスクエリと多くの類似した特性を共有します。
この種のアプローチでは、transform
上記の類推関数はこの形式に似たものになりました。
void transform(Object obj, const Matrix& mat)
{
// Wrapper that performs an interface query to see if the
// object implements the IMotion interface.
MotionRef motion(obj);
// If the object supported the IMotion interface:
if (motion.valid())
{
// Transform the item through the IMotion interface.
motion->transform(mat);
...
}
}
これは、古いコードベースの新しいチームが最終的にリファクタリングするために着手したアプローチです。また、柔軟性と保守性の点で元の製品よりも劇的に改善されましたが、まだ次のセクションでカバーするいくつかの問題がありました。
いくつかの長所:
短所:
IMotion
常にまったく同じ状態とまったく同じ実装を持ちます。これを軽減するために、同じインターフェイスに対して同じ方法で冗長に実装される傾向があるもののためにシステム全体でベースクラスとヘルパー機能を集中化することを開始しました。クライアントのコードは簡単でしたが、ボンネットの下は乱雑です。QueryInterface
ほとんどの場合、基本機能が中から上部のホットスポットとして表示され、場合によっては#1ホットスポットとしても表示されていました。それを軽減するために、コードベースの一部をレンダリングすることで、既にサポートされていることがわかっているオブジェクトのリストをキャッシュするなどのことを行います。IRenderable
、しかしそれは複雑さと保守コストを大幅に増大させました。同様に、これを測定することはより困難でしたが、すべてのインターフェイスが動的ディスパッチを必要とする以前に行っていたCスタイルのコーディングと比較して、明確なスローダウンに気付きました。分岐の予測ミスや最適化の障壁などは、コードの小さな側面以外では測定するのが困難ですが、ユーザーは一般に、ソフトウェアの以前のバージョンと新しいバージョンを並べて比較することにより、ユーザーインターフェイスの応答性や悪化することに気づいていましたアルゴリズムの複雑さは変わらず、定数のみが変更されたエリアに対応します。私たちが以前に気づいていた(または少なくとも私が)問題を引き起こしていたことの1つは、IMotion
100の異なるクラスによって実装されるが、まったく同じ実装と状態が関連付けられることでした。さらに、レンダリング、キーフレームモーション、物理学などの少数のシステムでのみ使用されます。
そのため、このような場合、インターフェースへのインターフェースを使用するシステム間の3対1の関係、およびインターフェースへのインターフェースを実装するサブタイプ間の100対1の関係があります。
複雑さとメンテナンスは、に依存する3つのクライアントシステムではなく、100のサブタイプの実装とメンテナンスに大きく偏りますIMotion
。これにより、メンテナンスの難しさはすべて、インターフェイスを使用する3つの場所ではなく、これらの100のサブタイプのメンテナンスにシフトしました。「間接的な遠心性カップリング」がほとんどまたはまったくない(直接的な依存性ではなく、インターフェースを介した間接的な)コード内の3つの場所を更新します。大したことはありません。 、かなり大した*。
* この意味で、実装の観点から「遠心性カップリング」の定義をねじ込むのは奇妙で間違っていることを認識しています。変更する必要があります。
だから私は一生懸命にプッシュしなければなりませんでしたが、もう少し実用的になり、「純粋なインターフェース」のアイデア全体を緩和することを提案しました。IMotion
豊富な種類の実装を使用するメリットがない限り、完全に抽象的でステートレスなものを作成しても意味がありません。私たちの場合、IMotion
さまざまな実装を行うことは、実際にはメンテナンスの悪夢になります。多様性は望まなかったからです。その代わりに、クライアントの要件の変化に対して本当に優れた単一のモーション実装を試みることを目指して反復し、多くの場合、純粋なインターフェースのアイデアを回避して、すべての実装者IMotion
に同じ実装と関連付けられた状態を使用させ、 t目標を複製します。
したがって、インターフェースBehaviors
は、エンティティに関連付けられた広範なものになりました。IMotion
単にMotion
「コンポーネント」になります(「コンポーネント」を定義する方法を、COMから、「完全な」エンティティを構成する通常の定義に近いものに変更しました)。
これの代わりに:
class IMotion
{
public:
virtual ~IMotion() {}
virtual void transform(const Matrix& mat) = 0;
...
};
次のようなものに進化させました。
class Motion
{
public:
void transform(const Matrix& mat)
{
...
}
...
private:
Matrix transformation;
...
};
これは、依存関係の反転の原則に対する露骨な違反であり、抽象から具体へと移行し始めますが、このような抽象化のレベルは、合理的な疑いを超えて将来の真のニーズを予測できる場合にのみ有用ですそのような柔軟性のために、ユーザーエクスペリエンスから完全に切り離されたとんでもない「what if」シナリオを実行する(おそらくデザインの変更が必要になるでしょう)。
そこで、私たちはこの設計へと進化し始めました。QueryInterface
のようになったQueryBehavior
。さらに、ここで継承を使用することは無意味に見え始めました。代わりに構成を使用しました。オブジェクトは、実行時に可用性を照会して注入できるコンポーネントのコレクションになりました。
いくつかの長所:
Motion
実装(たとえば、100のサブタイプに分散していない)により簡単に対応できます。短所:
発生した現象の1つは、これらの動作コンポーネントの抽象化を失ったため、より多くのコンポーネントがあったことです。たとえば、抽象IRenderable
コンポーネントの代わりに、コンクリートMesh
またはPointSprites
コンポーネントでオブジェクトをアタッチします。レンダリングシステムは、レンダリング方法Mesh
とPointSprites
コンポーネントを認識し、そのようなコンポーネントを提供し、それらを描画するエンティティを見つけます。他の時にはSceneLabel
、後知恵で必要だとわかったようなさまざまなレンダリング可能なものがあったのでSceneLabel
、それらのケースでは関連エンティティに(おそらくに加えてMesh
)を添付しました。レンダリングシステムの実装は、それらを提供するエンティティをレンダリングする方法を知るために更新され、それは非常に簡単な変更でした。
この場合、コンポーネントで構成されるエンティティを別のエンティティのコンポーネントとして使用することもできます。レゴブロックを接続して、そのように構築します。
その最後のシステムは私が自分で作った限りであり、私たちはまだCOMでそれを酷使していました。エンティティー・コンポーネント・システムになりたいと思っていましたが、当時は私はそれをよく知りませんでした。建築のインスピレーションのためにAAAゲームエンジンを検討すべきだったときに、私は自分の分野を飽和させたCOMスタイルの例を見て回っていました。私はついにそれを始めました。
欠けていたのは、いくつかの重要なアイデアでした。
私はついにその会社を辞め、インディとしてECSの作業を開始し(貯蓄を使い果たしながら作業を続けています)、これは管理が最も簡単なシステムでした。
ECSアプローチで気付いたのは、上記でまだ苦労していた問題を解決したことです。私にとって最も重要なことは、複雑な相互作用のある小さな村ではなく、健康的なサイズの「都市」を管理しているように感じたということです。モノリシックな「メガロポリス」ほど維持するのは難しくなく、人口が多すぎて効果的に管理することはできませんでしたが、それらの間で悪夢のようなグラフを形成しました。ECSはすべての複雑さを、レンダリングシステム、健全なサイズの「都市」のような「過密なメガロポリス」ではなく、かさばる「システム」に向けて蒸留しました。
生データになるコンポーネントは、OOPの基本的な情報隠蔽の原則さえも破るので、最初は本当に奇妙に感じました。OOPについて私が大切にしていた最大の価値の1つに挑戦することでした。しかし、インターフェイスの組み合わせを実装する数百から数千のサブタイプにそのようなロジックを分散させるのではなく、そのデータを変換するダースまたは非常に広範なシステムで何が起こっているのかがすぐに明らかになったため、それは無関心になり始めました。システムがデータにアクセスする機能と実装を提供し、コンポーネントがデータを提供し、エンティティがコンポーネントを提供している場合を除き、OOPスタイルのスタイルであると考える傾向があります。
広いパスでデータを変換するかさばるシステムがほんの一握りしかない場合に、システムによって引き起こされる副作用について推論するのは、直感に反してさらに簡単になりました。システムはかなり「フラット」になり、各スレッドの呼び出しスタックはこれまでになく浅くなりました。その監督レベルのシステムについて考えることができ、奇妙な驚きにぶつかることはありませんでした。
同様に、これらのクエリを削除することに関して、パフォーマンスが重要な領域でさえも簡単にしました。「システム」の概念が非常に形式化されたため、システムは関心のあるコンポーネントをサブスクライブし、その基準を満たすエンティティのキャッシュされたリストを渡すことができました。各キャッシングの最適化を管理する必要はなく、単一の場所に集中化されました。
いくつかの長所:
短所:
ただし、ゲームエンジンで一般的なComponent-Entity-Systemアーキテクチャを使用してアプリケーションを作成することは合理的ですか?
とにかく、私は絶対に「はい」と言い、私の個人的なVFXの例は有力候補です。しかし、それはまだゲームのニーズにかなり似ています。
ゲームエンジンの懸念から完全に切り離された遠隔地での実践は行っていませんが(VFXは非常に似ています)、ECSアプローチの候補としてはるかに多くの分野があるように思えます。おそらくGUIシステムでさえも適切でしょうが、私はそこでさらにOOPアプローチを使用します(ただし、Qtとは異なり、深い継承はありません)。
それは広く未開拓の領域ですが、あなたのエンティティが「特性」の豊富な組み合わせ(およびそれらが提供する特性のコンボが常に変化する可能性がある)で構成できる場合、そしてあなたが少数の一般化されている場合必要な特性を持つエンティティを処理するシステム。
これらのケースでは、多重継承または概念のエミュレーション(ミックスインなど)を使用して、深い継承階層または数百のコンボで数百以上のコンボを生成したい場合のシナリオの非常に実用的な代替手段になります。インターフェイスの特定のコンボを実装するフラットな階層のクラスですが、システムの数が少ない場合(数十など)。
これらの場合、コードベースの複雑さは、タイプの組み合わせの数ではなく、システムの数に比例するように感じ始めます。これは、各タイプが、生データにすぎないコンポーネントを構成する単なるエンティティになったためです。GUIシステムは当然、これらの種類の仕様に適合し、他の基本タイプまたはインターフェースから結合された数百の可能なウィジェットタイプがありますが、それらを処理する少数のシステム(レイアウトシステム、レンダリングシステムなど)のみがあります。GUIシステムがECSを使用している場合、継承されたインターフェースまたは基本クラスを持つ数百の異なるオブジェクトタイプの代わりに、これらのシステムのすべてによってすべての機能が提供されると、システムの正確性について推論するのがおそらくはるかに簡単になります。GUIシステムがECSを使用した場合、ウィジェットには機能がなく、データのみがあります。機能を持つのは、ウィジェットエンティティを処理するほんの一握りのシステムだけです。ウィジェットのオーバーライド可能なイベントがどのように処理されるかは私を超えていますが、これまでの限られた経験に基づいて、そのタイプのロジックを特定のシステムに集中的に転送することができない場合は、後知恵は、私が今まで期待していたはるかにエレガントなソリューションをもたらしました。
私の命の恩人だったので、私はそれがより多くの分野で使われるのを見てみたいです。もちろん、コンポーネントを集約するエンティティからそれらのコンポーネントを処理する粗いシステムまで、設計がこのように分解しない場合は不適切ですが、自然にこの種のモデルに適合する場合、これは私がこれまでに遭遇した中で最も素晴らしいものです。
ゲームエンジンのコンポーネントエンティティシステムアーキテクチャは、ゲームソフトウェアの性質、およびその独自の特性と品質要件のために、ゲームで機能します。たとえば、エンティティは、ゲーム内の物事に対処して作業するための統一された手段を提供します。これは、目的と用途が大幅に異なる場合がありますが、システムによって統一された方法でレンダリング、更新、またはシリアライズ/デシリアライズする必要があります。このアーキテクチャにコンポーネントモデルを組み込むことにより、シンプルなコア構造を維持しながら、必要に応じてより少ないコードカップリングで機能を追加できます。CADアプリケーション、A / Vコーデックなど、この設計の特徴を活用できるさまざまなソフトウェアシステムがあります。
TL; DR-設計パターンは、問題の領域が設計に課す機能と欠点に十分に適している場合にのみ機能します。
問題のドメインがそれに適している場合は、確かに。
私の現在の仕事には、実行時の要因に応じてさまざまな機能をサポートする必要があるアプリが含まれます。コンポーネントベースのエンティティを使用してこれらの機能をすべて分離し、拡張性とテスト容易性を分離して実現することは、私たちにとって牧歌的です。
編集: 私の仕事には、専用ハードウェア(C#)への接続性の提供が含まれます。ハードウェアのフォームファクター、インストールされているファームウェア、クライアントが購入したサービスのレベルなどに応じて、デバイスにさまざまなレベルの機能を提供する必要があります。同じインターフェースを持つ一部の機能でも、デバイスのバージョンに応じて実装が異なります。
ここにある以前のコードベースには、非常に幅広いインターフェイスがあり、多くは実装されていません。いくつかは、多くのシンインターフェースを持っていて、1つのクラスで静的に構成されていました。単に文字列->文字列辞書を使用してモデル化したものもあります。(私たちには、もっとうまくやれると思う部門がたくさんあります)
これらにはすべて欠点があります。幅の広いインターフェイスは、効果的にモック/テストするのに苦労します。新しい機能を追加するということは、パブリックインターフェイス(およびすべての既存の実装)を変更することを意味します。多くのシンインターフェースは非常にいコードを消費しましたが、最終的には大きな脂肪オブジェクトをテストすることに苦労しました。さらに、シンインターフェイスは依存関係を適切に管理しませんでした。文字列辞書には、通常の解析と存在の問題、およびパフォーマンス、可読性、保守性の問題があります。
現在使用しているのは、ランタイム情報に基づいてコンポーネントを検出および構成する非常にスリムなエンティティです。依存関係は宣言的に行われ、コアコンポーネントフレームワークによって自動解決されます。コンポーネント自体は、依存関係を直接処理するため、単独でテストできます。依存関係の欠落の問題は、依存関係の最初の使用ではなく、1つの場所で早期に発見されます。新しい(またはテスト)コンポーネントをドロップインすることができ、既存のコードが影響を受けることはありません。コンシューマーはエンティティにコンポーネントへのインターフェースを要求するため、比較的自由にさまざまな実装(および実装がランタイムデータにどのようにマップされるか)を自由に調整できます。
このような状況で、オブジェクトとそのインターフェースの構成に共通コンポーネントの(非常に多様な)サブセットを含めることができる場合、非常にうまく機能します。