📝

〜UnityのDOTSのドキュメントに触れてみる〜その1

に公開

UnityDOTSというものが気になったので、Unity公式のDOSTのドキュメントを少しずつ読みながら実装していこうと思います。
公式のドキュメント+Google翻訳したドキュメントを使用して読み進めていきます。

UnityDOTSとは

DOTSとは、Unity6から導入されたオブジェクト指向->データ指向に移行する為のUnityの新しい基礎Data-Oriented Tech Stackの略で、現状GameObjectと共存している様子だが、Unityが言うには最終的には完全に移行してしまうとの事、DOTSの構成は下記になっています。

  1. ECS(Entity Component System)
     GameObjectMonoBehaviourに変わるもの、デザインパターン(MVVM/MVP/MVC)のように
     EntityComponentSystemでやる事が分割されているものが用意されている
     (Entitiesパッケージが必要)
  2. C# Job System
     マルチスレッドでの処理や1つの特定の処理を実行する時に使用、簡単にマルチスレッドのコードが書けたりする
     (Jobsパッケージが必要)
  3. Burstコンパイラ
     最適化されたコンパイルをしてくれる、従来のコンパイルよりも高速
     (Burstパッケージが必要)

ドキュメントを読む

ドキュメントを上から読んでいきます。
(基本的にGoogle翻訳したドキュメントで日本語で読んでいきます)

Jobシステム

最初にパフォーマンスの事についての記載がありますが、割愛します。
(何故パフォーマンスが出ないのか、メモリやガベージコレクションの事などの記載があります)

MonoBehaviourの更新はメインスレッドでのみ実行されるため、多くのUnityゲームではゲームロ ジックのすべてが1つのCPUコアで実行されることになります。
追加のコアを活用するには、手動で追加のスレッドを生成して管理することもできますが、安全かつ効率的に行うことは非常に困難です。
より簡単な代替手段として、UnityはC#ジョブシステムを提供します。
 -ジョブシステムは、各コアごとに1つのワーカースレッドのプールを維持します。
ターゲットプラットフォーム。たとえば、Unityが8つのコアで実行される場合、1つのメインスレッ ドと7つのワーカースレッドが作成されます。
 -ワーカースレッドはジョブと呼ばれる作業単位を実行します。ワーカースレッドがアイドル状態のと きは、ジョブキューから次に利用可能なジョブを取得して実行します。
 -ジョブがワーカースレッドで実行を開始すると、完了するまで実行されます。(つまり、ジョブはプリエンプトされません。)

上記は日本語化したドキュメントのJobシステムの解説部分です。
自力でメインスレットを管理する事もできるようですが、Jobシステムを使うと効率良くマルチスレッドを実行してくれるようです。

ドキュメントのソースコード(コメント部分は追記しています)

MultiplicationJob.cs
using Unity.Collections;
using Unity.Jobs;

/// <summary>
/// 2つの配列の要素を乗算するジョブ(IJobを継承してジョブの構造体にする)
/// </summary>
struct MultiplicationJob : IJob
{
    // ガベージコレクションされないネイティブ配列で入力と出力リストを用意
    public NativeArray<float> Input;
    public NativeArray<float> Output;

    /// <summary>
    /// 実行関数
    /// </summary>
    public void Execute()
    {
        // 入力リストで出力リストを乗算する
        for (int index = 0; index < Input.Length; index++)
        {
            Output[index] *= Input[index];
        }
    }
}

解説

  1. Jobを使うにはstruct MultiplicationJob : IJobのようにIJobを継承して使用します。
    classを使用しても良いですがstructにする事を推奨されています。
    (今回はInputでOutputを乗算するJobを作成してます)
  2. NativeArrayなのはガベージコレクションを使わない事でパフォーマンスを向上させています。
  3. public void Execute()JobシステムではExecute関数の実装が必須でExecuteでそのJobでしたい事を実装します。
    (今回はInputでOutputを乗算しています、IJob以外のJobもあるようです)

動作テスト

using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using DOTS.Jobs;

/// <summary>
/// Jobの使用方法
/// </summary>
public class JobExample : MonoBehaviour
{
    [Header("テストデータの個数")]
    [SerializeField]
    int dataNum;
    
    /// <summary>
    /// 初期化
    /// </summary>
    void Start()
    {
        // 入力と出力データを100万配列用意
        var input = new NativeArray<float>(dataNum, Allocator.Persistent);
        var output = new NativeArray<float>(dataNum, Allocator.Persistent);

        // テストデータの初期化
        for (int index = 0; index < dataNum; index++)
        {
            input[index] = index + 1;
            output[index] = 10;
        }

        // ジョブを作成
        Debug.Log("Job作成");
        MultiplicationJob job = new MultiplicationJob
        {
            Input = input,
            Output = output
        };

        // job.Schedule()でjobを実行してJobHandleを保持
        Debug.Log("Jobを実行");
        JobHandle handle = job.Schedule();
        
        // handle.Complete()でjobの完了を待つ
        handle.Complete();
        Debug.Log("Job実行完了");

        // ネイティブ配列の解放(必須)
        Debug.Log("メモリ解放");
        input.Dispose();
        output.Dispose();
    }
}

使用方法解説

Allocatorについて

  1. 通常のクラスを作成するように作成
MultiplicationJob job = new MultiplicationJob
{
    Input = input,
    Output = output
};
  1. JobHandle handle = job.Schedule();でJobを実行して情報をJobHandleで保持
  2. handle.Complete();でJobの完了を待つ
  3. 最後に忘れずにNativeArrayのメモリ解放を行う
input.Dispose();
output.Dispose();

IJob以外のJob

  1. IJobParallelFor:Execute(int index)
  2. IJobChunk:Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
  3. IJobEntity:Execute(ECSの型を必要な数だけ引数に取れる)

Allocatorについて

  1. Temp:1フレームで破棄される、次フレームには使用できない
  2. TempJob:4フレームで破棄される、4フレーム以内に使用する
  3. Persistent:永続的に使用される、Disposeするまで破棄されない

あとがき

ドキュメント内ではIJobしか解説していなかったですが、一番使いそうなJobは並列実行できるとの事なのでIJobParallelForECSを使うのであればIJobEntityだと思います。
IJobに関しては何か決まった物を返すような処理に適しているのかなと思いました。
(役割的にはSystemとかModelが似ていると思います)
ドキュメントを見ると次はBurstコンパイラについてになりそうです。

参考にした記事

ファースト・スクラッチTech Blog

Discussion