【Unity】ECSでチャンク単位のバッチ処理を実現するChunk Iteration、それとEntityQuery
最近のECS界隈で特に理由もなくChunk Iterationを採用されている事をよく見るので、今回そのChunk Iterationついて書いてみます。
ComponentDataの組み合わせの爆発
ECSは基本的に「特定のComponentDataの組み合わせ」で処理する対象を判定しています。Interfaceのような特定の型ではなく組み合わせです。
例えば下の図では「人力で移動するシステム」と「エンジンで動くシステム」があり、荷物のComponentDataを保持している場合には荷物に関する処理が追加されます。
System要求するComponentDataを持つEnittyが処理されていきます。この要求は「全てのブツを所持している(AND)」及び「該当のブツを所持していない(NOT)」です。
さて上の図で少し気になるのが「人力」と「エンジン」のシステムです。基本的に動き自体は人力だろうとエンジンだろうと本質的には同じ「移動システム」です。コードで比較すると、多分殆ど同じようなコードになります。
ここで動力の種類の数だけSystemや EntityQuery
を用意すると同じようなクエリーが大量に生成されることになるので余り宜しくないです。Interfaceで抽出出来るなら楽なのですが、ComponentData単位でしか取得出来ません。
そこでECSでは「OR」を新しく追加して大雑把にEntityの組み合わせを取得、処理内で行うべき処理を割り振る機能を追加しました。
それがEntityArchetypeQuery、そしてChunk Iterationという機能です。
Chunk Iterationという回避方法
考え方は割と単純です。
EntityQuery
を要求する際に緩い条件で検索をかけて、内包するComponentDataの組み合わせに応じて処理を切り替えを行うだけです。
これで要求するQueryの種類が減らせます。
検索に使用できるのはこの3種類
- AND(全てのブツを含む)
- OR(指定したブツが含まれる)
- NONE(指定したブツを含まない)
例えば下の場合、今までは「人力&タイヤ」OR「エンジン&タイヤ」の組み合わせで取得していましたが、これを「(人力 OR エンジン) & タイヤ」で取得します。そして処理を行う際、保持するComponentDataが「人力」か「エンジン」かを確認し処理を切り替えます。
このように複数のComponentDataを許容すると処理を行う際に1Entity毎に「実行する処理の内容を切り替える」事になりそうですが、そこらへんは大丈夫になっています。
ECSでは基本的にComponentDataの組み合わせ(archetype)毎にまとめて配置されています。この塊はチャンク(Chunk)と呼ばれます。同じチャンク内なら全て同じComponentDataの組み合わせであることが保証されています。
要するに、処理の切替えはEntity毎ではなくチャンク毎です。
ということで、このChunkの処理はじめに指定のComponentDataを持っているかチェックし、処理を切り替えを行っていくことで効率的なバッチ処理を実現しています。
これ逆を言えば、ORを使用しないのであれば(付属するComponent次第で処理を切り替えしないならば)EntityQueryを素直に使ったほうが多分良いです。
また効率で言えばIJobProcessComponentDataを使うのが良いそうです。
コードを書いてみる
詳しい記述方法は 【Unity】Entity Component System入門(その2)【2018.2】 - Qiita で紹介されています。この書き方は色々と応用が効きそうですが正直この書き方は苦痛なので自分はIJobChunk
を使用して楽します。
まずEntityを取得する部分ですが[Inject]
ではなくGetEntityQuery
を使用します。その際にEntityArchetypeQuery
を使用すると取得時にORを指定できます。
query = GetEntityQuery(new EntityQueryDesc() { All = new ComponentType[] { ComponentType.ReadWrite<Translation>() }, Any = new ComponentType[] { ComponentType.ReadOnly<ManPowerData>(), ComponentType.ReadOnly<EngineData>() }, None = System.Array.Empty<ComponentType>() });
次はJob側の定義です。
ジョブの取得にはIJobChunk
を使用します。こちらはQueryを渡すと、Queryが参照するチャンクの数だけ並列で処理を実行してくれます。
return new MyJob { ... }.Schedule(query, inputDeps);
取得したチャンクにHas
を使用して、指定するチャンクが任意のComponentDataを保持しているかチェックします。
あとはComponentDataの有無に応じて処理を分けます。
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var array = chunk.GetNativeArray(typePosition); if (chunk.Has(typeManPower)) ProcessManPower(array); else if (chunk.Has(typeEngine)) ProcessEngine(array); }
あとはジョブを実行します。
IJobChunk
で実行する場合、EntityQuery
を渡せば良いのでかなり楽が出来ます。
この時、チャンクのComponentDataにアクセスするためにGetArchetypeChunkComponentType
を使用します。これはEntityManagerにも同名のAPIがありますが、必ずComponentSystem
のものを使用します。
自分はこのトラップに引っかかり「InvalidOperationException: The previously scheduled job」を散々見る事になりました。
全文
感想
Chunk Iterationでした。
利用するシーンの多い良い機能ではありますが、無条件で使うべきものでもないので、状況次第で使っていくのが良さそうです。
追記:低レベルAPI扱いとなりました。
関連
Chunk Iterationについてディープな解説をしている記事です。