【Unity】ECSのEntityの作成やComponentDataの追加等の操作を別スレッドから実行する
今回はECSのEntityを非同期に作成する方法についてです。
主に打規模なシーンのロード等に使用できそうです。
- Entityの生成や破棄、削除はメインスレッド上で行う
- ExclusiveEntityTransaction
- ExclusiveEntityTransactionを使用しつつEntityManagerも使いたい
- 関連
Entityの生成や破棄、削除はメインスレッド上で行う
ECSでEntityを作成する場合、大抵の場合はEntityManagerを通してEntityを作成したりコンポーネントを追加・削除したりします。EntityManagerは基本的にメインスレッドで動作する設計になっており、ジョブ上からComponentDataやEntityの追加・削除といった処理は実行できません。
EntityCommandBufferを利用してジョブから処理の指示を受ける事は可能ですが、処理を実行するのはメインスレッド上で動作するBarrierSystemです。
ただし、コレらの処理を非同期で行う方法が用意されていないわけでは無いみたいです。ExclusiveEntityTransactionを使用すれば、別のスレッドでEntityを追加したり削除したりも可能です。
ExclusiveEntityTransaction
ExclusiveEntityTransactionは異なるスレッドからEntityを追加・削除する為のAPIです。正確にはEntityManagerを排他(Exclusive)モードにして、他のスレッドからでもアクセス可能にします。
これを実現するために把握しておくAPIは大きく分けて4つあります。
BeginExclusiveEntityTransaction
EntityManagerを排他(Exclusive)モードにするEndExclusiveEntityTransaction
EntityManagerの排他モードを解除するExclusiveEntityTransaction
排他モード時のアクセスに使用する構造体ExclusiveEntityTransactionDependency
ジョブの依存関係を把握するために便利なメンバ変数
基本的に排他モードに入ったらコレを設定しておく
別スレッドで実行する場合、まずBeginExclusiveEntityTransaction
で排他モードに移行すると共にExclusiveEntityTransaction
を入手します。
後は別スレッドでExclusiveEntityTransaction
経由でEntityを追加・削除し、ソレが終わったらEndExclusiveEntityTransaction
で通常のモードに戻すという感じです。
なお、基本的に排他モード時にはEntityManagerが使えなくなります。排他時に使用すれば下のようなエラーが出ます。
InvalidOperationException: Access to EntityManager is not allowed after EntityManager.BeginExclusiveEntityTransaction(); has been called.
一応ExclusiveEntityTransactionを使用すればEntityの追加や削除は可能なのですが、あまり効率的じゃなさそうな気がします(主にロックの頻発で)
これを回避するために、EntityManagerとExclusiveEntityTransactionを同時に使用したい場合には別世界の別EntityManagerを使用します。
ExclusiveEntityTransactionを使用しつつEntityManagerも使いたい
ECSは基本的に同じシステムは一つしか作られませんが、それは同じ世界に一つという但し書きが付きます。ECSではシステムの上にWorldという概念が存在し、Entityは基本的に何処かのワールドに所属します。
このワールドはStaticを用いなければ保持するバッファが独立しておりパラレルな存在です。…この独立はStaticを使うと破壊されます。Staticはまさに世界の破壊者(ディケイド)か世界の架け橋(ディケイド)な訳です。
つまり何が言いたいかというと、この世界のEntityManagerと別世界のEntityManagerは独立しているので、別世界のEntityManagerが排他モードになっても私達の世界のEntityManagerは普通に使えるという事です。
Entityを生産するだけのWorldを用意し、そのWorld運用は別スレッドでのみ行い、その生産物(Entity)をアクティブなWorldが徴収する…という形にすれば、Entityの生産は非同期で行いつつも、普通にEntityの操作が行えるようになるというシナリオです。
C# Job Systemを使用した例
まずはC# Job Systemを使用した場合の例です。複数フレーム跨ぐ可能性があるので、コルーチンによる遅延実行を使用しています。
実行すると下のようにEntityが別スレッドで作られます。下のプロファイリング結果はEntityを毎フレーム38,000程作った結果です。開始と終了時(特にEntity移行時)に負荷がかかりますが、Entity生成それ自体は完全に別スレッドで行われています。
また事前にチャンクやアーキタイプを作っておけば、Entityの移し替えもかなり低コストで行えます。
内容はジョブにExclusiveEntityTransactionを渡してジョブ内でEntityの作成等を行っているだけです。なおジョブシステムの制約上4フレームを跨ぐような物は作れないので、あまり大規模な物は実は作りづらいです。
そのため、最大個数を決めてジョブを繋いで実行…みたいな形でやることになりそうですが、面倒なら単純にスレッドに投げてしまっても良いのかもしれません。
なお非同期ではなく並列で動くか試した所、IJobPralellForは無理そうでしたが、ワールドを複数作って実行したところ普通に動きました。
Async/Awaitを使用した例
排他モードの際には、Async/Awaitでもちゃんと動作しました。
その他
この機能を使用すれば大規模かつ非同期なシーンのロード等が実現できそうですが、どちらかといえばソレはUnity.Entities.Serialization.SerializeUtility.DeserializeWorld
の役目で、本機能はもう少しコンパクトな…例えばプロシージャルに大きな物を作る的な用途なんじゃないかなと予想しています。(中で似たような操作をしていそうですが)
追記:
SubSceneを使いましょう
関連
https://forum.unity.com/threads/serialization-questions.534187/