dasukoの技術ブログ

現役エンジニアのブログです。

【Unity】Job Systemを使ってみた

はじめに

Unity C# Job System を利用すると、Unity とうまく相互作用する マルチスレッドコード を書くことができ、正しいコードを書くことを容易にします。

C# Job System - Unity マニュアルより引用

  

公式ドキュメントにあるようにC# Job Systemはマルチスレッドコードをサポートする機能です。

FirebaseなんかはTaskの使用を推奨しており、FirebaseのSDKの内部の処理はバックグラウンドスレッドで行われるようになっていましたが、

MonoBehaviourのイベントがメインスレッドだったり、UnityのAPIがメインスレッドからの呼び出しを期待していることもあり、メインスレッドのみで処理を行うことが基本となっていました。

しかし、可能な限りマルチスレッドで処理を行うことにより、パフォーマンスを大幅に改善することができます。

特にゲーム開発ではフレーム落ちして、カクカクした表示になることを防ぐことができます。

JobSystemとは

ジョブシステムはスレッドの代わりに ジョブ (英語) を作成して マルチスレッドコード を管理します。

ジョブシステムは、複数のコアにわたる一揃いの ワーカースレッド を管理します。通常、論理 CPU コア (英語) ごとに 1 つのワーカースレッドがあり、コンテキストの切り替えを回避します (ただし、オペレーティングシステムやその他の専用アプリケーション用に一部のコアを予約する場合があります)。

ジョブシステムはジョブを ジョブキュー (英語) に加えて実行します。ジョブシステムのワーカースレッドは、ジョブキューから項目を取り出し、それらを実行します。ジョブシステムは 依存関係 (英語) を管理し、ジョブが適切な順序で実行されるようにします。

ジョブシステムとは何か - Unity マニュアルより引用

ジョブはジョブキューに追加して管理されるので、順番に実行されます。

スレッドはジョブシステムが管理してくれるのでスレッドプール的な役割にもなっていると思います。

Jobの実装

今回は簡単にIJobインターフェースを実装して、Vector3の配列の各zの値を1秒毎に1加算するサンプルを作ってみました。

using UnityEngine;
using Unity.Jobs;

public class JobTest : MonoBehaviour
{
    private Vector3[] positions = new Vector3[3];
 
    private void Awake()
    {
        for (var i = 0; i < positions.Length; ++i)
            positions[i] = Vector3.zero;
    }

    private void Update()
    {
        var pos = new Unity.Collections.NativeArray<Vector3>(positions.Length, Unity.Collections.Allocator.Persistent);
        pos.CopyFrom(positions);

        var job = new TestJob
        {
            positions = pos,
            deltaTime = Time.deltaTime
        };

        var jobHandle = job.Schedule();

        // Jobが完了するまで待機
        jobHandle.Complete();

        job.positions.CopyTo(positions);

        pos.Dispose();
    }

    struct TestJob : Unity.Jobs.IJob
    {
        public Unity.Collections.NativeArray<Vector3> positions;

        public float deltaTime;

        public void Execute()
        {
            // z方向deltaに進める
            const float delta = 1f;
            for (var i = 0; i < positions.Length; ++ i)
            {
                var pos = positions[i];
                pos.z += (delta * deltaTime);
                positions[i] = pos;
            }
        }
    }
}

今回はVector3の配列(長さ100)をUpdateで毎フレーム、z座標にTime.deltaTime * 1加算する処理を 1000回(フレーム)行っても、UnityEditorがクラッシュしたりはしませんでした。 (処理が終了してから次の処理をしているため、かなり時間はかかりますが)

IJobParallelForTransformの実装

Jobには基本的にbittable 型 か NativeContainerしか渡せません。

参照型を渡すことはできません。

Transformは例外的に渡すことができます。

(Unityで開発する上では嬉しい)

今回もz座標だけ更新してみます。

using UnityEngine;
using Unity.Jobs;
using UnityEngine.Jobs;

public class JobTest : MonoBehaviour
{
    private TransformAccessArray transformAccessArray;

    private void Awake()
    {
        var transforms = new Transform[1000];
        for (int i = 0; i < transforms.Length; ++ i)
        {
            var obj = new GameObject();
            transforms[i] = obj.transform;
        }

        transformAccessArray = new TransformAccessArray(transforms);
    }

    private void Update()
    {
        var deltaPositions = new Unity.Collections.NativeArray<Vector3>(transformAccessArray.length, Unity.Collections.Allocator.TempJob);

        for (int i = 0; i < deltaPositions.Length; ++ i)
        {
            deltaPositions[i] = new Vector3(0, 0, Random.Range(0, 1f) * Time.deltaTime);
        }

        var job = new TestJob
        {
            deltaPositions = deltaPositions
        };

        var jobHandle = job.Schedule(transformAccessArray);

        // jobが完了するまで待機
        jobHandle.Complete();

        deltaPositions.Dispose();
    }

    private void OnDestroy()
    {
        transformAccessArray.Dispose();
    }

    private struct TestJob : IJobParallelForTransform
    {
        public Unity.Collections.NativeArray<Vector3> deltaPositions;

        public void Execute(int index, TransformAccess transform)
        {
            var pos = transform.position;
            var delta = deltaPositions[index];
            pos += delta;
            transform.position = pos;
        }
    }
}

まとめ

Job Systemは主にbittable型しか渡せないのは、辛いですね

ただ、Transformの更新はよくあることなので、使えそうな気がしますね

参考

C# Job System - Unity マニュアル