こんにちは、ハッカーヌーン!今日は引き続き、Unity 開発における ECS (Entity-Component-System) について説明します。 では、ECS (エンティティ コンポーネント システム) とは何か、ECS が必要な理由、ECS の操作方法、Unity における ECS の長所、および の ECS の短所。 Unity の ECS (エンティティ コンポーネント システム) に関するガイド: パート 1 Unity このパートでは、Unity ゲームで ECS を使用する際の初心者の間違いと良い実践方法に焦点を当てます。 Unity/C# のフレームワークについても少し説明します。 Unity ゲームで ECS を使用する際の初心者の間違い このセクションでは、経験豊富な人々が私の古いコードに気づいた、開発の妨げとなったエラーについて説明します。また、多くの初心者が ECS をマスターし始めるときに犯す間違いについても説明します。これがいくつかのバグを回避するのに役立つことを願っています。 コンポーネントの継承とインターフェース コンポーネントの継承とインターフェイスの使用は、初心者が犯しやすい間違いです。では、なぜそれが Unity 開発にとって悪いのでしょうか? 4つの理由: コンポーネント レベルでの抽象化は、データによる ECS 抽象化と比較してまったくメリットがありません。 これにより制限が生じます。そのようなコンポーネントを拡張したり、関連するロジックを改良したりすることが難しくなります。 これは、コンポーネントに独自のロジックがある状況を引き起こします。これは、覚えているとおり、ECS の原則に違反するものであり、行うべきではありません。 システムを継承したり、種類ごとにあらゆる種類のスイッチケースを作成したりする必要性が不合理になります。 システムの継承は常に良いアイデアではありませんが、コンポーネントの継承とは異なり、何かひどいことをするわけではないことにすぐに注意してください。したがって、コンポーネントを継承したい場合は、それを行わないでください。別の方法で問題を解決する方法をもう一度考えてください。 ECS 抽象化を適切に使用できない ECS 抽象化とは、一般的なデータ (OOP で継承される必要がある) を別のコンポーネントに単純に配置することです。このようなコンポーネントの「継承者」を作成するには、必要なデータを含む新しいコンポーネントを追加し、 と でエンティティをフィルタリングするだけです。すべては初歩的なものです。コンポーネント/エンティティ間で共通のデータがある場合は、ほとんどの場合、それらを別のコンポーネントに入れることができます。早ければ早いほど良いです。 BaseComponent InheritorComponent システムによるロジック変更の有効化/無効化 ECS では、世界とそれを処理するシステムは静的で常に存在しますが、エンティティとそのデータは非常に動的です。また、一部のロジックを無効にする必要がある場合、システムを無効にすることは正しい解決策ではありません。多くの場合、他のシステムにはアクセスできません (これは良いことです)。より実用的なオプションは、いくつかのマーカー コンポーネントを作成することです。マーカーのあるエンティティに対してシステム ロジックが機能すべきではないというメッセージが表示されます。 多くの新規参入者は、「しかし、システムのエンティティがない場合、なぜシステムが動作する必要があるのでしょうか? 最適化のためにシステムをオフにしたほうが良いのではありませんか?」と主張します。 いいえ、それは良くありません。 エンティティが存在しない可能性があることを認める場合は、 追加する方が簡単です。システムのメインメソッドの先頭に。 の規模では、関数を呼び出して 2 つの int を比較することは大海の一滴であり、パフォーマンスにはまったく影響しません。 if (entities.Length < 1) return; Unity ゲーム 実行時にシステムを無効にする唯一の正当なケースは、A/B テストと特定のシステムのデバッグ/テストです。ただし、ほとんどのフレームワークは、これをコードからではなくエディター ウィンドウから実行するためのツールを提供します。 ECSを絶対的なものにする ECS 支持者にとって OOP は禁止されていないことに注意してください:D ECS を使用する場合は、逆効果になる可能性があるため、ECS に完全に執着してすべてを ECS に移すべきではありません。さらに、 で述べたように、すべてのデータ構造が ECS にうまく適合するわけではありません。したがって、そのような場合には、別の OOP クラスを作成する方が良いでしょう。 ECS の欠点 中には、さらに進んで、プロジェクトの一部の要素 (UI など) を ECS に従って作成せず、他の便利な方法で少し脇に置き、ブリッジによって ECS に接続する人もいます。また、すべての追加ロジック (構成のロード、ネットワークとの直接作業、ファイルへの保存) は、OOP を作成し、目的のシステムから直接操作する方が簡単です。 Unity 開発者の 常識に基づいて何をすべきかを選択する必要があります。 ECS は開発を妨げるものではなく、開発を支援するものである必要があります。 既存のコードを変更せずに ECS に移植しようとしています 非常に多くの場合、初心者は既存のコードをそのまま ECS に転送しようとします。 ECS のコード作成アプローチは従来のアーキテクチャ パターンとは異なるため、これは良いアイデアではありません。このような移植の結果は、通常、ECS による混乱と非常に貧弱な最終コードになります。 古いコードを ECS に移植する必要がある場合、最善の選択肢は、同じロジックを ECS 上で最初から作成することです。必要なのは、自分の知識と既存のコードをガイドとして使用することだけです。 システムでのデリゲート/コールバックまたはリアクティブ ロジックの使用 ECS では、システムからロジックを取得して、後で使用するためにコンポーネントに保存したり、何らかの変更に即座に反応したりすること (例: 別のシステムへのコンポーネントの追加にシステムが反応するなど) は危険な場合があります。 これは、システムに不必要な相互接続性を追加するだけではありません (システムは外部呼び出しに大きく依存するようになります)。また、呼び出しをほとんど制御できないロジックが追加されるため、美しいデータ処理パイプラインが破壊されます。 ファイルをタイプごとにフォルダーに整理する ECS の使用を開始するときは、まず新しいファイルをタイプ別に配置します。コンポーネントは Components フォルダーに、システムは Systems フォルダーに配置します。しかし、経験を積むと、この分類方法は効率的とは程遠いことがわかります。どのコンポーネントがどのシステムに関連しているかを理解するためにナビゲートするのは簡単ではありません。 最良のオプションは、特定の機能に関連するすべてが 1 つのフォルダー (おそらくコンポーネント/システムの内部階層) にある場合に、機能で並べ替えることです。つまり、健全性と損傷に関連するすべてのコンポーネントとシステムは、[健全性] フォルダーに格納されます。これにより、フォルダーを参照してそのフォルダー内のシステムの基本的なデータ コンテキストを理解できるようになり、プロジェクトのナビゲートが容易になります。 Unity ゲームで ECS を使用するためのベスト プラクティス おそらく上記で、ECS で Unity ゲームを開発するときにしてはいけないことについて正確に読んだでしょう。ここで、役立つ実践方法と何ができるかについてのヒントについて話しましょう。 マーカーコンポーネントによるエンティティのタグ付け ECSにはタグコンポーネントという概念があります。これは、エンティティのタグ付けの役割のみを実行するフィールドのないコンポーネントです。これはクラス内のブール フラグとして考えることができます。存在するか (true)、存在しないか (false) のいずれかです。 たとえば、1,000 個のユニットがあり、そのうちの 1 つをプレイヤーが制御するとします。空の コンポーネントを使用してマークを付けることができます。これにより、フィルタ内のプレイヤー ユニットのみを取得できるほか、すべてのユニットを一度に処理するときに通常のユニットを操作しているのか、プレイヤーが制御するユニットを操作しているのかを理解できるようになります。 PlayerMarker コンポーネントが変更される箇所を最小限に抑える コンポーネントが変更される箇所は少ないほど良いです。一般に、これは「同じことを繰り返さない」という有益な原則に従うようなものです (私はこれをすべての人に推奨します)。これを実践すると、次のような多くの利点があります。 これにより、プロジェクト内のデータ変更プロセスをより深く理解できるようになり、何か問題が発生した場合のデバッグが簡素化されます。 データ変更のロジックを更新する場合、更新する必要があるコードは少なくなり、理想的には 1 か所だけを更新する必要があります。 データのバグがその場で発生する可能性が低くなるだけです たとえば、ダメージのあるすべてのシステムの HealthComponent を変更するのではなく、HealthComponent でエンティティにダメージを与えることを目的としたDamageSystem を 1 つ作成する方がよいでしょう。 コンポーネントの接尾辞を忘れる コンポーネントポストフィックスは、「ここにはデータだけがある」ということを初心者に思い出させるため、初心者にとって非常に役立ちます。しかし、時間の経過とともに、思い出させる必要はなくなり、ユビキタスなコンポーネントではコードが理解しにくいままになります。 だからこそ私はアドバイスしたいのですが、コンポーネントの接尾辞については忘れても問題ありません。おそらく、IntelliSense での検索が多少簡素化されること以外は、何も役に立ちません。これは単なるヒントであり、好みの問題でもあるので、どう対処するかはあなた次第です:) たとえば、 単なる になり、コードはもう少し読みやすい になります。この場合、 変更されません。これは、ポストフィックスが単純なコンポーネントではないことに注意して、この場合に有用な情報が含まれるためです。 HealthComponent Health entity.Has<Health>() PlayerMarker 遅延反応性と単一フレームコンポーネント ECS の反応性は有害となる可能性があります。しかし、反応性が必要な場合はどうすればよいでしょうか?答えは反応の遅れです。 遅延反応とは、イベント発生時にロジックを直接呼び出すのではなく、イベントが発生したことを示すデータを作成すると、全員が必要な時間にイベントに反応するだけになることです。 OOP のダーティ フラグに似ています。誰でも イベントを宣言できますが、ロジックは適切と判断した場合にそのイベントに反応します。 SetDirty(true) ECS では、データの有無にかかわらずコンポーネントを作成するだけで (既存のコンポーネントにブール値フラグを追加するだけで済みます)、時間になったらシステムが処理します。このようなコンポーネントが、すべてのシステムに警告するために 1 つのフレームだけが存在し、次のフレームでロジックが繰り返されないことは珍しくありません。削除は、イベントを生成するシステムによって処理することも、タイプ X のすべてのコンポーネントを必要な場所から削除する別のシステムによって処理することもできます。 たとえば、 あるとします。与えるダメージの量を伝えるには、ダメージ量を指定して を宣言し、ダメージを受けるエンティティにそれを追加します。 、 と を持つすべてのエンティティを調べ、エンティティにダメージを与え、 削除し、 以降のすべてのシステムに損傷したエンティティを通知する を作成します。フレームの終わりに、個々のシステムは、 削除し、システムが次のフレームでこのマーカーを再度処理しないようにします。 DamageSystem MakeDamageComponent DamageSystem HealthComponent MakeDamageComponent MakeDamageComponent DamageSystem DamagedEntityMarker DamagedEntityMarker システムの API としてのリクエスト/イベント 単一フレーム コンポーネントのアイデアを発展させると、それらを使用してシステム用の一種の API を表現できます。外部リクエスト用のリクエスト コンポーネントと、イベントについて全員に通知するためのイベント コンポーネントを区別する必要があります。システム自体は、両方のコンポーネントのライフサイクルを制御できます。イベントの処理およびクリーニング後、新しいイベントを開始する前にすぐにリクエストを削除することが可能です。正確にどのように名前を付けるか、および Requests/Events 接尾辞を追加するかどうかは、あなた次第です。 たとえば、前の段落の あります。 コンポーネントを使用してダメージ リクエストを表現し、 コンポーネントを使用して他のシステムに通知できます。システム内部のロジックは次のとおりです。最後のフレームからすべての クリアし、リクエストですべてのエンティティにダメージを与え、リクエストを削除して、 コンポーネントを追加します。 DamageSystem MakeDamageRequest DamagedEntityEvent DamagedEntityEvent DamagedEntityEvent コンポーネント内の別のエンティティへの参照を保存する 「ECS でエンティティ間のリンクを構築するにはどうすればよいですか? エンティティにコンポーネントをマークして、ループ内でエンティティを検索する必要がありますか?」という質問があるでしょう。 もちろん違います。すべてははるかに単純で、より一般的です。参照を保存するだけです。唯一の違いは、参照が対象となる別のエンティティのコンポーネントではなく、エンティティ自体への参照であることです。 したがって、エンティティ (またはフレームワークがエンティティを格納する方法) を含むフィールドをコンポーネントに追加します。使用する前に、エンティティが生きており、必要なコンポーネントがあることを確認します。次に、このコンポーネントを取得し、必要に応じて操作します。 たとえば、 エンティティに対して直接作成するのではなく、ターゲット エンティティを参照して別のエンティティ イベントとして実行することができます。これを行うには、 フィールドを に追加し、 を再加工します。これで、すべてのリクエストを処理し、ターゲットが を持つ生きたエンティティであることを確認し、 を取得してダメージを与える必要があります。 MakeDamageRequest Entity target MakeDamageRequest DamageSystem HealthComponent HealthComponent を実行すると、見た目も変わります。コンポーネントをエンティティに直接追加する代わりに、 を使用して新しいエンティティを作成し、 を指定します。このようにして、フィルタリングの利便性を犠牲にして、単一の エンティティに対して複数の異なるダメージ イベントをトリガーできます。 MakeDamageRequest MakeDamageRequest target target 反復ロジックを StaticUtils/Extensions に移動する 時間が経つにつれて、異なるシステムで同じロジックを実行していることに気づき始めます。通常、それは新しいシステムを作る時期が来たというサインです:D ただし、繰り返されるロジックは二次的なものであり、1 つまたは 2 つの特定のコンポーネント/エンティティに関連しており、その結果が別の目的に使用されることがあります。たとえば、コンポーネント内のデータの特別な解釈です。一部の開発者は、このような追加ロジックをコンポーネント内で直接宣言することを許可しています (たとえば、ゲッターの形式で)。ただし、ECS への違反を避けるために、システムから呼び出す静的ユーティリティ (または C# の拡張機能) という別のオプションをお勧めします。 たとえば、 あります。その中にチームカラーが入っています。 2 つのエンティティが同じチームに属していることを確認することは、複数のシステムで必要になる場合があります。そこで、静的クラス とその中のメソッド を作成し、2 つのエンティティのチームを比較する繰り返しロジックを記述します。 InTeamComponent TeamUtils IsInSameTeam(Entity, Entity) 実行の瞬間によるシステムのグループ化 すでにご存知のとおり、ECS ではシステムが呼び出される順序が重要です。したがって、フレーム内で呼び出される順序に従ってシステムを最上位にグループ化すると便利です。 たとえば、フレーム内で呼び出される最初のシステムは、すべての入力関連システムである可能性があります。ユーザー入力を収集し、ECS 形式で準備します。 2 番目は、入力データを独自の方法で解釈し、ECS ワールドを更新するゲーム ロジックを備えたシステムのグループです。そして最後に、レンダリングを担当するシステムのグループ、またはすべてのゲーム ロジックの後に呼び出されるさまざまな追加のものがある場合があります。 主要なフィーチャーを個別のアセンブリに分離する このアプローチでは、機能を相互に分離し、その依存関係を制御します。理想的な世界では、それらはまったく重ならないはずです。フィーチャ間の順序は重要ではありません。 すべての機能が動作するために必要なコンポーネントが配置されるコア アセンブリも必要です。 ECS ではコンポーネント/システムをより小さな部分に分割する必要がありますか? この質問は、さまざまな経験を持つ Unity 開発者によってさまざまな意見があるため、非常に興味深いものです。ただし、ニーズを理解していただくために、両方の答えを取り上げたいと思います。 はい、常にコンポーネントを分割する必要があります ECS 組織におけるこのアプローチはアトミックと呼ぶことができます。このアプローチの最上位は、各コンポーネントがフィールドを 1 つだけ持つことです。これにより、プロジェクトの組み合わせ論の頂点に達し、ECS 抽象化の名の下にリファクタリングを行う必要がなくなりました。私たちはもはや「エンティティと X プロパティをどのように組み合わせるか」を考えることはできません。 ECS でコンポーネントを常に分割することのデメリット: クラスとファイルの数が増えるため、プロジェクトの構成に適切な注意を払わないと、大規模なプロジェクトで混乱が生じる可能性があります。 コンポーネントの数 (一般またはエンティティごと) は、フレームワークのパフォーマンスに影響を与える可能性があります 多数のプロパティによってエンティティが何であるかを理解するのは難しくなります (通常の名前のマーカーで解決できます)。 さて、セカンドオピニオンに移りましょう。 いいえ、必要な場合にのみコンポーネントを分割する必要があります この原則は「時間前に分割しない」と呼ばれます。これは、私が個人的に遵守している原則です。データを分割する必要がある場合、指標は単純です。このデータは、このコンポーネントとは別の場所で使用されているか、または使用される予定ですか? そうでない場合、それに時間を費やす理由はありません。同様のアプローチでロジックをシステムに分割できます。 必要な場合にのみ ECS でコンポーネントを分割することのデメリット: ECS の抽象化にはまだ時間がかかる エンティティを設計する自由度が低下する どちらの側を選ぶかはあなた次第です:) Unity/C# のフレームワーク あなたが初心者であれば、まず Unity DOTS を学ぼうと考えていると思います。しかし、警告したいと思います。 Unity DOTS は巨大で複雑なため、初心者にとっては良い選択肢ではありません。私たちが望んでいるほど多くのドキュメントや経験豊富な人々はいません。さらに、古い Unity コードとの互換性があまりありません (この記事の公開後にすべてが変更される可能性があります)。 Unity エディターが好きで、コンポーネントをゲームオブジェクトに直接ぶら下げることにすでに慣れている場合は、 が最良の選択です。シンプルな API、Unity エディターへの緊密な統合 (Unity の外部でも動作可能)、MonoBehaviour との便利な連携を提供します。シンプルで便利です。 Unity を使用するために必要なものがすべて含まれています。インストールして操作するだけです。その主な欠点は、Unity で完全に作業するには有料の が必要であることです。 Morpeh Odin Inspector Entitas と DOTS (Unity ECS): それらをマスターする必要がありますか? ここで、私が個人的に出会った Unity/C# フレームワークと、私が気づいた長所/短所について簡単にレビューします。以下に説明する内容はすべて、この記事の公開以降に変更される可能性があるため、フレームワークを自分で確認することをお勧めします。 エンティタス これは Unity/C# 用の最も古い ECS フレームワークであり、今でも最も人気があります。これは、ECS の求人情報で最もよく見られるものです。 エンティタスの長所: Unity エディターの優れた WorldViewer 流暢なコード スタイル (コード生成のおかげで) 優れたドキュメント 非常に大きなコミュニティ 多くの成功したプロジェクト 開発者からの必須サポート (AssetStore に感謝) Unity の外部で純粋な C# で使用可能 エンティタスの短所: 他の ECS フレームワークと比較するとパフォーマンスが劣ります (ただし、MonoBehaviour よりは優れています) 大量の割り当てが GC に悪影響を与える コンポーネント構造の変更ごとにコード生成が必要 大規模なプロジェクトでは、コード生成により API が非常に肥大化します Github バージョンではコンパイル エラー時にコード生成を呼び出すことができませんが、AssetStore バージョンでは呼び出すことができます。 少なくとも基本的なレベルではマスターする必要があります。 DOTS (ユニティ ECS) 紹介の必要はないと思います。 DOTS はフレームワークではなく、Unity ECS をフレームワークとして内蔵した本格的なプラットフォーム (技術スタック) です。 以下の内容はやや古い経験であることを指摘しておきます。最新の DOTS を使用すれば、以下に説明する問題を解決できる可能性があることを認めます。 DOTS (Unity ECS) の長所: ECS 上の本格的な開発プラットフォーム エンジン開発者によって構築され、エディターに可能な限り緊密に統合されています ジョブとバーストをサポート ジョブやバーストと組み合わせることで、ECS フレームワークの中で最大のパフォーマンスを実現します。 NetCode 予測を備えた優れたネットワーク ライブラリを備えています サブシーンの仕組み DOTS (Unity ECS) の短所: 古いコードの変更や新たなバグを破壊する進行中の作業 ドキュメントが弱く、変更に対応していないことが多く、ソースコードを読まなければなりません アナログよりも技術的なコードを書く必要がある コードが読みにくく、簡潔とは程遠い バースト/ジョブを使用しないオープンソース ソリューションよりも動作が速くありません (場合によっては遅くなります)。 古い Unity コードとうまく適合しない DOTS は本質的にはランタイムであり、古い機能のすべてが DOTS に移植されているわけではありません。これにより、使用の可能性が制限されます。 Unity の ECS に関する結論 多くの欠点のリストからお気づきかと思いますが、ECS は特効薬ではありません。このアーキテクチャ ソリューションには、他のソリューションと同様、長所と短所があり、このアーキテクチャ パターンを使用して開発することを選択した場合は、それらを我慢する必要があります。したがって、プロジェクトで ECS を使用するかどうかの選択は完全にあなた次第です。 このアプローチが好きかどうかを理解するために、少なくとも ECS で小さなプロジェクトを実行してみることを強くお勧めします。 また、ECS に関する多くの質問への回答が記載されているこの を参照することをお勧めします。レポートへのリンク、さまざまな言語のフレームワークのリスト、ECS を使用するゲームやプログラムの例があります。 リポジトリ 私の個人的な観点から見ると、ECS は Unity ゲームを作成するための優れたオプションのように見えます。私個人にとって、開発は楽しいものです。ゲームを開発しているのは、何も壊さずに新しいコードを古いシステムに統合する方法を見つけようとしているわけではありません。 ECS 開発の使いやすさはフレームワークの選択によって大きく影響されるため、さまざまなオプションを試して慎重に選択してください。 私の経験に基づいて、私は ECS (またはそのバリエーション) がインタラクティブ ゲーム開発の未来であると考える傾向があります。 (そしておそらく Epic も) が主な焦点としてこれを選択したからというだけでなく、単に ECS がゲーム開発の文脈において有利な利点を持っているからです。 Unity Technologies そして一般に、これは実践的なアプローチであり、最初はぎこちなく見えますが、長期的には効果があります。