今回はC# Job Systemと並びよく紹介されるEntity Component System…通称ECSについてです。
実行可能なビルドと簡単ではないプロジェクトが公開されているので、ついでに紹介していきます。
ただ経験的に、UnityのECSの「データ指向設計」と「ECS」と「Unityの制約」を一気に聞くと意味不明になるので、分けます。
まずはECSの概念から。
ECSとは何か?
まずECSとは何か?ECSは 何か凄い機能 というよりは、コンポーネント志向に変わる新しいアーキテクチャパターンです。
要するに、(目標としては)GameObject / Componentを差し替えるモノです。
ECSはGameObject / MonoBehaviourと較べて、膨大なオブジェクトを処理しやすい設計になっています。ECSはUnityのコンポーネント志向と用語的には似ており、ECSはUnity的な視点から言えば「コンパクトな GameObject & Monobehaviourのようなモノ」とも言えなくはないです。
ECSの特徴は、ちゃんと実装するとデータ指向設計になるので、ハードウェアに優しく高性能な実装になる点、及びC# Job Systemに処理を渡しやすく並列処理で高速化しやすい点が挙げられます。
またBurstコンパイラと合わせて使用することでパフォーマンスが爆発的に向上します。
Unite Austin Technical Presentation。膨大なオブジェクトを動かしています。
ただ現状は本当に基本的な機能しか提供されていないので、現状使うのに非常に大変な思いをします(ちゃんとした公開はまだ先)。例えばRigidbody SystemやAnimationSystemが無く、というかマトモなRendererすらありません。殆どの実装を自分で行う必要があります。
とは言え、機能が揃えばGameObjectよりパフォーマンス面で良いので、今のうちに覚えておいても損はないです。
ちなみにCPU使用率を100%近くにしたりするのは、どちらかと言えばC# Job Systemの力で、量を増やすのはECSの力です。Burstは、制約の元パフォーマンスを大きく向上させます。
ECSの用語
ECSは幾つか用語があります。
例えばEntity、Component Data、ComponentSystem、Groupの4つです。
- Entity:入れ物のようなもの
- ComponentData:Entityに格納するデータ(処理は含まない)
- ComponentSystem:処理
- Group:ComponentSystemが動作するために要求するComponentData一覧
他にも色々とありますが、とりあえず覚えておくべきはこの4つです。
物凄い厄介な事に、本家ECSの用語と微妙に違います。
- Entity = Entity
- ComponentData = Component
- ComponentSystem = System
この用語ですが、UnityのGameObject /Componentを差し替えるものだけあって、少し近いところがあります。物凄い大雑把に言ってしまえば、以下のモノと一致します。
ECSはコンポーネントの組み合わせで動作する
UnityのGameObjectとComponentは、GameObject(モノ)にMonoBehaviour(フルマイ)を与えて動作を定義します。審判が居て動きを指示する事もあります(というか、自己完結してる方が少ない)
また特定のコンポーネントが基本的にキャラクターを動かします。
例えばMoveというコンポーネントが動かす場合、MoveコンポーネントがGetComponentでTransformにアクセスしてオブジェクトを動かす訳です。
一方、ECSの動作はこんな感じです。
- ECSは振る舞い(System)とデータ(Component)は別々に実装される。
- モノ(Entity)にデータ(Component)が複数種類格納される。
- モノ(Entity)に格納されているデータ(Component)が、Groupの要求するデータ一覧を満たしていれば振る舞い(System)が呼ばれる。
MonoBehaviourはデータと処理が1:1でしたが、ECSの場合はComponentDataの組み合わせにたいして処理が付きます。
例えばTransformとMoveというComponentDataをGroupを通してSystemが要求し、その二つが揃ったEntityを全て呼び出して一括で処理します。
処理する際にはTransformとMoveというコンポーネントの情報がSystemに直接渡されます。
こんな感じで、Entityが持つComponentData、ComponentDataを要求するSystem(及びGroup)の組み合わせで、どういった処理が呼ばれるのか決まります。
また参照するEntityの指定等は無く、全てのEntity(正確にはアーキタイプと呼ばれるEntityの雛形)をチェックし、一致するEntityは全てSystemで一括処理されます。
どういった処理が動くのかは、下の表のようにすれば納得しやすいかもしれません。
何にせよ、ECSはComponentDataの組み合わせで動作が定義される…という事です。
補足
「分からん」「さっぱりわからんw」というコメントが付いたので、もう少し説明を追加します。「違う、そうじゃない」と思う場合は、まだ説明してない内容かもしれませんが、下のコメントに書いてもらえれば補足できるかもしれません。
それでもワカランと思った貴方!大丈夫、たぶん自分も含めてほとんどの人は一発で理解出来ません。
さて、まずEntityですが、これはEntityManagerから生成されます。Entityは最初からComponentDataを含めた状態で生成することが推奨されますが、後から足したり減らしたりもできます。
EntityはGameObjectと同じように、ゲーム内に沢山あります。そういった意味でも、Entity ≒ GameObjectと言えます。
SystemはEntity一つひとつに対して処理を実行(注入)します。イメージとしては、ベルトコンベアーでEntityが流れてきてアームで処理するようなイメージです。
Systemが二つの場合は、Groupが一致してれば全て動きます。これは一つのスレッドで動いてるかもしれませんし、C# Job Systemで並列化されている事もあります。
例題1
例えばシンプルに、ひたすら前進して障害物にあたったら右に曲がる…という機能を持ったEntityを考えてみます。
このEntityの表現のために必要なComponentDataを考えると、以下の通りです。
- 座標
- 前方の障害物との距離
- 移動方向
さて、このパラメータで動きを実現しようと思ったら、以下のような実装が考えられます。
- 「座標」と「移動方向」から「前方障害物との距離」を求める
- 「前方の障害物」が一定以下なら、「移動方向」を変える
- 「移動方向」の方向に「座標」を動かす
これらをシステムに落とし込みます。
距離をチェックして、向きを更新し、座標を更新するシステムを実装します。
これで、「座標」と「前方の障害物との距離」「移動方向」を持ったEntityが生成されると、そのEntityは「ひたすら前進して障害物にあたったら右に曲がる」動作が実行されるようになります。
例問題 2
では今度は「指定座標の方向に向かって移動する」ようなEntityが欲しくなったら?
今度のEntityは以下のようなComponentDataを持ちそうです。
- 座標
- 指定座標
- 移動方向
システムはこんな感じ。
- 「指定座標」と「座標」から「移動方向」を求める
- 「移動方向」の方向に「座標」を動かす
さて、この2番めのシステムですが、例題1の3番目のシステムと全く同じものが使われています。要求するグループが同じなので、共通化出来ている訳です。
Systemは細かくわければ広い範囲で使い回しが効きますが、実装が色々と面倒くさくもなります。その辺りはバランスを考える必要がありそうです。
ComponentDataの組み合わせについての補足
ComponentDataの組み合わせですが、実は複数のGroupを設定することが出来ます。
例えばGameObjectEntity(唯一)とUnit Entity(沢山)の組み合わせで作成し、GameManagerの状況に応じてUnitの動作を一気に止めたりとかを期待出来ます。
またComponentDataの中に特に何も含まない…つまり定義だけのComponentDataをGroupに指定することもあるクマ。
例えば「パンダ」や「クマ」に特別な処理をしたい場合、PandaComponentDataが登場することもあるクマ。
それとComponentData、EntityにAddしたりRemoveしたりSet(Override?)したりできます。 そこらへんもますますGameObjectっぽい
ComponentSystemは誰が呼んでるの?
ここまで書くと少し気になるのが「Systemを誰が呼び出しているのか?」です。
これはその内ちゃんと書く予定ですが、雑に言えば「定義したら勝手に呼ばれる」ようになります。強いて言うなら"世界"が呼び出してます。
また「誰が呼び出したか」で気になるのは呼び出し順ですが、その辺りもコントロールできるようになっています。
単純に◯◯システムの後…以外でも色々とありますが、またそれは後ほど。
もう一つ、勝手に呼び出されるならON/OFFはどうなってるの?という事ですが、System自体はGameObjectのComponent等として呼び出す必要は無く、マッチングするEntityが一つでも存在すればRunning状態になります。
逆に無ければ動かないみたいです。
OFFにしたいなら、GameManagerとか作って状態を参照するような形になるのかもしれません。またSystem自体は実はフィールド持てます。イベントとかも(あまり巨大なモノは悪影響があると思いますが…)
Entityは誰が作るべき?
次に気になるのはEntityは誰が作るべきか?という話ですが、シーン内で完結させたいならSceneのMonoBehaviourから、それ以外ならRuntimeInitializeOnLoadMethodといった形になると思います。
これには理由があって、実はEntityはSceneとは紐付けられてないので、シーンが変化してもデータは残り続けます。
なので、後片付けが必要ならMonoBehaviourのイベントで、それ以外なら別にどうでも良いよ♪という話です。
詳しくは今度ですが、Entityを作る場合にはEntityManager.Instantiateを使います。GameObject.Instantiateと違って1000個一気に作る…みたいな事も出来るので、少しだけ良いです(しかも生成コストが軽い)
要するにUnityのSceneエディターを使えない訳ですが、ハイブリットと呼ばれる「現行コンポーネントとECSの組み合わせ」な使い方も提供されています。普通のコンポーネントをComponentDataとして登録する方法で、Inspectorから色々とパラメータをいじれるようになります。
ただ、「逆に使いにくい」「わかりにくすぎる」「中途半端」「キモイ」等々、すこぶる評判が悪いので、もしかしたら完全に作り直される気もします。
Entity同士のメッセージング
Entity同士のメッセージのやりとりですが、出来ません。
特定のEntityにメッセージを送りたい場合、Entityの持つComponentDataの中身を上書きし、Systemで上書きされたデータを判定する的な処理を実装します。
(例えばDamageComponentDataというデータがあり、誰かがソレに入力、DamageSystemで反映させる)
System同士のメッセージは、実は普通に可能です。フィールドも持っても良いです。あまり良くないかもしれませんが、まぁ多分イケます。
補足
ECSはオブジェクト指向では無いので、OOPの文脈で理解しようとすると上手く理解できません(経験談
「何となくフワっとしか理解できない」と思う場合は、データ指向設計を先に見ると良いかもしれません。これはECSを利用する分には不要ですが、スッキリします。
次は実際の使い方について
つづく