しゅみぷろ

プログラミングとか

SpringBoneのJobSystem化でわかったこと

はじめに

Unityで揺れもの動かすの、重くない...?

ということで最近ちょこちょこ作っているのがuSpringBone。以下のやつ。

github.com

揺れもの用のBoneを揺らすSpringBoneの計算処理をJobSystem使ってごっそりWorkerThreadに持っていこうというのがコンセプトです。

ベース部分しか作っていないのでまだまだ未完成なんですが、JobSystemを使う上で気をつけるべき事がわかってきたのでメモがてら色々書き残しておこうと思います。

ちなみにuSpringBoneは試行錯誤しながら場当り的に書いたので、今後ちゃんと作るとしたらよりパフォーマンスを改善するために色々変わると思います。というか変えてます。

ちょっと前の検証結果ですが、パフォーマンスの変化はこんな感じ。

因みに

  • Unity: 2018.1.0f2
  • Burst: 0.2.4 Preview

で開発してます。

さて、今回はuSpringBoneの基盤を作ってく上で得たJobSystemの知識についてとか、気をつけておきたいこととか、そこらへんをメモしておこうと思います。あくまで現状のJobSystemについてのメモなので、今後色々変わると思います。

エディタと実機での負荷計測

JobSystemを使う上で、実機だとそこまで高負荷にならないような部分がエディタ上では負荷が高く見える事があるので注意が必要です。

一部処理については、UnityのJobsメニューから各種チェックを外したりすれば多少実機での実行速度に近付きますが、ちゃんとした計測は実機で行うことをお勧めします。

f:id:es_program:20180724121940p:plain:w300

Jobはなるべく減らす

Jobは可能な限り減らしましょう。 当然ですが、JobをScheduleするのにも多少コストがかかります。 Jobを細分化しすぎると、JobのScheduleがちょっとした負荷になってしまう場合があります。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using System.Threading;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJob
        {
            public void Execute()
            {
                Thread.Sleep(System.TimeSpan.FromMilliseconds(0.1));
            }
        }

        public void Update()
        {
            const int JobCount = 1000;

            var job = new JobA();
            JobHandle[] jobHandles = new JobHandle[JobCount];

            Profiler.BeginSample(">> Schedule");
            for(int i = 0; i < JobCount; ++i)
                jobHandles[i] = job.Schedule();
            Profiler.EndSample();

            JobHandle.ScheduleBatchedJobs();

            for(int i = 0; i < JobCount; ++i)
                jobHandles[i].Complete();
        }
    }
}

阿保みたいな例ですが、上記コードは適当なJobを1000個Scheduleしてみるやつです。

とりあえずエディタとビルド後で実行してプロファイラーでScheduleの負荷を計測してみました。

f:id:es_program:20180723121126p:plain:w600

エディタで計測

f:id:es_program:20180723121558p:plain:w600

ビルド後計測

これくらいだと「そこまで高負荷じゃないじゃん!」と思われるかもしれませんが、塵も積もればです。 せっかくJobSystemに重い計算処理を退避させるので、MainThreadにはちょっとでも楽させてあげたいところ。

上記のような簡単な例だと、単純にIJobParallelForを使って以下のように書き換えられます。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using System.Threading;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelFor
        {
            public void Execute(int index)
            {
                Thread.Sleep(System.TimeSpan.FromMilliseconds(0.1));
            }
        }

        public void Update()
        {
            const int JobCount = 1000;
            var job = new JobA();

            Profiler.BeginSample(">> Schedule");
            var jobHandle = job.Schedule(JobCount, 32);
            Profiler.EndSample();

            JobHandle.ScheduleBatchedJobs();

            jobHandle.Complete();
        }
    }
}

f:id:es_program:20180724121132p:plain:w600

エディタで計測

f:id:es_program:20180724121310p:plain:w600

ビルド後計測

当然、Schedule呼び出し回数が減ったのでMainThreadに掛かる負荷が少なくなりました。

ただし、今回のように単純には解決できないケースもあります。というより殆どがそういったケースだと思います。 Jobがあまりにも多くなる場合はより良い設計を考え直しましょう。

NativeContainerへのアクセスについて

下記2つのスクリプトを実行して、NativeArrayへのアクセス部分の負荷の変化を見てみました。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using System.Threading;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJob
        {
            [WriteOnly]
            public NativeArray<int> rand;
            public void Execute()
            {
                var random = new System.Random();
                for(int i = 0; i < rand.Length; ++i)
                    rand[i] = random.Next(0, 100);
            }
        }

        public void Update()
        {
            var rand = new NativeArray<int>(100000, Allocator.Temp);
            var job = new JobA() { rand = rand };

            Profiler.BeginSample(">> Schedule");
            var jobHandle = job.Schedule();
            Profiler.EndSample();

            JobHandle.ScheduleBatchedJobs();
            jobHandle.Complete();

            var sum = 0;
            Profiler.BeginSample(">> Access");
            for(int i =0; i < rand.Length; ++i)
                sum += rand[i];
            Profiler.EndSample();
            Debug.Log(sum);

            rand.Dispose();
        }
    }
}
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using System.Threading;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJob
        {
            [WriteOnly]
            public NativeArray<int> rand;
            public void Execute()
            {
                var random = new System.Random();
                for(int i = 0; i < rand.Length; ++i)
                    rand[i] = random.Next(0, 100);
            }
        }

        public void Update()
        {
            var rand = new NativeArray<int>(100000, Allocator.Temp);
            var job = new JobA() { rand = rand };

            Profiler.BeginSample(">> Schedule");
            var jobHandle = job.Schedule();
            Profiler.EndSample();

            JobHandle.ScheduleBatchedJobs();
            jobHandle.Complete();

            var sum = 0;
            var tmp = new int[rand.Length];
            rand.CopyTo(tmp);
            Profiler.BeginSample(">> Access");
            for(int i =0; i < rand.Length; ++i)
                sum += tmp[i];
            Profiler.EndSample();
            Debug.Log(sum);

            rand.Dispose();
        }
    }
}

2つのコードの違いはNativeArrayに直接アクセスするか、配列にコピーしてアクセスするかです。

f:id:es_program:20180725125637p:plain:w600

【Editor】NativeArrayに直接アクセス

f:id:es_program:20180725125616p:plain:w600

【Editor】配列にコピーしてアクセス

f:id:es_program:20180725130111p:plain:w600

【Build】NativeArrayに直接アクセス

f:id:es_program:20180725125841p:plain:w600

【Build】配列にコピーしてアクセス

素数及びアクセス回数が多ければ多いほど、配列にコピーしてそちらを使うようにした方がいいです。

ちなみにNativeArray.CopyTo自体のコストを計測すると

f:id:es_program:20180725131051p:plain:w600

【Editor】NativeArrayコピー

f:id:es_program:20180725131156p:plain:w600

【Build】NativeArrayコピー

コピーのコスト合わせても、NativeArrayのインデクサーから各要素にアクセスするより安定して速いです。

ちなみにrandtmpのアクセス回数を増やし、それぞれ該当箇所を

sum = rand[i] + rand[i];
sum = tmp[i] + tmp[i];

としてBuildして計測したところ、以下のようになりました。

f:id:es_program:20180725132053p:plain:w600

【Build】NativeArray要素へのアクセス回数を増やした

f:id:es_program:20180725132040p:plain:w600

【Build】コピー後の配列要素へのアクセス回数を増やした

コピー安定かなと思います。

ちなみに、アクセス部分をunsafeコードに置き換えて以下のような感じにしてみます。

unsafe
{
    var ptr = (int*)rand.GetUnsafeReadOnlyPtr();
    Profiler.BeginSample(">> Access");
    for (int i = 0; i < rand.Length; ++i)
        sum += *(ptr + i);
    Profiler.EndSample();
}

f:id:es_program:20180725142229p:plain:w600

【Build】ポインタでアクセス

コピーの必要がない上高速なので、使えるのであればガシガシ使っていきたいところ。 uSpringBoneはガシガシ使う方針にしました。

2018-08-31 追記

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using System.Threading;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJob
        {
            [WriteOnly]
            public NativeArray<int> rand;
            public void Execute()
            {
                var random = new System.Random();
                for(int i = 0; i < rand.Length; ++i)
                    rand[i] = random.Next(0, 100);
            }
        }

        public void Update()
        {
            var rand = new NativeArray<int>(300000, Allocator.Temp);
            var job = new JobA() { rand = rand };

            Profiler.BeginSample(">> Schedule");
            var jobHandle = job.Schedule();
            Profiler.EndSample();

            JobHandle.ScheduleBatchedJobs();
            jobHandle.Complete();

            var sum = 0;
            var tmp = new int[rand.Length];
            rand.CopyTo(tmp);

            Profiler.BeginSample(">> Access");
            for(int i = 0; i < rand.Length; ++i)
                sum += rand[i];
            Profiler.EndSample();
            Debug.Log(sum);

            sum = 0;
            Profiler.BeginSample(">> Access Copy");
            for(int i = 0; i < rand.Length; ++i)
                sum += tmp[i];
            Profiler.EndSample();
            Debug.Log(sum);

            unsafe
            {
                sum = 0;
                var ptr = (int*)rand.GetUnsafePtr();
                Profiler.BeginSample(">> Access Unsafe Pointer");
                for(int i = 0; i < rand.Length; ++i)
                    sum += *(ptr + i);
                Profiler.EndSample();
                Debug.Log(sum);
            }

            rand.Dispose();
        }
    }
}

上記コードをIL2CPP環境でScript Debuggingをfalseにし、StandaloneでビルドしてProfilerで測定した結果、以下のようになりました。

f:id:es_program:20180831162122p:plain:w600

小さくて見辛いですが、NativeArrayへのアクセス速度はインデクサーを用いた一般的なアクセスが一番高速でした。

また、以下はScripting BackendをMonoにし、先ほどと同様ビルド後に測定した結果です。

f:id:es_program:20180831163312p:plain:w600

アクセス速度はインデクサーが一番遅く、コピーした配列へのアクセスとポインタでのアクセスの方が速いという結果でした。

現状は ENABLE_IL2CPP で要素へのアクセス方法を変更するのが良いかもしれません。

因みにIL2CPPでのNativeArrayに関するパフォーマンスについてはJacksonDunstan.comにとても詳しく書かれています。

IJobParallelForTransformについて

IJobParallelForTransformを使う際には色々気をつけるべきことがあります。

まずはTransformAccessArray内のTransformが親子関係にあるかどうかでの違いについて。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access;

        void Start()
        {
            if(access.isCreated)
                access.Dispose();
            access = new TransformAccessArray(Count);
            for(int i = 0; i < Count; ++i)
            {
                var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                access.Add(obj.transform);
            }
        }

        void OnDestroy()
        {
            access.Dispose();
        }

        public void Update()
        {
            var job = new JobA();
            var handle = job.Schedule(access);
            handle.Complete();
        }
    }
}
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access;

        void Start()
        {
            if(access.isCreated)
                access.Dispose();
            access = new TransformAccessArray(Count);
            Transform buf = null;

            for(int i = 0; i < Count; ++i)
            {
                var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj.transform.SetParent(buf);
                buf = obj.transform;
                access.Add(obj.transform);
            }
        }

        void OnDestroy()
        {
            access.Dispose();
        }

        public void Update()
        {
            var job = new JobA();
            var handle = job.Schedule(access);
            handle.Complete();
        }
    }
}

f:id:es_program:20180725163029p:plain:w600

【Editor】親子関係がない場合

f:id:es_program:20180725162600p:plain:w600

【Editor】親子関係がある場合

WorkerThreadの使われ方に注目してください。 親子関係がある場合、子は親のTransformに依存することになるので、1つのThreadしか使われません。関係がない場合は複数のThreadで処理してくれています。 Editorでの実行結果のみ載せていますが、ビルドしても同様の使われ方になります。

また、親子階層がめちゃんこ深いので、負荷も爆上がりしています。

因みに兄弟でも1つのThreadしか使われないようでした。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access;

        void Start()
        {
            var parent = GameObject.CreatePrimitive(PrimitiveType.Cube);

            if(access.isCreated)
                access.Dispose();
            access = new TransformAccessArray(Count);

            for(int i = 0; i < Count; ++i)
            {
                var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj.transform.SetParent(parent.transform);
                access.Add(obj.transform);
            }
        }

        void OnDestroy()
        {
            access.Dispose();
        }

        public void Update()
        {
            var job = new JobA();
            var handle = job.Schedule(access);
            handle.Complete();
        }
    }
}

f:id:es_program:20180725202419p:plain:w600

【Editor】兄弟関係がある場合

兄弟の場合でも、親子関係同様で1つのThreadしか使われません。 ただ、親子関係に比べると負荷は大分マシです。

Transform変更処理の負荷は階層構造が複雑であればあるほど重くなります。 なるべくシンプルな構造にし、深くネストしないことが大事です。

ちなみにIJobParallelForTransformなJobを複数扱う場合についてもメモしておきます。 まずは親子関係がなく、全てSceneのルートオブジェクトの場合

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access1;
        TransformAccessArray access2;

        void Start()
        {
            if(access1.isCreated)
                access1.Dispose();
            if(access2.isCreated)
                access2.Dispose();
            access1 = new TransformAccessArray(Count);
            access2 = new TransformAccessArray(Count);

            for(int i = 0; i < Count; ++i)
            {
                var obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                access1.Add(obj1.transform);

                var obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                access2.Add(obj2.transform);
            }
        }

        void OnDestroy()
        {
            access1.Dispose();
            access2.Dispose();
        }

        public void Update()
        {
            var job1 = new JobA();
            var handle1 = job1.Schedule(access1);
            var job2 = new JobA();
            var handle2 = job2.Schedule(access2);

            JobHandle.ScheduleBatchedJobs();
            Thread.Sleep(5);

            handle1.Complete();
            handle2.Complete();
        }
    }
}

f:id:es_program:20180726165207p:plain:w600

【Editor】親子関係がなく、複数のJobを扱う場合

普通にいい感じですね。 次は親子関係がある場合です。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access1;
        TransformAccessArray access2;

        void Start()
        {
            if(access1.isCreated)
                access1.Dispose();
            if(access2.isCreated)
                access2.Dispose();
            access1 = new TransformAccessArray(Count);
            access2 = new TransformAccessArray(Count);
            Transform buf = null;

            for(int i = 0; i < Count; ++i)
            {
                var obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                access1.Add(obj1.transform);
                obj1.transform.SetParent(buf);
                buf = obj1.transform;

                var obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                access2.Add(obj2.transform);
                obj2.transform.SetParent(buf);
                buf = obj2.transform;
            }
        }

        void OnDestroy()
        {
            access1.Dispose();
            access2.Dispose();
        }

        public void Update()
        {
            var job1 = new JobA();
            var handle1 = job1.Schedule(access1);
            var job2 = new JobA();
            var handle2 = job2.Schedule(access2);

            JobHandle.ScheduleBatchedJobs();
            Thread.Sleep(5);

            handle1.Complete();
            handle2.Complete();
        }
    }
}

f:id:es_program:20180726165559p:plain:w600

【Editor】親子関係があり、複数のJobを扱う場合

Jobが分割されてはいますが、Transformへのアクセスが直列になります。 具体的には、1つめのJob実行中は関連するTransformへのAccessがブロックされるため、2つのJobが並列に実行されません。

あと親子関係が複雑で激重です。

次は兄弟関係がある場合です。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access1;
        TransformAccessArray access2;

        void Start()
        {
            var parent = GameObject.CreatePrimitive(PrimitiveType.Cube);

            if(access1.isCreated)
                access1.Dispose();
            if(access2.isCreated)
                access2.Dispose();
            access1 = new TransformAccessArray(Count);
            access2 = new TransformAccessArray(Count);

            for(int i = 0; i < Count; ++i)
            {
                var obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj1.transform.SetParent(parent.transform);
                access1.Add(obj1.transform);

                var obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj2.transform.SetParent(parent.transform);
                access2.Add(obj2.transform);
            }
        }

        void OnDestroy()
        {
            access1.Dispose();
            access2.Dispose();
        }

        public void Update()
        {
            var job1 = new JobA();
            var handle1 = job1.Schedule(access1);
            var job2 = new JobA();
            var handle2 = job2.Schedule(access2);

            JobHandle.ScheduleBatchedJobs();
            Thread.Sleep(5);

            handle1.Complete();
            handle2.Complete();
        }
    }
}

f:id:es_program:20180726170043p:plain:w600

【Editor】兄弟関係があり、複数のJobを扱う場合

これも同様ですね。 では、そもそも系譜の違う2つのJobにした場合はどうなるでしょうか。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using UnityEngine.Profiling;
using UnityEngine.Jobs;
using System.Threading;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs.LowLevel.Unsafe;

namespace Es.JobSystem.Sample._01
{
    public class JobSystemSample01 : MonoBehaviour
    {
        struct JobA : IJobParallelForTransform
        {
            public void Execute(int index, TransformAccess transform)
            {
                transform.rotation *= Quaternion.Euler(1,1,1);
            }
        }

        const int Count = 1000;
        TransformAccessArray access1;
        TransformAccessArray access2;

        void Start()
        {
            var parent1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
            var parent2 = GameObject.CreatePrimitive(PrimitiveType.Cube);

            if(access1.isCreated)
                access1.Dispose();
            if(access2.isCreated)
                access2.Dispose();
            access1 = new TransformAccessArray(Count);
            access2 = new TransformAccessArray(Count);

            for(int i = 0; i < Count; ++i)
            {
                var obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj1.transform.SetParent(parent1.transform);
                access1.Add(obj1.transform);

                var obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj2.transform.SetParent(parent2.transform);
                access2.Add(obj2.transform);
            }
        }

        void OnDestroy()
        {
            access1.Dispose();
            access2.Dispose();
        }

        public void Update()
        {
            var job1 = new JobA();
            var handle1 = job1.Schedule(access1);
            var job2 = new JobA();
            var handle2 = job2.Schedule(access2);

            JobHandle.ScheduleBatchedJobs();
            Thread.Sleep(5);

            handle1.Complete();
            handle2.Complete();
        }
    }
}

f:id:es_program:20180726170439p:plain:w600

【Editor】系譜が違うオブジェクトに対して複数のJobを扱う場合

TransformAccessArrayに登録されているTransformの関係が疎であれば並列化できています。

一言でまとめるとTransformAccessArrayに登録するTransformの階層構造に注意することが大切です。

ローカル座標の更新とグローバル座標の更新
今回の例では、TransformAccess.rotationに対して回転計算を行い更新していました。 TransformAccess.localRotationに対して更新を行なった場合には、親子関係が複雑であっても今回の例ほど負荷が高くなりません。 Transformが保持しているのはローカル座標です。 グローバル座標で値の更新を行う場合はオブジェクトの親のグローバル座標を求め、その値から自身の新しいローカル座標を計算する作業が必要になります。 Transformの更新では、可能な場合はローカル座標を更新するようにしましょう。

複数のIJobParallelForTransformを扱う際の罠
f:id:es_program:20180727160555g:plain:w600 uSpringBone開発中に、複数のIJobParallelForTransformでシーンルートオブジェクトのTransformを更新しようとすると時々上記GIFのようにScheduleに非常に時間がかかる現象に遭遇しました(ビルド後の実行結果です)。 原因が究明出来てないのですが、ScheduleするJobを1つにまとめることで回避出来ます。 かなり厄介なので、IJobParallelForTransformを扱う際には注意が必要かもしれません。

NativeContainerへのポインタを使う

Jobで複数のNativeContainerを扱いたいけれど、その数が動的に変化するケースがあります。

そういったケースに遭遇した場合、NativeContainerへのポインタを取得して、ポインタをNativeContainerに詰めてJobへの入力としてやるのが楽です。

uSpringBoneでは複数のJobで計算処理を実行後、計算結果が格納されている複数のNativeArrayへのポインタをTransform更新用の1つのJobに渡して更新処理を行っています。

[BurstCompile]
struct ApplyTransformJob : IJobParallelForTransform
{
    [ReadOnly]
    public NativeArray<IntPtr> boneHeadPtrArray;
    [ReadOnly]
    public NativeArray<int> boneLengthArray;

    public unsafe void Execute(int index, TransformAccess transform)
    {
        var ptr = GetDataPtr(index);

        transform.position = ptr -> grobalPosition;
        transform.rotation = ptr -> grobalRotation;
    }

    private unsafe BoneData* GetDataPtr(int currentIndex)
    {
        var headPtrIndex = 0;
        var elemPtrIndex = currentIndex;

        for(int i = 0; i < boneLengthArray.Length; ++i)
        {
            headPtrIndex = i;
            elemPtrIndex = currentIndex;
            currentIndex = currentIndex - boneLengthArray[i];
            if(currentIndex < 0)
                break;
        }

        var head = (BoneData*)boneHeadPtrArray[headPtrIndex];
        var elem = (BoneData*)(head + elemPtrIndex);
        return elem;
    }
}

boneHeadPtrArrayは連結している各ボーンの先頭のボーンデータへのポインタが入っています(ボーンデータには計算後の位置・回転情報が入っています)。 boneLengthArrayは連結しているボーンの連結数です。

この2つの情報からGetDataPtr()で、現在変更しようとしているTransformAccessに対応するボーンデータを取得し、その情報を使ってTransformを書き換えます。

以下の記事も参考に。

tsubakit1.hateblo.jp

Scheduleのタイミング

esprog.hatenablog.com

こちらでも書いてますが、Jobはできる限りなるべく早くScheduleしてFlushしたほうがいいです(ただ、JobをFlushするのにもコストがかかるので、1つのJobをScheduleする度にFlushすべきではないです。用法・用量を守って正しく使用しましょう)。 他の処理実行中にWorkerThreadが計算してくれます。

Unityの各種イベント関数のはじめに実行したい場合、ScriptExecutionOrderを設定すると良いです。

設定が面倒であれば、こういったものを利用しましょう。

Control Unity's Script Execution Order by code · GitHub

uSpringBoneでは、JobのScheduleを管理するクラスに上記コードを適用して、他のUpdate処理よりも前にScheduleをFlushさせるようにしています。

具体的には以下のようなコードになっています。

namespace Es.uSpringBone
{
    // 他のスクリプトに記述されたイベント関数よりも前に実行させる
    [ScriptExecutionOrder(-32000)]
    public class SpringBoneJobScheduler : MonoBehaviour
    {
        /********     省略     *********/

        private void Update()
        {
            // 必要最低限のJobをSchedule
            foreach (var chain in chains)
                chain.ScheduleJob();

            // Flush
            JobHandle.ScheduleBatchedJobs();
        }

        /********     省略     *********/

PlayerLoopで云々も出来そうですが、特に理由がなければこちらは多用しないほうが良さそうです。

さいごに

JobSystemを利用する上で一番大事なのは、その設計です。 少ないJobで最大限の効果を発揮し、MainThreadにかかる負荷を少しでも減らすよう心がけましょう。