paint-brush
Unity DOTS と ECS の探求: それはゲームチェンジャーですか?@deniskondratev
3,382 測定値
3,382 測定値

Unity DOTS と ECS の探求: それはゲームチェンジャーですか?

Denis Kondratev12m2023/07/18
Read on Terminal Reader

長すぎる; 読むには

この記事では、シンプルで直感的なアーキテクチャを通じてゲーム開発を最適化する Unity のデータ指向テクノロジー スタック (DOTS) とエンティティ コンポーネント システム (ECS) について詳しく説明します。これらのテクノロジーと追加の Unity パッケージにより、高パフォーマンスで効率的なゲーム作成が可能になります。これらのシステムの利点は、Conway のライフ ゲーム アルゴリズムの例を通じて実証されています。
featured image - Unity DOTS と ECS の探求: それはゲームチェンジャーですか?
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item

Unity DOTS を使用すると、開発者は最新のプロセッサの可能性を最大限に活用し、高度に最適化された効率的なゲームを提供できます。これには注目する価値があると私たちは考えています。


Unity がデータ指向テクノロジー スタック (DOTS) の開発を初めて発表してから 5 年以上が経過しました。今回、長期サポート (LTS) バージョンである Unity 2023.3.0f1 のリリースにより、ついに正式リリースが見られました。しかし、なぜ Unity DOTS がゲーム開発業界にとってそれほど重要なのでしょうか?また、このテクノロジーにはどのような利点があるのでしょうか?


こんにちは、みんな!私の名前は Denis Kondratev です。MY.GAMES のUnity開発者です。 Unity DOTS が何なのか、そしてそれが探索する価値があるかどうかを理解したいと思っている場合、これはこの魅力的なトピックを詳しく掘り下げる絶好の機会です。この記事では、まさにそれを説明します。


エンティティ コンポーネント システム (ECS) とは何ですか?

DOTS はその中核として、Entity Component System (ECS) アーキテクチャ パターンを実装しています。この概念を単純化するために、次のように説明しましょう。ECS は、エンティティ、コンポーネント、システムという 3 つの基本要素に基づいて構築されています。


エンティティ自体には、固有の機能や説明がまったくありません。代わりに、これらはさまざまなコンポーネントのコンテナとして機能し、ゲーム ロジック、オブジェクト レンダリング、サウンド効果などの特定の特性をコンポーネントに与えます。


コンポーネントにはさまざまなタイプがあり、独自の独立した処理機能を持たずにデータを保存するだけです。


ECS フレームワークを完成させるのは、コンポーネントを処理し、エンティティの作成と破棄を処理し、コンポーネントの追加または削除を管理するシステムです。


たとえば、「スペース シューター」ゲームを作成する場合、プレイグラウンドには、プレイヤーの宇宙船、敵、小惑星、戦利品など、さまざまなオブジェクトが表示されます。



これらのオブジェクトはすべて、明確な特徴を持たず、それ自体がエンティティであるとみなされます。ただし、さまざまなコンポーネントを割り当てることで、独自の属性を与えることができます。


これらすべてのオブジェクトがゲーム フィールド上の位置を持っていることを考慮して、オブジェクトの座標を保持する位置コンポーネントを作成できることを示します。さらに、プレイヤーの宇宙船、敵、小惑星については、健康コンポーネントを組み込むことができます。オブジェクトの衝突の処理を担当するシステムが、これらのエンティティの健全性を管理します。さらに、敵のタイプ コンポーネントを敵にアタッチして、敵の制御システムが割り当てられたタイプに基づいて敵の動作を制御できるようにすることができます。


この説明は単純化された初歩的な概要を示していますが、実際はもう少し複雑です。それにもかかわらず、私は ECS の基本的な概念は明確であると信じています。それはさておき、このアプローチの利点を詳しく見てみましょう。

エンティティ コンポーネント システムの利点

エンティティ コンポーネント システム (ECS) アプローチの主な利点の 1 つは、それが促進するアーキテクチャ設計です。オブジェクト指向プログラミング(OOP) には、継承やカプセル化などのパターンに関する重要な遺産があり、経験豊富なプログラマーでも、開発の最中にアーキテクチャ上の間違いを犯し、長期プロジェクトでリファクタリングやロジックのもつれにつながる可能性があります。


対照的に、ECS はシンプルで直感的なアーキテクチャを提供します。すべてが自然に独立したコンポーネントとシステムに分類されるため、このアプローチを使用すると理解と開発が容易になります。初心者の開発者でも、最小限のエラーでこのアプローチをすぐに理解できます。


ECS は複合アプローチに従い、複雑な継承階層の代わりに分離されたコンポーネントと動作システムが作成されます。これらのコンポーネントとシステムは簡単に追加または削除できるため、エンティティの特性と動作を柔軟に変更できます。このアプローチにより、コードの再利用性が大幅に向上します。


ECS のもう 1 つの重要な利点は、パフォーマンスの最適化です。 ECS では、データは連続的かつ最適化された方法でメモリに保存され、同一のデータ型が互いに近くに配置されます。これにより、データ アクセスが最適化され、キャッシュ ミスが減少し、メモリ アクセス パターンが改善されます。さらに、個別のデータ ブロックで構成されるシステムは、異なるプロセス間での並列化が容易であり、従来のアプローチと比較して優れたパフォーマンスの向上につながります。

Unity DOTS のパッケージを調べる

Unity DOTS には、Unity に ECS コンセプトを実装する Unity Technologies によって提供される一連のテクノロジーが含まれています。これには、ゲーム開発のさまざまな側面を強化するために設計されたいくつかのパッケージが含まれています。そのうちのいくつかを説明しましょう。


DOTS の中核はEntitiesパッケージであり、これにより、使い慣れた MonoBehaviours および GameObjects から Entity Component System アプローチへの移行が容易になります。このパッケージは、DOTS ベースの開発の基礎を形成します。


Unity Physicsパッケージは、ゲームで物理を処理するための新しいアプローチを導入し、並列計算を通じて驚くべき速度を実現します。


さらに、 Havok Physics for Unityパッケージを使用すると、最新の Havok Physics エンジンとの統合が可能になります。このエンジンは、高性能の衝突検出と物理シミュレーションを提供し、ゼルダの伝説: ブレス オブ ザ ワイルド、ドゥーム エターナル、デス ストランディング、モータル コンバット 11 などの人気ゲームを強化します。


Death Stranding は、他の多くのビデオ ゲームと同様に、人気のある Havok Physics エンジンを使用しています。


Entities Graphicsパッケージは、DOTS でのレンダリングに重点を置いています。これにより、レンダリング データの効率的な収集が可能になり、ユニバーサル レンダー パイプライン (URP) やハイ デフィニション レンダー パイプライン (HDRP) などの既存のレンダー パイプラインとシームレスに連携できます。


もう 1 つ、Unity は Netcode と呼ばれるネットワーク テクノロジの開発にも積極的に取り組んでいます。これには、低レベルのマルチプレイヤー ゲーム開発用の Unity Transport、従来のアプローチ用の Netcode for GameObjects、DOTS 原則に準拠した注目すべきUnity Netcode for Entitiesパッケージなどのパッケージが含まれています。これらのパッケージは比較的新しいものであり、今後も進化し続けるでしょう。

Unity DOTS 以降のパフォーマンスの強化

DOTS に密接に関連するいくつかのテクノロジーは、DOTS フレームワーク内およびそれを超えて使用できます。 Job Systemパッケージは、並列計算を行うコードを記述するための便利な方法を提供します。これは、作業をジョブと呼ばれる小さな塊に分割することを中心に展開され、ジョブは独自のデータに対して計算を実行します。ジョブ システムは、効率的に実行するために、これらのジョブをスレッド間で均等に分散します。


コードの安全性を確保するために、ジョブ システムは blittable データ型の処理をサポートしています。 Blittable データ型は、マネージド メモリとアンマネージド メモリで同じ表現を持ち、マネージド コードとアンマネージド コード間で受け渡されるときに変換を必要としません。 blittable 型の例には、byte、sbyte、short、ushort、int、uint、long、ulong、float、double、IntPtr、UIntPtr などがあります。ブリッタブルなプリミティブ型の 1 次元配列およびブリッタブルな型だけを含む構造もブリッタブルであるとみなされます。


ただし、blittable 型の変数配列を含む型は、それ自体 blittable とは見なされません。この制限に対処するために、Unity はジョブで使用するためのアンマネージド データ構造のセットを提供するCollectionsパッケージを開発しました。これらのコレクションは構造化されており、Unity メカニズムを使用してアンマネージド メモリにデータを保存します。 Disposal() メソッドを使用してこれらのコレクションの割り当てを解除するのは開発者の責任です。


もう 1 つの重要なパッケージはBurst Compilerです。これをジョブ システムとともに使用すると、高度に最適化されたコードを生成できます。 Burst コンパイラにはコードの使用に一定の制限がありますが、パフォーマンスが大幅に向上します。

ジョブ システムとバースト コンパイルによるパフォーマンスの測定

前述したように、ジョブ システムとバースト コンパイラは DOTS の直接のコンポーネントではありませんが、効率的で高速な並列計算のプログラミングに貴重な支援を提供します。実際の例を使用してその機能をテストしてみましょう。 Conway のライフ ゲーム アルゴリズム。このアルゴリズムでは、フィールドはセルに分割され、各セルは生きているか死んでいる可能性があります。各ターン中に、各セルの生きている隣接セルの数をチェックし、それらの状態が特定のルールに従って更新されます。



従来のアプローチを使用したこのアルゴリズムの実装は次のとおりです。


 private void SimulateStep() { Profiler.BeginSample(nameof(SimulateStep)); for (var i = 0; i < width; i++) { for (var j = 0; j < height; j++) { var aliveNeighbours = CountAliveNeighbours(i, j); var index = i * height + j; var isAlive = aliveNeighbours switch { 2 => _cellStates[index], 3 => true, _ => false }; _tempResults[index] = isAlive; } } _tempResults.CopyTo(_cellStates); Profiler.EndSample(); } private int CountAliveNeighbours(int x, int y) { var count = 0; for (var i = x - 1; i <= x + 1; i++) { if (i < 0 || i >= width) continue; for (var j = y - 1; j <= y + 1; j++) { if (j < 0 || j >= height) continue; if (_cellStates[i * width + j]) { count++; } } } return count; }


計算にかかる時間を測定するために、プロファイラーにマーカーを追加しました。セルの状態は、 _cellStatesという 1 次元配列に格納されます。最初に一時的な結果を_tempResultsに書き込み、計算が完了したらそれらを_cellStatesにコピーして戻します。最終結果を_cellStatesに直接書き込むと後続の計算に影響を与えるため、このアプローチが必要です。


1000x1000 セルのフィールドを作成し、プログラムを実行してパフォーマンスを測定しました。結果は次のとおりです。



結果からわかるように、計算には 380 ミリ秒かかりました。


次に、ジョブ システムとバースト コンパイラを適用してパフォーマンスを向上させましょう。まず、Conway のライフ ゲーム アルゴリズムの実行を担当するジョブを作成します。


 public struct SimulationJob : IJobParallelFor { public int Width; public int Height; [ReadOnly] public NativeArray<bool> CellStates; [WriteOnly] public NativeArray<bool> TempResults; public void Execute(int index) { var i = index / Height; var j = index % Height; var aliveNeighbours = CountAliveNeighbours(i, j); var isAlive = aliveNeighbours switch { 2 => CellStates[index], 3 => true, _ => false }; TempResults[index] = isAlive; } private int CountAliveNeighbours(int x, int y) { var count = 0; for (var i = x - 1; i <= x + 1; i++) { if (i < 0 || i >= Width) continue; for (var j = y - 1; j <= y + 1; j++) { if (j < 0 || j >= Height) continue; if (CellStates[i * Width + j]) { count++; } } } return count; } }


CellStatesフィールドに[ReadOnly]属性を割り当て、任意のスレッドから配列のすべての値に無制限にアクセスできるようにしました。ただし、 [WriteOnly]属性を持つTempResultsフィールドの場合、 Execute(int Index)メソッドで指定されたインデックスを介してのみ書き込みを行うことができます。別のインデックスに値を書き込もうとすると、警告が生成されます。これにより、マルチスレッド モードで作業する際のデータの安全性が確保されます。


ここで、通常のコードからジョブを起動しましょう。


 private void SimulateStepWithJob() { Profiler.BeginSample(nameof(SimulateStepWithJob)); var job = new SimulationJob { Width = width, Height = height, CellStates = _cellStates, TempResults = _tempResults }; var jobHandler = job.Schedule(width * height, 4); jobHandler.Complete(); job.TempResults.CopyTo(_cellStates); Profiler.EndSample(); }


必要なデータをすべてコピーした後、 Schedule()メソッドを使用してジョブの実行をスケジュールします。このスケジューリングでは計算がすぐに実行されるわけではないことに注意することが重要です。これらのアクションはメイン スレッドから開始され、実行はさまざまなスレッドに分散されたワーカーを通じて行われます。ジョブが完了するのを待つには、 jobHandler.Complete()を使用します。そうして初めて、取得した結果を_cellStatesにコピーして戻すことができます。


速度を測定してみましょう:



実行速度はほぼ 10 倍に向上し、実行時間は約 42 ミリ秒になりました。プロファイラー ウィンドウでは、ワークロードが 17 人のワーカーに分散されていることがわかります。この数は、テスト環境のプロセッサ スレッド数 (10 コアと 20 スレッドを備えたインテル® Core™ i9-10900) よりわずかに少ないです。コアの数が少ないプロセッサでは結果が異なる場合がありますが、プロセッサの能力を最大限に活用することができます。


しかし、それだけではありません。Burst Compiler を利用するときが来ました。Burst Compiler はコードを大幅に最適化しますが、特定の制限があります。 Burst Compiler を有効にするには、 [BurstCompile]属性をSimulationJobに追加するだけです。


 [BurstCompile] public struct SimulationJob : IJobParallelFor { ... }


もう一度測定してみましょう:



結果は最も楽観的な予想をも上回りました。速度は初期結果と比較してほぼ 200 倍向上しました。現在、100 万セルの計算時間は 2 ミリ秒以下です。プロファイラーでは、バースト コンパイラーでコンパイルされたコードによって実行される部分が緑色で強調表示されます。

結論

マルチスレッド計算の使用が必ずしも必要なわけではなく、バースト コンパイラーの利用が常に可能であるとは限りませんが、プロセッサ開発ではマルチコア アーキテクチャに向かう共通の傾向が観察できます。これは、私たちが彼らの力を最大限に活用する準備ができている必要があることを意味します。 ECS、特に Unity DOTS は、このパラダイムと完全に一致しています。


少なくとも Unity DOTS は注目に値すると思います。すべてのケースに最適なソリューションというわけではありませんが、ECS は多くのゲームでその価値を証明できます。


データ指向およびマルチスレッドのアプローチを備えた Unity DOTS フレームワークは、Unity ゲームのパフォーマンスを最適化するための大きな可能性を提供します。 Entity Component System アーキテクチャを採用し、Job System や Burst Compiler などのテクノロジを活用することで、開発者は新しいレベルのパフォーマンスとスケーラビリティを実現できます。


ゲーム開発が進化し続け、ハードウェアが進歩するにつれて、Unity DOTS を採用する価値がますます高まります。これにより、開発者は最新のプロセッサーの可能性を最大限に活用し、高度に最適化された効率的なゲームを提供できるようになります。 Unity DOTS はすべてのプロジェクトにとって理想的なソリューションではないかもしれませんが、パフォーマンス主導の開発とスケーラビリティを求める人々にとっては間違いなく大きな可能性を秘めています。


Unity DOTS は、パフォーマンスを強化し、並列計算を可能にし、マルチコア処理の将来を受け入れることにより、ゲーム開発者に大きな利益をもたらす強力なフレームワークです。最新のハードウェアを最大限に活用し、Unity ゲームのパフォーマンスを最適化するために、その導入を検討および検討する価値があります。