テラシュールブログ

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

【Unity】C# Job Systemからでもランダムを使いたい

今回はC# Job Systemでランダムを使うアプローチについてです。

UnityEngine.Randomがメインスレッドでしか動作しない

シューティングの弾や移動の判断、様々な要因でランダムを使いたくなります。特に C# Job Systemでデモ的な何かを作ろうと思った時など、要素が同じ動きにならないように動きをバラけさせたくなります。
ここで UnityEngine.Randomがメインスレッドでしか動作しない という問題にぶち当たります。

UnityException : RandomRangeInt can only be called from the main thread

強引なアプローチではランダムテーブルを作ってズラすという物もありますが、今回は異なるアプローチを試します。

Unity.Mathematics.Random

Mathematicsに追加された新しいAPI Unity.Mathematics.Random を試します。
このAPIUnityEngine.Randomと異なり、以下のような特徴があります。

  • 複数のインスタンスを持てる
    (他の箇所でRandomを実行しても再現性がある)
  • 別スレッドからも呼べる
  • Burstで最適化される

特に有り難いのがインスタンスを持てる事による再現性ですが、これは逆を言えば インスタンスを用意し初期化しないと使えません 。static関数ではないので、インスタンスの管理や生成といった手間が入ります。

RandomをC# Job Systemで使ってみる

Randomを使用してみます。
作ったのは下のような物です。

  • ボールが跳ねる
  • 着地時に別の座標(ランダム)へ移動

f:id:tsubaki_t1:20181029230516g:plain

とりあえずジョブにランダムを突っ込む。しかし

ランダムをとりあえず突っ込んでみます。普通に C# Job Systemのフィールドとして登録 し、中で使用するという感じです。
この時、UnityEngine.RandomUnity.Mathematics.Randomの宣言が重複するので、using Random = Unity.Mathematics.Random;とか宣言しておきます。

f:id:tsubaki_t1:20181029231444j:plain f:id:tsubaki_t1:20181029231127j:plain

これで実行してみますが、Random自体は使えてるのですが 値が非常に偏ります 。 対策にと毎フレームSeedを設定しても下のような感じ。対策しないと本当に偏る…というか最早固定値?

f:id:tsubaki_t1:20181029231851g:plain

同じ結果が出ないようにする

というのもジョブは実行時にコピーを作るので、Nextで次の値を取得しても大本に反映されない ため、全てのジョブで 最初のRandom を取得してしまってるからだと思われます。実際、上のはジョブ生成時に新しいSeedを与えた場合で、Seedを与えなければ全てのジョブが毎回同じ結果を返します。

毎回同じ結果を返さない為にジョブ毎にSeedを変えるかインスタンスを共有するかする必要があり、スレッドセーフにしてアクセスとか色々考えましたが、最終的に NativeArrayでインスタンスを共有してシングルスレッドでBurstかけてぶん回したほうが早いなー という結論に至りました。Burst強い。並列処理だと複数のスレッドからRandomにアクセスしちゃうのでヤバイですが、まぁIJobProcessComponentData.ScheduleSingleでシングルスレッドで動かせばまあ良いかなという発想です。

あ、別にStateを共有しても問題ないです。進行を保存出来れば良いので。

f:id:tsubaki_t1:20181029232825j:plain

コード

gist.github.com

感想

Randomを毎回維持してくれるのは有り難い事なのですが、インスタンスが独立してるせいで若干面倒くさい目に合いました。

なお .NET 4系を要求 します。.NET 3系の場合は諦めてランダムテーブルなり自前実装なりをするのが良いです。

関連

Randomのソースコード。xorshiftベース github.com

”ランダム”についての詳しい解説

www.slideshare.net

乱数の独自実装が紹介されています。 kan-kikuchi.hatenablog.com

【Unity】Animation C# Jobsで遊んでみる

Unite LAで盛り上がってますが、今回はソレは置いといてAnimation C# Jobsを試してみます。

Animation C# Jobs

Animation C# Jobsは言うならば 「アニメーションを独自実装するモノ」 です。
Playable APIや派生物(Animation Controller、Timeline、Simple Animator)が 「アニメーションの制御を定義するモノ」 なのに対して、アニメーションを実現するためのストリームに直接操作を行う機能です。

blogs.unity3d.com

そのため一工夫が必要ですがGameObjectを経由せず直接Skin Meshの行列への書込等も期待出来ます。

その他の用途としては、2ボーンIKやフルボディIKのようなコントロール、LookAtや揺れモノのようなエフェクト、マスク等があるみたいです。

www.youtube.com

www.youtube.com

作ってみたもの

f:id:tsubaki_t1:20181025233734j:plain

f:id:tsubaki_t1:20181025233531j:plain

ボーン構造を持つオブジェクトに対してEffectorの回転を流し込むだけのサンプルです。

実行前は単純に複数のボーン構造を持つようなオブジェクトですが、実行時に階層を平坦化して、かつ他のGameObjectから操作します。
コレはEffectorを使用していますが、実際にはNative ArrayでQuaternionを用意しておいてC# Job Systemでガガっと計算するなり、ファイルからストリームで書き込むなりするのが良いんじゃないかなと思います。

解説

f:id:tsubaki_t1:20181025234705j:plain

まずAnimation C# Jobsを実行するにはアバターが必ず必要です。基本アバターからストリームにアクセスしているので、コレが無い場合は InvalidOperationException: The TransformStreamHandle cannot be resolved. のエラーが出力されます。 もしモデルからインポートしていない場合、 Bind Generic Avatarを使用します 。実行後GameObjectの階層構造を変更した場合、アバターの再構築が必ず必要です。
なおバグか分かりませんがPrefabにBind Generic Avatarで作成したアバターは格納されてません。

f:id:tsubaki_t1:20181025234610j:plain

あとはAnimation C# Jobsで作成したジョブをPlayableに格納して実行します。

Animation C# Jobsで行うべき内容は3つ

  1. ジョブを定義し、ストリームにアクセスして色々とするコードを記述する
  2. アニメーションのStreamにアクセスする用のハンドルの取得
  3. Playable Graphにジョブを登録

まずはAnimation C# Jobsを定義します。

見るべき所は2点で、まずIAnimationJobを継承している点です。このインターフェースでAnimation Streamにアクセスします。

あとはハンドルを元にAnimaiton Streamからpositionやrotationを取得し、書き込みます。
もしAnimationClip等と連動したい場合、Playableの子としてアニメーションを再生しておきます。

f:id:tsubaki_t1:20181026000324j:plain

次は使用する側です。 Transformのハンドルを入手します。

animator.BindStreamTransformではAnimatorの子オブジェクトからハンドルを取得します。
正直これAvatarから取得すべきだと思うのですが、なぜか現状はTransformに必ずアクセスが必要です。多分Humanoidは回避出来ます。
このコードではtransform.Findで子オブジェクトを探索して取得しました。

なおTransformStreamHandleを取得したら用済みなので、AnimatorUtility.OptimizeTransformHierarchy(gameObject, null);でHierarchyをスッキリさせます。

あとはBindSceneTransformAvatar外のオブジェクトに対してのストリームを入手します。こちらは特にAvatarを用意したりする必要もなく、シーン内のオブジェクトにアクセス出来ます。
主にIKのエフェクター用です。

f:id:tsubaki_t1:20181025235253j:plain

あとはAnimation C# Jobsを登録します。
AnimationScriptPlayable.CreateでPlayableを作成し、作ったジョブを登録するだけです。
outputを云々するのが面倒くさいので AnimationPlayableUtilities.Play(animator, playable, graph);でサクッと動かしました。PlayableGraphを複数キャラクターで共有する良い子は真似しないでね。

f:id:tsubaki_t1:20181026000117j:plain

そこそこ配置して実行すると、こんな感じで実行されました。
それなりに並列化されています。

f:id:tsubaki_t1:20181026001205j:plain

コード

gist.github.com

感想

Playableを使用しないと使えない(=Animation Controllerをオーバーライドする)ので、ぶっちゃけ普通にGameObjectとLateUpdateでアクセスするのが楽そうな気がしますが、ストリームに直接云々出来るのは楽しいです。特にNativeArray経由でアニメーションデータを流し込んでやるとAnimationClip無しに色々できr…え?GameObjectベースでも出来るって? HAHAHA

関連

サンプルコード一覧です。

github.com

Playable APIとAnimation C# Jobsの比較があります

www.slideshare.net

聞いた話によるとKinematicaもAnimation C# Jobsで動いてるらしいです。

blogs.unity3d.com

【Unity】ECSの並列処理"IJobForEach"におけるスケジューリングの挙動

デカイ記事を書こうとして中々更新まで辿り着かないので、今回はサクっとECSの並列処理におけるスケジューリングの挙動について書いておきます。

 

 

IJobForEach?

 

以前は`IJobProcessCommand`という名前のAPIでしたが`IJobForEach`と分かりやすい名前になりました。

ECSは参照するComponentDataでスケジュールが決まる

ECSのIJobForEachを使用すると、特に面倒くさい事を考えなくとも良い感じにスケジューリングしてくれます。
どういった感じにスケジューリングされるのか見ていきます。

f:id:tsubaki_t1:20181223211917j:plain

結論を言えば、どちらかが同じComponentDataに書き込む可能性がある場合、並列で処理されません。逆を言えば、異なるComponentDataに書き込む場合は並列処理が可能で、ミッチリとスレッドを詰めて処理されます。


異なるComponentDataへアクセスするジョブは並列処理する

まず異なるComponentDataへアクセスするジョブは、並列で処理されます。
下の例ではSampleData1とSampleData2という異なるComponentDataへアクセスしている場合の挙動です。

gist.github.com

実行してみると、2つのジョブが並列で実行されるのが確認出来ます。

f:id:tsubaki_t1:20181023234353j:plain

これは複数のJobHandleを合成した場合も同様です。
例えば下のコードではSampleData1とSampleData2に対しての処理と、SampleData3とSampleData4に対する処理を呼び出します。
結果、全て並列で処理されました。

gist.github.com

f:id:tsubaki_t1:20181024000456j:plain

同じComponentDataへアクセスするジョブは並列処理されない

一方、同じComponentDataへアクセスしている場合。この場合は並列処理されず、片方の終了を必ず待ちます。
下のコードでは2つのシステムがComponentDataへアクセスしているので、並列で処理されることはありません。

gist.github.com

これは片方がReadOnlyだった場合も同様です。

f:id:tsubaki_t1:20181023234637j:plain

またJobHandleをまとめた場合、中にReadWriteが一つでもあれば分割されます。
下のコードではまとめたJobHandleの一つがReadWriteなので、処理が分割されています。

gist.github.com

f:id:tsubaki_t1:20181024000846j:plain

ReadOnlyのComponentData同士は並列でアクセスになる

同じComponentDataにアクセスしている場合でも、ReadOnlyの場合は同時にアクセスが可能です。

gist.github.com

f:id:tsubaki_t1:20181023235251j:plain

RequireComponentTagは並列アクセスになる

RequireComponentTagを使用しても並列アクセスになります。絶対に書き込まない設定なので、当然といえば当然ですが。

gist.github.com

f:id:tsubaki_t1:20181223210753j:plain

分割数はチャンクに依存

どうもIJobForEachの処理分割数はチャンクに依存するようになったみたいです。

要するに、たくさんComponentData(タグは除く)を持っているEntityは積極的に分割されやすく、余り持っていないデータは積極的に統合されます。
(分割数が少なければ少ない程、効率的に処理される)

 

感想

ということで並列処理の動作を見てみました。
ECSで「単一の動作を突き詰める」ようなデモを作ろうとすると、頑張って並列化を推し進めようとします。ただ、そこまで頑張らなくともジョブを発行しまくってたら意外とWorker Threadがジョブで埋まるねという感想

とりあえずJobHandleで依存関係を勝手に作ってくれるのは楽で良いです。

【Unity】ECSでチャンク単位のバッチ処理を実現するChunk Iteration、それとEntityQuery

最近のECS界隈で特に理由もなくChunk Iterationを採用されている事をよく見るので、今回そのChunk Iterationついて書いてみます。

ComponentDataの組み合わせの爆発

ECSは基本的に「特定のComponentDataの組み合わせ」で処理する対象を判定しています。Interfaceのような特定の型ではなく組み合わせです。
例えば下の図では「人力で移動するシステム」と「エンジンで動くシステム」があり、荷物のComponentDataを保持している場合には荷物に関する処理が追加されます。 System要求するComponentDataを持つEnittyが処理されていきます。この要求は「全てのブツを所持している(AND)」及び「該当のブツを所持していない(NOT)」です。

f:id:tsubaki_t1:20181016191933j:plain

さて上の図で少し気になるのが「人力」と「エンジン」のシステムです。基本的に動き自体は人力だろうとエンジンだろうと本質的には同じ「移動システム」です。コードで比較すると、多分殆ど同じようなコードになります。

ここで動力の種類の数だけSystemや EntityQuery を用意すると同じようなクエリーが大量に生成されることになるので余り宜しくないです。Interfaceで抽出出来るなら楽なのですが、ComponentData単位でしか取得出来ません。

f:id:tsubaki_t1:20181016230302j:plain

そこでECSでは「OR」を新しく追加して大雑把にEntityの組み合わせを取得、処理内で行うべき処理を割り振る機能を追加しました。
それがEntityArchetypeQuery、そしてChunk Iterationという機能です。

Chunk Iterationという回避方法

考え方は割と単純です。

EntityQueryを要求する際に緩い条件で検索をかけて、内包するComponentDataの組み合わせに応じて処理を切り替えを行うだけです。
これで要求するQueryの種類が減らせます。

検索に使用できるのはこの3種類

  • AND(全てのブツを含む)
  • OR(指定したブツが含まれる)
  • NONE(指定したブツを含まない)

例えば下の場合、今までは「人力&タイヤ」OR「エンジン&タイヤ」の組み合わせで取得していましたが、これを「(人力 OR エンジン) & タイヤ」で取得します。そして処理を行う際、保持するComponentDataが「人力」か「エンジン」かを確認し処理を切り替えます。

f:id:tsubaki_t1:20181016195416j:plain

このように複数のComponentDataを許容すると処理を行う際に1Entity毎に「実行する処理の内容を切り替える」事になりそうですが、そこらへんは大丈夫になっています。

ECSでは基本的にComponentDataの組み合わせ(archetype)毎にまとめて配置されています。この塊はチャンク(Chunk)と呼ばれます。同じチャンク内なら全て同じComponentDataの組み合わせであることが保証されています。

要するに、処理の切替えはEntity毎ではなくチャンク毎です。

f:id:tsubaki_t1:20181016201957j:plain

tsubakit1.hateblo.jp

ということで、この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」を散々見る事になりました。

全文

gist.github.com

感想

Chunk Iterationでした。

利用するシーンの多い良い機能ではありますが、無条件で使うべきものでもないので、状況次第で使っていくのが良さそうです。

追記:低レベルAPI扱いとなりました。

関連

Chunk Iterationについてディープな解説をしている記事です。

【Unity】Entity Component System入門(その2)【2018.2】 - Qiita