こんにちは、ハッカーヌーン!私が選んだ次のトピックは、 における ECS (Entity-Component-System) です。すべての情報をよりアクセスしやすくするために、2 つの部分に分けました。 Unity 開発 エンティティ コンポーネント システムについて私が知っていることをすべてお話しし、このアプローチについてのさまざまな先入観を払拭したいと思います。 ECS の長所と短所、このアプローチの特殊性、ECS と仲良くなる方法、潜在的な落とし穴、有用な実践方法について多くの言葉が見つかります。 Unity/C# 用の ECS フレームワークについても簡単に説明します。 この記事は、ECS について知りたい、または知り始めたい人に適しています。 ECSを味わったことがある人も、自分にとって何か新しいことを強調できるようになると思います。 C# 以外の言語でゲームを作成する場合でも、この記事が役立つ可能性があります。コードサンプルやパターンの歴史はなく、私の経験、推論、観察だけが含まれます:) ECS (エンティティ コンポーネント システム) とは何ですか? Entity-Component-System は、ゲーム開発専用に作成されたアーキテクチャ パターンです。ダイナミックな仮想世界を表現するのに最適です。その特殊性のため、これをほぼ新しいプログラミング パラダイムだと考える人もいます。 ECS は、継承よりも構成の絶対的な原則です。これはデータ指向設計 (DOD) の特定の例である可能性がありますが、特定の実装によるパターンの解釈に依存します。 このパターンの名前を解読してみましょう。 - 最大限に抽象化されたオブジェクト。これは、このエンティティが何になるかを定義するプロパティの条件付きコンテナです。多くの場合、データにアクセスするための識別子として表されます。 エンティティ - オブジェクト データを含むプロパティ。 ECS のコンポーネントには、ロジックが一切含まれていない純粋なデータのみが含まれている必要があります。それにもかかわらず、一部の開発者はコンポーネント内でさまざまなゲッターとセッターを許可しています。それでも、これらの目的には静的ユーティリティの方が適していると思います。 コンポーネント - データ処理のロジック。 ECS のシステムにはデータを含めず、データ処理ロジックのみを含める必要があります。ただし、繰り返しになりますが、一部の開発者は、定数やさまざまな補助サービスなど、システムの補助的な動作を定義することを許可しています。 システム 上記からすでにお気づきのとおり、ECS はデータをロジックから厳密に分離します。オブジェクトの動作は、古典的なオブジェクト指向プログラミング (OOP) で慣れ親しんでいたようなインターフェイス/コントラクト/パブリック API によって決定されるのではなく、データと処理ロジックが個別に存在するオブジェクトに割り当てられたプロパティによって決定されます。 ECS では、データがすべてを定義します。これは、他の開発アプローチと区別する主な特性です。すべてがデータであるということです。オブジェクトのプロパティ、特性、イベントは、ECS の世界では単なるデータです。ロジックは、単にこのすべてのデータのパイプライン処理です。 なぜECSが必要なのでしょうか? おそらく、すでに「なぜ ECS が必要なのですか? それは何の役に立つのですか?」という疑問を持っているでしょう。この記事をさらに読むかどうかを決めるのに役立つように、私が ECS を好む理由を説明します。 個人的に ECS が大好きなのは、次の理由からです。 ECS を使用すると、プロジェクトのアーキテクチャと格闘するのではなく、ただ座って だけです。大きくて美しい階層を構築したり、多くのつながりについて考えたり、「X は Y について知ってはならない」と心配したりする必要はありません。同時に、ECS 原則は、さらなるプロジェクト開発が非常に困難になった場合に、不適切なアーキテクチャによって引き起こされる絶望的な状況から (もちろん 100% ではありませんが) 保護します。また、何か問題が発生した場合でも、ECS でのリファクタリングは問題になりません。私の意見では、これが ECS の最も良い点です。 Unity でゲームを作成する ECS のコードはシンプルかつ明確です。特定のシステムが何を行うかを理解するために、クラス間の呼び出しをくまなく調べる必要はありません。特に機能をシステムに分割し、システムをメソッドに分割し、コードを過度に複雑にしない場合は、すべてを一度に確認できます。さらに、ECS はプロファイリングを大幅に簡素化します。どのロジック(システム)でどれくらいのフレームタイムがかかっているかが一目でわかります。呼び出しの深さで遅延の原因を探す必要はありません。 ロジックを操作するのは簡単です。新しいロジックの追加は、ほとんど手間がかかりません。コードの残りの部分に直接影響を与えることを恐れることなく、新しいシステムを適切な場所に挿入するだけです (データを介した間接的な影響が発生する可能性があることに注意してください)。使用するデータ(コンポーネント)を維持したまま、クライアントとサーバー間で共通のロジック(システム)を問題なく使用できます。コードの残りの部分に影響を与えることなく、システムを簡単に書き直し、古いシステムをリファクタリングされたシステムに置き換えることができます。結果が気に入らない場合は、古いシステムをオンに戻してください。同じメカニズムで A/B テストを簡単に構成できます。 すべてはデータを中心に展開します。それは非常に便利であることがわかりました。エンティティのデータを直接操作することにより、組み合わせ論の可能性は計り知れません。データを使用してエンティティをあらゆるものに成形できます。そして、フレームワークがエンティティのデータを表示するツールを提供しているとします。その場合、デバッガを実行してメモリ内を調べることなく、任意のエンティティのデータとそのダイナミクスを調べることができます。 さて、私を理解できましたか? ECS と連携するにはどうすればよいですか? ここでは、最も単純な例で ECS を使用した開発プロセスがどのように機能するかを簡単に説明します。プログラミング言語を参照せずに、これをできるだけ抽象的に実行します。すでに ECS の経験がある場合は、そのまま次のセクションに進んでください :) 指定された動きベクトルの方向に移動するオブジェクトを作成します。 タスク: まず、作業に必要なデータを定義しましょう。このタスクでは、オブジェクトの位置と指定された動きベクトルが必要になります。 ECS 言語では、次のようになります。 位置ベクトルを格納するための PositionComponent 動きベクトルの MovementComponent 次のステップはロジックを説明することです。 を作成しましょう。システムのメイン メソッドでは、実装に応じて、 などになります。 と を持つ ECS 内のすべてのエンティティを取得します。これをどのように正確に実行できるかはフレームワークによって異なりますが、多くの場合 のような一種の SQL クエリのように見えます。 MovementSystem Run()/Execute()/Update() PositionComponent MovementComponent GetAllEntities().With<PositionComponent>().With<MovementComponent>() 最後に、2 つのコンポーネントを使用してエンティティ (10 個でも) を作成し、ゼロ以外の動きベクトルを設定するだけです。これで、 を呼び出すたびに (いつどこで呼び出すかに関係なく)、オブジェクトの位置が指定された動きベクトルの方向に変わります。任務は達成されました! :) MovementSystem 多くの場合、システムは何らかの形でプロジェクトの GameLoop に組み込まれており、エンジン自体によってすべてのフレームを切り替えます。ただし、これは単なるメソッド呼び出しであるため、手動でも他の方法でも行うことができます。 主な問題の解決に加えて、開発にどのような追加の可能性が得られるかを見てみましょう。 他のシステムでは、MovementComponent プロパティの存在を確認するだけで、オブジェクトが動いているかどうかを判断できます。 他のシステムは必要に応じて動きベクトルを取得できます 他のシステムは、任意のエンティティの動きベクトルを自由に指定できます。 必要に応じて、 と を配置するだけで、他のエンティティを移動させることもできます。これは、 ときに非常に便利です。 PositionComponent MovementComponent Unity ゲームを作成する Unity における ECS の長所 このセクションでは、ECS の良い点と悪い点について説明します。以下で説明する機能の一部には、コインの裏表があります。これらは開発にとって有益であると同時に不快であり、場合によっては回避しなければならない制限を生み出します。まず、Unity における ECS の利点について説明します。 コードの結合力が弱い これは、 にとって有益な特性です。これにより、古いコードを壊すことなく、コードベースを比較的簡単にリファクタリングおよび拡張できます。古いデータを使用して、古いロジックに一切干渉することなく、いつでも新しい動作を追加できます。 ECS は、データがエンティティ内のすべてのロジック相互作用を表現するため、この効果を実現します。これは、C#/Java の一部のオブジェクトと同様、保証のない最大限の抽象オブジェクトです。 Unity ゲーム開発者 ただし、ECS では、データ変更の順序が重要な役割を果たすことに注意してください。最終的にはリファクタリングの複雑さに影響を及ぼし、古いロジックが壊れたり、不快な副作用のバグが発生したりする可能性があります。 ロジックの完璧なモジュール性とテスト容易性 すべてのインタラクションが純粋なデータで表現される場合、ロジックは常にデータ ソースから完全に切り離されます。これにより、ロジックをプロジェクト間で移動して再利用したり (もちろんデータ形式を維持しながら)、入力データに対してロジックを実行してその動作をテストしたりすることができます。 貧弱なコードを書くことが難しくなる ECS は、本当に悪いコード設計を作成することがより困難になるフレームワークを設定するため、アーキテクチャに対する要求が低くなります。同時に、上で述べたように、たとえ不適切なコード設計が発生したとしても、コードの残りの部分への影響を最小限に抑えながら、比較的簡単に問題を修正できます。 ECS を使用すると、「何も壊さずにこのロジックをアーキテクチャに組み込む方法」について考える必要がなくなり、新しい機能を追加できるようになります。 プロパティの組み合わせ論 この利点により、ECS は動的な世界を記述するための優れたオプションになります。想像してみてください。手間をかけずに、任意のエンティティに任意のプロパティ (したがってロジック) を与えることができます。 カメラにヘルスを持たせたい場合は、カメラに 配置できます。ダメージを受けます(そのようなシステムがあれば)。 エンティティに配置すると、 はすぐに燃焼によるダメージを受け始めます。プレイヤーの制御下で家を動かしたいですか?問題ありません。 を使用してください。 HealthComponent InFireComponent HealthComponent PlayerInputListenerComponent 経験豊富な開発者は、「ああ、ほとんどの継承パターンよりも合成パターンはこれを処理できます。ECS の方が優れているのはなぜですか?」と言うでしょう。私の答えは次のとおりです。「ECS を使用すると、エンティティの形成という観点からプロパティを組み合わせるだけでなく、同じエンティティ上で複数のプロパティ (コンポーネント) を組み合わせるときに特定のロジックを作成することもできます。」 エンティティのコンポーネントに触れることなく、古いデータにまったく新しいロジックを追加できる機能についてはまだ言及していません。 単一の責任を強制する方が簡単です ロジックがデータから完全に分離され、オブジェクトやエンティティに関連付けられていない場合、階層内の位置ではなく目的によってロジックの分割を制御することが容易になります。各システムは、そのシステムに固有の特定のタスクを実行するだけです。多くの場合、システム コードは、同じタイプの多数のコンポーネントに対する単一のメソッド呼び出しのように見えます。その結果、コードはほとんど読みやすく、認識しやすくなります。 より明確なプロファイリング プロファイリングを行うと、どのようなロジックとどれくらいのフレーム時間がかかるかを確認できます。これは、処理を担当するロジックを備えた別個のシステムのおかげで可能になります。最も時間がかかるものを理解するためにコールスタックを深く調べる必要はありません。有罪の CharMovementSystem がすぐにわかります。 フレームワーク自体には呼び出しスタックがある可能性があるため、この利点は ECS フレームワーク デバイスに依存することに注意してください。 ECS はパフォーマンスを大幅に向上させます 多くの人は、優れたパフォーマンスが ECS の主な利点であると考えています (Unity の宣伝のおかげです)。これは完全に真実ではありません。コードの実行速度は、ある場所にデータ - 別の場所にロジック + SIMD (単一命令、複数データ) というパターンの原則から得られる素晴らしいボーナスです。また、ECS の実装時にフレームワークが DOD に従い、良好なデータ局所性を達成できれば、キャッシュに適したコードも得られ、プロセッサーが満足することになります。 ECS の最終的なパフォーマンスは、フレームワークがデータをどの程度正確に保存するか、フレームワークがエンティティをフィルタリングする方法、システムがデータにアクセスする速度、システム内のコードが動作する速度など、多くの要因によって決まります。 ただし、 、特に大量のデータの場合、ECS は通常の MonoBehaviour アプローチよりも常に高速になります。ただし、ゲームのパフォーマンスで重要なのはアーキテクチャ パターンではなく、作成するコードのアルゴリズムの複雑さとパフォーマンスであることを忘れないでください。 Unity 開発のコンテキストでは データ処理の並列化が容易になる ロジックは別のデータプロセッサに分離されており、データは実際には線形シーケンスであるため、1 つのシステム内で問題なく処理を並列化できます。これは、システムが大量のエンティティを同時に処理し、それらが互いにまったく交差しない場合に非常に重要です。 さらに進んで、変更されたデータと重複しないロジックを別のスレッドに送信することもできます。ただし、制御と監視ははるかに困難です。ただし、データを準備するためのメインスレッドとの同期にはボトルネックが発生します。さらに、データの準備とスレッド間での配布のオーバーヘッドが、システムでのコードの実行時間よりも高くなる場合があります。したがって、本当にそれだけの価値があるかどうかを評価する必要があります。 クリーンなデータは非常に扱いやすい ほぼすべての Unity ゲームでは、ネットワーク経由で送信するために何かを保存、ロード、またはシリアル化する必要があります。データがロジックから分離されている場合、これははるかに簡単になります。 「これをどのようにプライベート データに取り込むべきか...」と考えて、適切なシリアル化のために特別なメソッドを呼び出す必要はありません。必要なコンポーネントをエンティティに保存/ロードするだけです。その後、システムが必要と判断した場合は、それを目的の状態に完成させます。 ECS フレームワークは必要なだけ頻繁に変更できます ECS フレームワークは原理が同じであるため、互いに似ています。 ECS 用に自分の脳を再構築し、あるフレームワークを一度よく理解した開発者は、別の ECS フレームワークを問題なく使用できます。 API と特定のフレームワークの特徴を学ぶには時間がかかるだけです。しかし、新しいアプローチのために頭を再構築する必要はありません。 Unity の ECS の短所 ご覧のとおり、Unity の ECS には他のパターンに比べて多くの貴重な利点があります。次に、Unity における ECS の欠点について説明します。 経験豊富な Unity 開発者にとって敷居が高い ECS の概念は一文で説明できますが、正しく使用できるようにするには多くの練習が必要です。 ECS では、これまでデザインについて知っていたことをすべて忘れる必要があります。つまり、垂直継承階層のすべて、オブジェクトの動作はそのインターフェイスによって決定されること、オブジェクトは具体的で不変なものであること、オブジェクトはプライベート スペースを持つことができ、ロジックはどこにでも呼び出されます。 ECS では、すべてがそうではありません。それは上で説明したことの逆です。ここでは、すべてのデータがオープンであり、すべてのエンティティは抽象的で非常に動的であり、そのプロパティは 1 つの平面にあり、誰でもアクセスでき、ロジックはコンベアの原理で機能し、エンティティの動作は一般的にデータに基づいてその場で変化します。 コードの結合力が弱いと問題になる可能性がある 突然 2 つの具体的なエンティティ (たとえば、キャタピラ本体と戦車砲塔) の間で緊密な相互作用が必要になったとします。その場合、エンティティが抽象的であり、キャタピラ本体が反対側にあることをコンパイラ レベルで保証できないという問題に直面します。 Unity ゲームは多くの緊密な対話が行われる場所であり、プロパティと動作が保証された直接参照を常に必要とするため、これは邪魔になります。コンポーネントの存在を確認し、その不在を何らかの方法で処理したり、エンティティからコンポーネントにアクセスして対話を開始したりする必要があります。 どこからでもあらゆるデータにアクセス ECS の世界は、すべてのコンポーネントで利用できるデータを備えたエンティティのオープン ボックスです。上記の弱いコードの結合と同様に、これは ECS の長所でもあり短所でもあります。 一方で、それは非常に便利です。当面の問題を解決するために、設計プロセスの初期に作成された自己制限フレームワーク (「X は Y について知ってはならない」) をバイパスして、これまで隠されていたデータを公開する方法を見つける必要はありません。 一方、経験の浅いプログラマーは、データを変更すべきではない場所から変更しようとします。ただし、通常、チームワークには他の人の仕事を信頼することが含まれるため、信頼するだけでなく確認してください ;) システムはフローのみで次々と動作します ECS の原則に正しく従う場合は、あるシステムのロジックを別のシステム内で呼び出さないでください。システムはお互いの存在をまったく認識すべきではありません。そうしないと、コードに不必要な凝集が生じ、プロジェクトに損害を与える可能性があります。ただし、この制限は不便な場合があり、ECS 原則に違反しないさまざまな回避策が必要になる場合があります。今ここでまだコードを呼び出す必要がある場合は、メソッドを含む通常のオブジェクトを作成し、それをコンポーネントに配置するだけです。自分を苦しめる必要はありません。 再帰ロジックではうまく機能しません この欠点は、前の欠点の結果です。スレッドの外部のシステム コードをどこでも呼び出すことができないため、ECS では特定のシステムの外部で再帰コードを作成することがほぼ不可能になります。 この欠点の解決策 (別名、ECS 原則に準拠するための回避策) として、私が提案できるのは、特定の条件が満たされる限り、無限ループでシステムの特定のリストを呼び出す特殊な構造/システムを作成することだけです。つまり、DoActionComponent を持つエンティティが存在する限りです。もっと洗練された回避策がある場合は、コメントで喜んで読んでいきます:) システムの実行順序は重要です ECS では、システムがデータをどのように変更するかを理解し、制御することが重要です。作業中のデータに対する何らかのシステムの影響を見逃して、さまざまな予期せぬ副作用が発生する可能性がよくあります。ちなみに、追跡が複雑になる場合があります (これが次の欠点です)。ただし、システムを作成する場合、システムが呼び出される順序が問題にならないように設計することも可能です。 デバッグが難しくなる これは、特に最新のスマート IDE ではかなり物議を醸す点です。深い StackTrace が欠如しており (システムにはエンティティに関連付けられていないロジックが存在します)、データとエンティティの状態が誰によってどのように変更されたかを追跡できないため、システムが変更された理由を見つけるのが困難になる場合があります。突然意図したとおりに動作しなくなります。誰かがエンティティにコンポーネントを追加したり、追加の ++ を作成しただけであっても、何が特定の呼び出しにつながったのかを理解するのは簡単ではありません。 要約すると、ECS では、デバッグ ツールがなければ、コンポーネント内のデータがなぜどのように変更されたのかを追跡するのは困難です。特に、エンティティが数千もあり、問題のあるエンティティが 1 つだけの場合はそうです。これは、フレームワークが提供するデバッグ ツールによって解決できます。ただし、それらはそのままでは入手できない場合があり、自分で作成するか苦労する必要があります。 データ構造、特に階層構造のひどいオプション ECS を使用してデータ構造を実装するのは難しく、不便で、私の意見では、意味がありません。不可能とは言いませんが(頑張れば何でも可能です)、結局はあまりメリットのない茨の道になるので、合理的に選択してください。 ECS でデータ構造を実現しようとするときに妨げとなる問題をいくつか挙げます。 ECS では、すべてのデータにどこからでもアクセスできます。これは、最大限の一貫性が必要なデータ構造にとっては非常に危険です。 「ワニ」が通過すると、内部データが変更されてロジックがバイパスされ、データ構造が完全に破壊される可能性があります。 ECS の原則に正直に従っている場合、データ構造を操作するときに通常必要となる、データ構造のロジックを今ここで呼び出すことはできません。ただし、この点は静的ユーティリティ/拡張機能を使用して対処できます。 ECSは水平アーキテクチャの代表格です。その中のすべてのデータは 1 つの平面にあり、ほとんどの場合、コンポーネントの 1 次元配列にすぎません。データ構造に垂直性や階層性が必要な場合、これが困難になります。 データ構造で要素 (階層) 間の相互参照が必要になることは珍しいことではありません。しかし、覚えているかもしれませんが、ECS ではすべてが最大限に抽象化されたエンティティを中心に展開します。相手側で必要なタイプの要素が保証されていないため、作業が困難になります。そのため、個別に処理する必要があります。 データ構造とその要素は通常、実行時にデータ形式を変更する必要はなく、組み合わせも必要ありません。かなり硬いです。各データ構造エンティティは、最終的にコンポーネントを 1 つだけ持つことになる場合があります。 データ構造がまだ必要だとします。その場合は、メソッドを備えた別個のオブジェクトとして作成し、このオブジェクトをコンポーネントに配置して、通常どおりシステムから操作することをお勧めします。 より多くのファイルとクラス では、プロジェクト内のファイルの数が、従来のアプローチの同様のコードの場合よりも速く増加します。少なくとも、データとロジックを含む 1 つのクラスではなく、コンポーネントとシステムという 2 つのクラスがあるためです (これらを 1 つのファイルに隠すこともできます)。せいぜい、すべてのコンポーネントをアトミック (1 コンポーネント - 1 フィールド) にすると、非常に多くのファイルが存在することになります... ECS アプローチ 定型コード この欠点は、ECS フレームワークの特定の実装に大きく依存します。一部のフレームワークでは、大量の技術コードを作成する必要があります。他の例では、開発者は可能な限り単純な API を作成し、定型文を最小限に抑えようとしました。ただし、他のアプローチと比較すると、ほとんどの場合、少なくとも少量の追加コードを作成する必要があります。つまり、コンポーネントの宣言、必要なコンポーネントを含むフィルターの取得、そこからエンティティの取得、エンティティからコンポーネントの取得などです。 小さな結論 これで1部は終了です。パート 2 では、次のことについて説明します。 ECS における初歩的なミス ECS のグッドプラクティス Unity/C# で ECS を操作するためのフレームワーク ご質問がございましたら、コメント欄に残してください。