テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】ECSのComponentSystem実行順をコントロールする

今回はECSのComponentSystemも実行順番をコントロールする方法についてです。

 

 

ComponentSystemの実行順番は非常に重要

ECSは無数に存在するComponentDataをComponentSystemで一括制御する事で実現しています。ソレが単なる技術デモでComponentDataとComponentSystemが一対一の関係なら大した問題にはなりません。

問題になるのは複数のComponentSystemで複数のComponentDataを制御する場合で、その場合ComponentSystemの実行順によって結果が変わってきます

例えば「Entity生成」「Entityの向きをターゲットへ変更」「Entityの向きに従って前方をチェック」「当たっているならEntityを生成」といったシステムを考えてみます。

f:id:tsubaki_t1:20180904191205j:plain

これが順番通り動くなら問題はないのですが、「Entityの向きをターゲットへ変更」の前に「Entityの向きに従って前方をチェック」が入ると、最初に向いていた方へチェックが入る事になり、思ったとおりの結果を得られなくなります。

f:id:tsubaki_t1:20180904191241j:plain

 

実行順の確認方法

まず実行順の確認方法です。
使っていて気づいたのですが、EntityDebuggerのSystem並び順は実行順で並ぶみたいです。
例えば下の場合、SampleSystem1~3を用意したのですが、実行してみると実行順はSampleSystem 2、SampleSystem 3、SampleSystm1の順でした。

f:id:tsubaki_t1:20180904175105j:plain

f:id:tsubaki_t1:20180904175211j:plain

まぁ一番確実なのはProfilerで順番を見る事ですが、処理時間が短いと探すのが大変なので、多分大丈夫だろう程度で使えそうです。

 

特定のComponentSystemの前に実行する、後に実行する

ComponentSystemはScript Execution Orderのような機能はありません。その代り、UpdateAfterUpdateBeforeというAttributeが使えます。

 このAPIComponentSystemを指定したシステムより前もしくは後に実行するように指定するというものです。PlayerLoopに登録する時、依存関係を見て実行順を作ってくれるとか云々。
下のコードの場合、System1よりSystem2が先に実行されるようになります。

gist.github.com

 なお、実はこのUpdateAfterUpdateBeforePlayerLoopのAPIにも使用できます。例えば下のように記述すると、Updateの前後に処理を挟むことも出来ます。

gist.github.comf:id:tsubaki_t1:20180904180713j:plain

なお実行のタイミングによってEntityDebuggerの表示される場所が微妙に変わる事があります。例えば下のコードでは[UpdateBefore(typeof( UnityEngine.Experimental.PlayerLoop.Initialization))]と記述した所、所属するグループがInitializationに変化しました。

f:id:tsubaki_t1:20180904180951p:plain

 

UpdateInGroupで処理のタイミングを大まかに纏める

次にUpdateAfterを特定のシステムではなく、システムをまとめた「グループ」を指定してみます。これは複数人で開発する時に重要な要素です。

例えば[A][B][C]という3つのシステムがあり、この処理後に何かをしたい場合です。この場合はUpdateAfter(c)で良いです。
では[A][B][C]に追加で[D]というシステムが増え、処理結果は[D]以降に取らなくてはいけなくなった場合、チーム全体にDというシステムが増えた事を共有する必要が出てきます。

f:id:tsubaki_t1:20180904183546j:plain

そこでグループを使用します
[A][B][C]を纏めてグループ:[G]に登録しておきます。そうすると他の人はABCの最後のシステムを把握せずともUpdateAfter(G)で良くなります。逆に「前」で実行する場合もUpdateBefore(G)で良くなります。

f:id:tsubaki_t1:20180904191658j:plain

gist.github.com上のコードを実行すると、ABCの前にX1、後にX2が実行されます。新しいシステムを追加しても、グループで囲ってる範囲に入り込むことはありません。
なお、Xをグループに紛れ込ませるとバグります。

f:id:tsubaki_t1:20180904185243j:plain

関連

Documentation for system update order · GitHub

【Unity】エディター操作でシーン間参照を実現する guid-based-reference

Unityでは複数のシーンを同時に編集することは可能ですが、シーン間の参照は出来ません。今回はソレをGUIDベースでなんとかするアプローチについてです。

シーン間参照(Multi Scene Reference)が出来ない

基本的にUnityのSceneというシステムは、別のシーンへのアクセスは出来ません。そのため、スクリプトを利用して参照先を取得します。例えば

  • 現在読込中のシーン全体にFindを実行
  • Staticなフィールドに格納
  • 特定のオブジェクトに登録し、マッチング
  • ユニークなアセットに格納

といった感じです。実際、様々なフレームワークは存在しますが、大抵の場合は上のどれかに行き着くという認識です。

 

シーン間参照がやりたくなるケース

ここまでして得たい他シーンの情報とは何でしょう。それが○○マネージャーといったオブジェクトを制御するユニークな存在であれば、staticなフィールドに突っ込んでも、まぁ何とかなります。

問題になるのが、シーンから別のシーンにアクセスするようなギミックの場合です。例えばScene1のライトをScene2で制御しようと思うと、これが非常に面倒くさい。
Staticのようにユニークなオブジェクトとして格納する訳にはいきません。

f:id:tsubaki_t1:20180904010641j:plain

こういった場合に強力な解決策になりそうなのが、Guid Based Referenceです。

 

GUID BASED REFERENCE

guid-based-referenceはオブジェクトのシーン間参照を実現するために便利なアセットです。Unity Companion Licenseで提供されています。

github.com

これは、任意のオブジェクトにGUID(Globally Unique Identifier:ユニークなID)を発行し、GUIDを基点にオブジェクトの参照を行うというアプローチです。インスタンスIDはちょくちょく変わるので、変更しないIDを追加で割り振る事で実現しています。
なんでFileIDじゃ駄目だったんでしょう。

ID管理はコンテンツカタログのように全IDを事前に保存しているタイプではなく、オブジェクトが生成されたタイミングで共有スペースに書き込むタイプです。
つまりロードされていないオブジェクトは参照出来ませんが、参照先と参照元のオブジェクトが揃えば利用可能になります。

f:id:tsubaki_t1:20180904013958g:plain

なおお察しの通り、この機能は「GUID Manager(シングルトン)に全部の参照を突っ込む」パターンです。

 

使い方

参照される対象となるGameObjectにGuidComponentを追加します。
これでオブジェクトにユニークなIDが割り振られます。

f:id:tsubaki_t1:20180904014448j:plain

参照する側のコードを用意します。今回はオブジェクトが接触したらアクティブになる…的なやつです。
同じシーンなら[SerializeField]GameObject target;と書く所ですが、[SerializeField] GuidReference target;と記述します。

gist.github.com

後はGuid Componentを追加したオブジェクトをドラッグ&ドロップしてやれば、オブジェクトが登録されてアクセス出来るようになります。
また設定時「どのシーンのオブジェクトを参照しているのか」が表示されているので、多少混乱を抑えられます。

f:id:tsubaki_t1:20180904015224g:plain

なお、取得できるのはGameObjectだけです。ジェネリックも使えないので、対象がその点に注意です。
また内部的にGameObjectをキャッシュしてくれてるのでGUIDで毎回検索する訳ではありませんが、取れるのがGameObjectのため必然的にGetComponent連打することになります。毎フレーム動かすような物はキャッシュしておくと色々と良いです。

 

 

関連

この機能を作るに当たって得られた幾つかのTipsが紹介されています。

blogs.unity3d.com似たような違う機能

tsubakit1.hateblo.jp共有するのがスコアの場合

tsubakit1.hateblo.jpオブジェクトへ直接参照するんじゃなくて、一旦ScriptableObjectを通せば…というアプローチ。

tsubakit1.hateblo.jp

baba-s.hatenablog.com

【Unity】Cinemachineの挙動を拡張してみる

 今回はCinemachineの挙動を拡張してみました。

 

 

Cinemachineの挙動を拡張

CinemachineはLookAtとFollow、カメラの向きと位置を各々の要素が補完してカメラワークを作ります。
この挙動は基本的な機能として存在する幾つかの項目(例えばComposerやTransposer)だけでなく、自分で拡張することも出来ます。
なので、今回はとあるついでに拡張してみました。

f:id:tsubaki_t1:20180901231001j:plain

 

 対象をX軸に追跡するだけのカメラ

カメラを横に振るだけのカメラを作ってみました。
Composerのように特定の位置にキャラクターを配置する等は出来ませんが、オフセットで角度を持って対処を追跡することは出来るようにしました。

gist.github.com

特にパッケージを改造する必要などは無く、CinemachineComponentBaseを継承したクラスをプロジェクト内に配置しておけば、自動的に一覧に組み込まれます。
その際、CinemachineCore.Stage Stageプロパティに何を指定するかで、BodyかAIMのどちらに指定するのかが変わります。

 

内容は割と単純で、MutateCameraStateでカメラの動作を指定しているだけです。最終的な位置に向けてRawPositionRawOrientationを設定すれば、よしなにやってくれます。場合によってはPositionCorrection等もどうぞ。
まぁ動けばいいやクォリティなので、実際間違いはあるかもしれませんが、自分の欲しい動きはしたので、まあいいやっていう。

Struct CameraState | Package Manager UI website

 

サンプルコードはPackageManagerにある

コレを実装する上で何をどう書いて良いのか分からんとなるので、コードを色々と参考にさせてもらいました。PackageManagerにあるやつです。

f:id:tsubaki_t1:20180903004534j:plain

PackagerManagerで取得したパッケージはProjectビューから確認することが出来るので、中身を色々と参考にさせてもらいました。
幾つかのパッケージ(例えばECS)はテストも付随してるので「この機能をどうやって使えば良いの?」や「どんな想定の機能なの?」といった疑問には、コレが結構役に立ちます。

ちなみにCinemachineにテストはありません。マア、モトモト販売シテタAssetダシネ

tsubakit1.hateblo.jp

 

【Unity】ECSのEntityの作成やComponentDataの追加等の操作を別スレッドから実行する

今回はECSのEntityを非同期に作成する方法についてです。
主に打規模なシーンのロード等に使用できそうです。

 

 

Entityの生成や破棄、削除はメインスレッド上で行う

ECSでEntityを作成する場合、大抵の場合はEntityManagerを通してEntityを作成したりコンポーネントを追加・削除したりします。EntityManagerは基本的にメインスレッドで動作する設計になっており、ジョブ上からComponentDataやEntityの追加・削除といった処理は実行できません
EntityCommandBufferを利用してジョブから処理の指示を受ける事は可能ですが、処理を実行するのはメインスレッド上で動作するBarrierSystemです。

f:id:tsubaki_t1:20180902194448p:plain

tsubakit1.hateblo.jp

ただし、コレらの処理を非同期で行う方法が用意されていないわけでは無いみたいです。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はまさに世界の破壊者(ディケイド)か世界の架け橋(ディケイド)な訳です。

f:id:tsubaki_t1:20180902192018j:plain

つまり何が言いたいかというと、この世界のEntityManagerと別世界のEntityManagerは独立しているので、別世界のEntityManagerが排他モードになっても私達の世界のEntityManagerは普通に使えるという事です。
Entityを生産するだけのWorldを用意し、そのWorld運用は別スレッドでのみ行い、その生産物(Entity)をアクティブなWorldが徴収する…という形にすれば、Entityの生産は非同期で行いつつも、普通にEntityの操作が行えるようになるというシナリオです。

f:id:tsubaki_t1:20180902192922g:plain

 

C# Job Systemを使用した例

まずはC# Job Systemを使用した場合の例です。複数フレーム跨ぐ可能性があるので、コルーチンによる遅延実行を使用しています。

gist.github.com

実行すると下のようにEntityが別スレッドで作られます。下のプロファイリング結果はEntityを毎フレーム38,000程作った結果です。開始と終了時(特にEntity移行時)に負荷がかかりますが、Entity生成それ自体は完全に別スレッドで行われています。
また事前にチャンクやアーキタイプを作っておけば、Entityの移し替えもかなり低コストで行えます。

f:id:tsubaki_t1:20180902212707j:plain

内容はジョブにExclusiveEntityTransactionを渡してジョブ内でEntityの作成等を行っているだけです。なおジョブシステムの制約上4フレームを跨ぐような物は作れないので、あまり大規模な物は実は作りづらいです。
そのため、最大個数を決めてジョブを繋いで実行…みたいな形でやることになりそうですが、面倒なら単純にスレッドに投げてしまっても良いのかもしれません。
f:id:tsubaki_t1:20180902202405j:plain

なお非同期ではなく並列で動くか試した所、IJobPralellForは無理そうでしたが、ワールドを複数作って実行したところ普通に動きました。

f:id:tsubaki_t1:20180902220247j:plain

 

Async/Awaitを使用した例

排他モードの際には、Async/Awaitでもちゃんと動作しました。

gist.github.com

 

その他

この機能を使用すれば大規模かつ非同期なシーンのロード等が実現できそうですが、どちらかといえばソレはUnity.Entities.Serialization.SerializeUtility.DeserializeWorldの役目で、本機能はもう少しコンパクトな…例えばプロシージャルに大きな物を作る的な用途なんじゃないかなと予想しています。(中で似たような操作をしていそうですが)

 

追記:

SubSceneを使いましょう

tsubakit1.hateblo.jp

関連

https://forum.unity.com/threads/serialization-questions.534187/