【Unity】 JobSystem 調査記録

AvatarPosted by

この記事はGRIPHONE Advent Calendar 2022 21日目の記事です。

初めまして、Unityエンジニアのkudoです。

いきなりですが、皆さんDOTSしていますでしょうか?

いつかやろうと思ってなかなか手を出していない人が多いのではないでしょうか。私もいつかDOTSを勉強しようと思っていたらいつの間にか1年が過ぎていました。。

そろそろ動き出そうと思っていた時にタイミングよくアドベントカレンダーがスタートしたので、ついに私もDOTSの道を一歩踏み出しました!すごい!

とはいってもECSをいきなり学ぶにはコストが高いので、まずはJobSystemから始めよう!ということで今回は重い腰を上げて一歩踏み出した私の議事録をこの記事に書いていこうと思います。

環境

 Unity2021.3.15f1

 jobs Version 0.70.0-preview.7

目次

JobSystem概要

Unityでは基本的にメインスレッド処理が行われています。Update処理中はWorkerスレッドは処理をしておらず、ほとんどIdle状態です。

JobSystemは、画像のように「job」という単位で処理をMainThreadを含んだ各スレッドに振り分けることができます。

CPUの1コアに対して一つずつ「WorkerThread」が与えられます。

マルチスレッドでコードを書くと、高いパフォーマンスを得ることができ、フレームレートの大幅な向上が見込めます。これはすごい!

メモ

JobSystemで可能になるのは「分散処理」です。よく「並列処理」と一緒にされがちですが、似ているようで少し違います。

  • 並列処理
    • 2つのスレッドで別々の処理を並列で行うこと
  • 分散処理
    • 2つのスレッドが同じデータに対して同じ処理を並列で行うこと

↑のように明確な違いがあります。

JobSystemの制約

使う前に、JobSystemには値型しか使えない(classやManagedHeapを渡せない)という制約があります。

どうやらManagedHeapはJob内では使うことができるらしいのですが、「BurstCompiler」の適用ができなくなってしまうそうです。(BurstCompilerはGCが無い前提で最適化を行うため)

また、MainThread以外で動くことが想定されているので、UnityEngineAPIの多くを使うことができません。C言語っぽい実装を要求されます。(GameObjectとかは使えない)

サンプルコード

public struct Ball
{
    public Vector3 position;
    public float radius;
    public float speed;
    
    public void Move(float time)
    {
        // 現在の位置を計算します。
        float xPos = radius * Mathf.Cos(time * speed);
        float zPos = radius * Mathf.Sin(time * speed);

        // ゲームオブジェクトの位置を設定します。
        position = new Vector3(xPos, 0, zPos);
    }
}

↑データをstructで定義。Jobsystemの制約でClassが使えないので基本的にstructになります。

public struct BallJob : IJob
{
    public NativeArray<Ball> Balls;

    public float time;
        
    // 1つのコアで一回実行される
    void IJob.Execute()
    {
        for (int i = 0; i < Balls.Length; i++)
        {
            var ball = Balls[i];
            ball.Move(time);
            Balls[i] = ball;
        }
    }

↑IJobを継承したstructを定義。このジョブでデータを受け取って演算処理をする。

public class TestJobSystem : MonoBehaviour
{
    public GameObject ballPrefab;
    public int ballNum = 1000;

    private NativeArray<Ball> ballArray;
    private Transform[] ballTransform;
    
    private BallJob ballJob;
    private JobHandle jobHandle;
    
    private void Start()
    {
        var pos = transform.position;
        
        //配列の確保
        ballArray = new NativeArray<Ball>(ballNum, Allocator.Persistent);
        ballTransform = new Transform[ballNum];

        for (int i = 0; i < ballNum; i++)
        {
            var ball = new Ball();
            ball.position = pos;
            var random = new Random();
            ball.radius = random.Next(10,100);
            ball.speed = (float)random.NextDouble();

            ballArray[i] = ball;
            var obj = Instantiate(ballPrefab);
            ballTransform[i] = obj.transform;
        }

        ballJob.Balls = ballArray;
    }
    
    private void OnDestroy()
    {
        if (ballArray.IsCreated)
        {
            ballArray.Dispose();
        }
    }
    
    private void Update()
    {
        ballJob.time = Time.time;
        // ジョブをスケジュールする
        var handle = ballJob.Schedule();
        // 実行&完了待ち
        handle.Complete();
        for (int i = 0; i < ballNum; i++)
        {
            ballTransform[i].localPosition = ballArray[i].position;
        }
    }
}

↑ジョブの発行を行い、実行と完了を待つ。

ProfilerでWorker Threadが使われていて、Main Threadと同期も取れていることを確認できます。

まとめ

いかがでしたでしょうか?

JobSystemで割と簡単に分散処理を実装できることがわかりました。

とはいってもまだDOTSの半分も学べていないので引き続きDOTSについての知識を深めていこうと思います。