私は(学習目的で)最初のc ++ゲームエンジンプロジェクトを作成しています。その中で、データ指向の設計原則を利用してエンティティ/コンポーネントシステムを実装しようとしましたが、オブジェクト指向の考え方を完全に諦めていません。データのみのコンポーネント構造があります:
(例)
Position.h
struct Position
{
Vector3D position{ 0.0f, 0.0f, 0.0f };
};
私のSceneManagerクラス内の配列に格納されています:
SceneManager.h
class SceneManager
{
//Some other code....
private:
//So only systems have access to component arrays
friend class BGraphics::RenderSystem;
friend class BInput::InputSystem;
friend class BPhysics::MovementSystem;
friend class BPhysics::CollisionSystem;
friend class BAudio::AudioSystem;
//Component arrays. Each index of the arrays represents an entity.
//So positionComponents.at(1) represents entity 1's position.
Array<Position, 10> positionComponents;
Array<Velocity, 10> velocityComponents;
};
ご覧のとおり、コンポーネントの配列にアクセスできるのは私のエンジンのシステムクラスだけなので、すべてのシステムがコンポーネントを処理できます。このセットアップには、単純化のために深く掘り下げたくない、他のいくつかのものがあります。私の問題は次のとおりです。
各システムは、次のようにUpdate関数でSceneManager参照を受け入れます。
RenderSystem::Update(SceneManager& scene)
{
//Use scene component arrays necessary for processing...
}
私が今苦労しているのは、すべてのシステムがコンポーネントにアクセスして状態を変更できるという点で、コンポーネントがグローバル変数のように見えることです。データ指向の設計の多くは、他の関数が操作できるパブリックデータを含むこれらの構造体を含み、状態を変更するようです。それが言われると、私のコンポーネントデータがよりカプセル化され、より安全になる私の実装に欠けているものはありますか?データ指向の設計を利用するときに、安全な方法でデータを処理することについてどう思いますか?明確化を歓迎します。
私が今苦労しているのは、すべてのシステムがコンポーネントにアクセスして状態を変更できるという点で、コンポーネントがグローバル変数のように見えることです。
これは、ECSでの私の最大の闘いであり、回避しようとしたものです(コンポーネントを、データの設定/取得を超えた機能を持つオブジェクトにすることで、ECSの最初の実装を失敗させました)。
私が適切なエンジニアリング標準を主張しているチームの迷惑な男だったレガシーCコードベースで働いていたのは助けにはなりませんでした。そのコードベースでは、いくつかのArray struct
(に似ている std::vector
)負の値であるsize
フィールドがあり、コードベースの多くがそのsize
フィールドに直接接触し、データに対してrealloc
を呼び出す。そのフィールドにメモリブレークポイントを設定し、1つのブレークポイントから次のブレークポイントまで何時間も費やして、コードのどの行がそのrawフィールドを負の値に設定するかを見つけなければなりませんでした...これは、size
がネガティブに入ることが決して許可されないなど、不変条件を維持できるようにシステムがパブリックインターフェイスを通過する必要があるプライベートフィールドを持つクラスである場合は、より早く。
そして、それは珍しい出来事でさえありませんでした。デバッガーのメモリブレークポイントに頼る必要があり、何百万行ものコードの中で、このrawデータフィールドを決して必要のない値に設定する必要があったのは、毎日のことでした。バグを見つけて修正する時間を短縮する方法としてこれらをクラスに向けてリファクタリングすることは、バグを修正し、それをすべて禁止することは禁止されていました。私がそのチームに加わったとき、QAチームが非常に注意深く重複を除外しているにもかかわらず、5,000を超えるバグが報告されているバグレポートリストを継承しました。バグの90%は、これらのような基本的な不変量の違反であり、情報の非表示によって防止されていました。初期化されていない変数(および初期化されていない変数の定義時に初期化すること)のような他の愚かな防止可能なものでは、初期化の前に変数が使用されていることが証明できるまで、コードベースが生成するのに役立ちませんでした。初期化されていない可能性のある変数についての警告の多くが無害であることを含む、100,000を超える警告。そのコードベースは、私が実際にCを嫌い、もともとCのコーダーであるにもかかわらず、C++の考え方を最大限に取り入れました。脳の損傷を元に戻し、再びCを愛するようになるまでに数年かかりました。
とにかく、あなたが想像できるように、コンポーネントがパブリックにアクセス可能なフィールドを持つ生データであるという考えを受け入れることは非常に困難であったので、ECSでの最初の試みで、コンポーネントを機能を持つオブジェクトにして、頑固に抵抗しました別々のパブリック/プライベートインターフェースを備えていますが、すぐに過剰でPITAであることが判明しました。
ECSを使用すると、広範なシステムが非常に少なくなり、すべてが一般に完全に分離される傾向があります。たとえば、OOP階層内の数百の異なる相互依存サブタイプまたは上記のような手続き型レガシーコードベースで相互に呼び出す無数の手続き型関数。メンテナンスの観点からの魅力の多くは、コードベース内にロジックが関連付けられている場所が少ないため、コードベース内で実行できる場所が少ないことです。何かがおかしいです。生データは決して誤動作することはなく、改ざんするコードのみです。コンポーネントに機能を追加することになった場合、システムでは、データではなく実際のコードがある場所が増えることになります。レガシーコードベースの理由上記で説明したのは維持するのが悪夢であり、Hiveが絶望的なバグであったのは、特定のデータに触れることができ、しばしば触れる可能性のある膨大な量のコードが原因でした。
ECSインバリアントの維持
特定のコンポーネントタイプにアクセスするシステムが非常に少なく、どのシステムがどのコンポーネントタイプにアクセスするか、またはどのシステムがそれを行うかを簡単なテキスト検索ですばやく見つけることができるので、コンポーネントの不変量を維持するのは簡単です。コードベース。設計上、ECSのシステムは、コンポーネントタイプにアクセスする前に、explicitlyリクエストしてコンポーネントタイプにアクセスする必要があります。少なくとも私が経験上非常にまれであることがわかっているコンポーネントレベルで不変式に違反している場合、私がしなければならなかったような膨大なコードの海を通り抜ける必要なしに、容疑者の非常に狭いリストがすぐに得られます。すべてが潜在的にすべてに触れていた私のレガシーコードベース。 ECSではそれほど複雑ではありません。フィールドに設定してはいけない値が設定されているモーションコンポーネントを見つけた場合は、すぐに物理コンポーネントとアニメーションシステムのみがモーションコンポーネントを改ざんしていると推定できますしたがって、容疑者を劇的にすぐに絞り込みます。
そして、クラスの実装の詳細と同じように、デバッグ用の健全性アサーションを振りかけて、各システムが有効な状態を処理していて、コンポーネントフィールドを本来あるべきでない値に設定していないことを確認できます。
実際、ECSで最も難しいシステム全体の不変量を維持することは簡単です。データ構造がゼロになるsize
を決して持たないような種類の細かい不変式があります-十分に単純で、OOPそのカプセル化と情報の隠蔽は常に素晴らしいものでしたレガシーコードベースの細かい不変条件の違反に対処するのに非常に多くの時間を費やしました。しかし、愚かなバグで無秩序に広がっていないソフトウェアには、GPUレンダリングデータが確実に含まれるように、システム全体の不変条件があります。 CPUシーンデータと同期します。それらは維持するのがはるかに困難であり、すべてをオブジェクトに細かく分割して、より大きなオブジェクトに向かって作業することが常に役立つとは限らないため、はるかに多くの脳力が必要です。コード間のこのような複雑な相互作用で問題が発生した場合、疎結合の単純なコードではありますが、相互依存のボートロードが発生します。
一方、ECSは、最小限のコード相互作用(システムからコンポーネントへのカップリングがタイトな場合でも最小限のカップリング数)で非常に広いスケールで非常にフラットなコードモデリングロジックを生成し、多くの場合、これらの広範囲で高レベルのシステムについて推論しやすくします全体の不変条件。最小限のコード変更で新しいデザインアイデアに適応するためのECSのとんでもない柔軟性の恩恵はありませんでした(私はVFXにはECSを使用しており、ゲームは珍しいので珍しいです)。そして、システムを維持し、その正確さについての理由を、だれがどこにいるかを知ることにつながる抽象関数を呼び出す、小さいが相互依存する抽象オブジェクトの海に迷うことなく維持する。
一部のコンポーネントが少数のシステムによってのみアクセスされることが確実にわかっている場合は、それらを残りのコードベースに対して不透明型(宣言されているが定義されていない)として扱い、コンポーネントstruct
を定義するだけです。それらのいくつかのシステムからしか見えない場所に。また、コンポーネントが故意に生データになるように意図的に作成されている理由についても、今後クリックし始めるでしょう。ポーカーでのポジションのようなものです。 10万以上のハンドで賞金を記録し、実際に常にポジションで最も多くのお金を獲得したことがわかるまでは、それがそれほど有利である理由を理解することは困難です。 ECSは私にとってはそのようなものでした。実際にコードベースを適切に作成して保守を開始するまでは、複雑なコードベースの保守がいかに簡単になるかを理解できませんでした。それでも、人間のアイデアをモデル化していることを除いて、保守が非常に簡単である理由を正確に説明するのは難しいです。 SOLID OOP設計で得られるよりもカップリングがタイトな場合でも、より粗いレベルでカップリングの数を最小限に抑えます。
また、生データは関数を完全に回避する必要があることを意味する必要はありませんが、関数はデータへのアクセスのみに関係する必要があります。たとえば、vector
に関数がある場合でも、ECSにvector
フィールドがあることは、必ずしもこのタイプのECSモデルの違反ではありません。違いは、vector
には、生データへのアクセスと操作に関連する関数しかないことです。同様に、コンストラクタとデストラクタがあることは間違いなく問題ありません。概念的には、コンポーネントは未加工のデータです。