この記事は、私たちの新しい Data-Oriented Tech Stack(DOTS)に関するいくつかのブログ記事のうちの 1 つです。私たちの現状と今後の方向性について説明します。
前回の記事では、今後の Unity の低水準レイヤーの基本的なテクノロジーとして HPC# と Burst について説明しました。ここでは、私たちのスタックのこのレベルを「ゲームエンジンのエンジン」と呼びたいと思います。このスタックを使えば、誰でもゲームエンジンを作成することができます。私たちには作成できる能力があり、その意思があります。読者の皆さんも同じです。私たちのゲームエンジンに不満がある場合は、自作したり、好みに合わせて変更したりしてください。
私たちが次に作ろうとしているレイヤーは、新しいコンポーネントシステムです。Unity の中心には常に、コンポーネントの概念があります。Rigidbody コンポーネントをゲームオブジェクトに追加すると、ゲームオブジェクトは落下するようになります。Light コンポーネントをゲームオブジェクトに追加すると、光を放出するようになります。AudioEmitter コンポーネントをゲームオブジェクトに追加すると、音を出すようになります。
これは、プログラマーにとってもプログラマー以外の人にとっても、非常に自然な発想であり、直感的な UI を簡単に作ることができます。この発想は、非常に驚くべきことにとてもよく成熟しています。その成熟度合いは、そのまま利用し続けたいほどです。
しかし、Unity のコンポーネントシステムの実装方法はそれほど成熟していません。コンポーネントシステムは、オブジェクト指向の考え方で開発されました。コンポーネントもゲームオブジェクトも、「重い C++」オブジェクトです。これらを作成/破壊するには、id->objectpointer のグローバルリストを変更するために、ミューテックスロックが必要となります。すべてのゲームオブジェクトには名前があります。それぞれが、C++ のオブジェクトを指す C# ラッパーオブジェクトを取得します。その C# オブジェクトは、メモリ内であればどこでも存在する可能性があります。また、C++ オブジェクトもメモリ内であればどこでも存在する可能性があります。キャッシュミスがたくさん発生します。私たちはこのような症状をできる限り軽減しようと試みていますが、一方で、開発者にできることは限られています。
データ指向の考え方をすると、もっとよい対応ができます。Unity の新しいコンポーネントシステムでは、優れた特性をユーザーから見て同じように維持しながら(Rigidbody コンポーネントを追加すると物が落下する)、素晴らしいパフォーマンスと並列処理も実現します。
この新しいコンポーネントシステムが、Unity の Entity Component System(ECS)です。非常に大まかに言うと、現在ゲームオブジェクトを使って行っていることを、新しいシステムではエンティティを使用して行います。コンポーネントは引き続きコンポーネントと呼びます。では、何が違うのでしょうか。データのレイアウトです。
一般的なデータアクセスパターンをいくつか見てみましょう
Unity で従来の方法を使って一般的なコンポーネントを記述すると、以下のようになるでしょう。
class Orbit : MonoBehaviour { public Transform _objectToOrbitAround; void Update() { //この演算は半端なので無視してください。ここでは重要ではありません :) var currentPos = GetComponent<Transform>().position; var targetPos = _objectToOrbitAround.position; GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos) } }
このパターンは繰り返し使われます。あるコンポーネントは、同じゲームオブジェクト上でその他のコンポーネントを 1 つ以上検索し、そのコンポーネントの値の読み取り/書き込みを行わければなりません。
このコードには多くの誤りがあります。
ECS で使用するデータレイアウトでは、これが非常によく見られるパターンであることを認識し、メモリのレイアウトを最適化してこのような操作を高速化します。
ECS は、まったく同じ一連のコンポーネントをメモリ内に集めたエンティティすべてをグループ化します。そのようなセットをアーキタイプと呼びます。アーキタイプの一例は、「Position & Velocity & Rigidbody & Collider」です。ECS によるメモリ割り当ては、16k のチャンク単位で行われます。各チャンクに含まれるのは、単一のアーキタイプのエンティティのコンポーネントデータだけです。
ECS では、実行時に Update メソッドを使用して操作対象の他のコンポーネントを検索するのではなく、Orbit インスタンスごとに次のように静的に宣言すればかまいません。「Velocity、Rigidbody、および Orbit コンポーネントを持つすべてのエンティティに対して、ある操作を実行したい」と。そのようなエンティティを見つけるには、特定の「コンポーネント検索クエリ」に一致するすべてのアーキタイプを検出するだけです。各アーキタイプには、そのアーキタイプのエンティティが格納されているチャンクのリストがあります。そのチャンクをすべてループ処理し、各チャンク内で密度の高いメモリに対して線形ループを実行して、コンポーネントデータの読み取りと書き込みを行います。各エンティティで同じコードを実行するこの線形ループは、Burst でベクトル化を行う格好の対象にもなります。
多くの場合、このプロセスは当然ながら複数のジョブに分割することができ、ECS コンポーネントを操作するコードは 100% 近いコア使用率で実行されます。
上記の処理はすべて ECS によって行われるため、開発者が行う必要があるのは、各エンティティで実行するコードの指定だけです(必要に応じて、チャンクの反復処理を手動で行うこともできます)。
エンティティでコンポーネントを追加/削除したりすると、アーキタイプが切り替わります。該当するコンポーネントを現在のチャンクから新しいアーキタイプのチャンクに移動し、前のチャンクの最後のエンティティをもう一度スワップして「穴埋め」します。
ECS では、コンポーネントデータを使用する目的も静的に宣言します(ReadOnly または ReadWrite)。Position コンポーネントから読み取りしか行わないことを約束する(この約束は検証済み)ことで、ECS はジョブをより効率的にスケジュールできます。Position コンポーネントから読み取りを行う他のジョブも、待機しなくてよくなります。
このデータレイアウトによって、長年にわたって不満であった、ロード時間とシリアル化パフォーマンスに関しても対処できるようになります。大きなシーンの ECS データをロード/ストリーミングするのは、ディスクから raw バイトをロードしてそのまま使用するのとほとんど違いがありません。
『Megacity』デモがスマートフォンに数秒でロードされるのはこのためです。
エンティティでは、現在ゲームオブジェクトで行える処理と同じ処理が可能です。さらに、エンティティは軽量なので、ゲームオブジェクトよりもできることが広がります。実際のところ、エンティティとは何なのでしょうか。このブログ記事のドラフトでは当初、「エンティティはチャンクに格納されます」と説明していましたが、後から「エンティティのコンポーネントデータはチャンクに格納されます」に変更しました。これは、エンティティがたった 32 ビットの整数であることをはっきりさせるうえで、重要な違いです。エンティティに関して、そのコンポーネントのデータ以外に格納したり、割り当てたりするものはありません。エンティティは非常に負荷が軽いので、ゲームオブジェクトが適していなかったシナリオに利用することができます。たとえば、パーティクルシステムの個々のパーティクルにエンティティを使用するようなシナリオです。
次に構築する必要があるレイヤーは、非常に巨大です。その「ゲームエンジン」レイヤーは、「レンダラー」、「物理演算」、「ネットワーク」、「入力」、「アニメーション」などの機能で構成されます。ざっくり言えば、これが私たちの現状です。それら構成要素に取り組み始めていますが、それらを一晩で準備できるわけはありません。
残念に思われるかもしれません。ある意味ではそのとおりですが、別の意味では違います。ECS と、その上に構築されるすべてのものは C# で記述されているため、従来の Unity 内で実行することができます。ECS は Unity 内で実行されるので、ECS 以前の機能を使用する ECS コンポーネントを記述することができます。現時点では、純粋な ECS メッシュ描画システムはありません。しかし、純粋な ECS バージョンが配布されるまでの間、ECS より前の Graphics.DrawMeshIndirect API を実装として使用する ECS MeshRenderSystem を記述することができます。『Megacity』デモでは、まさにこの手法を利用しています。純粋な ECS システムによって、ロード/ストリーミング/カリング/LOD 処理/アニメーションは行われますが、最終的な描画は行われません。
そこで、うまく組み合わせることで対処できます。この対処方法の利点は、すべてのサブシステムの純粋な ECS バージョンが配布されるのを待たなくても、Burst によるコード生成と、ゲームコードでの ECS のパフォーマンスのメリットをすぐに享受できることです。一方、欠点としては、ECS はまだ過渡期にあるため、「2 つの異なる世界を繋ぎ合わせて使用している」というちぐはぐさを感じる可能性があります。
私たちは、ECS HPC# サブシステムのソースコードすべてをパッケージに含めて配布する予定です。これにより、各サブシステムを調査、デバッグ、変更できるほか、どのサブシステムをいつアップグレードするかをより詳細に制御できます。たとえば、Physics サブシステムパッケージだけをアップグレードして、他のサブシステムはアップグレードしないようにできます。
ゲームオブジェクトはなくなりません。ゲームオブジェクトを利用して、今後もずっと素晴らしいゲームを配布することができます。その基本事項はどこにも行きません。
変わるのは、Unity の改善のためにエネルギーを注ぐ先です。徐々に、ゲームオブジェクトの世界ではなく、ECS の世界だけにエネルギーが注がれるようになります。
ECS について説明するときによく指摘される、文句を言いたいのも当然な点は、タイピングする量が非常に多いことです。開発者が、達成しようとしている目的を果たすには、さまざまな定型コードを入力する必要があります。
ほとんどの定型コードを不要にしたり、開発者が意図を表現しやすくしたりするために、多数の改善が行われる予定です。私たちが現在注力している対象は基本的なパフォーマンスであるため、予定されている改善の多くはまだ実装していません。しかし、ECS ゲームコードにたくさんの定型コードが含まれていたり、MonoBehaviour を記述するよりもかなり多くの手間がかかったりしてよい理由はないと考えています。
Project Tiny には、先述の改善事項のいくつかがすでに実装されています(ラムダベースのイテレーション API など)。それについて紹介しましょう。
Project Tiny は、このブログ記事で取り上げているのと同じ C# ECS をベースにして配布されます。私たちにとって Project Tiny は、以下の点で ECS の大きなマイルストーンとなります。
Unity では、DOTS スタックに関するさまざまな職務で人材を募集しています。特に、バーバンクとコペンハーゲンのオフィスで勤務できる方の応募をお待ちしています。詳しくは、careers.unity.com をご覧ください。
Unity Entity Component System と C# Job System フォーラムにもぜひ参加して、実験的機能やプレビュー機能に関するフィードバックをお寄せください。また、フォーラムでは、そのような機能に関する情報を入手することもできます。
Is this article helpful for you?
Thank you for your feedback!