見出し画像

Unity ハイパフォーマンスゲーム制作入門 Vol.2

はじめに

みなさんUnity DOTSはご存知でしょうか?
本記事「Unity ハイパフォーマンスゲーム制作入門 」は、Unity DOTSについて体系的に理解できる連載シリーズです。

DOTSの代表的な技術

  • ECS(Entity Component System)

  • C# Job System

  • Burst

  • Unity.Mathematics

  • NetCode でのオンライン化

  • Physics

に関して、実践的にゲームを作りながら一緒に学んでいきましょう。

サンプルゲーム「Whales」

最初のサンプルではDOTSのパフォーマンスを活かした魚群シュミレーションを使用したゲームを作ってみたいと思います。通常のUnityでの作り方(GameObject)では動作させることが難しいのですが、DOTSであればパフォーマンスよく動作させることが可能です。イメージとしては以下👇

サンプルゲーム「Whales」

約数100匹の魚の群れに対してプレーヤー達がクジラとなり囲んで漁をする。上(海面)に追い詰めることで捕食することを目的としたゲームで、ザトウクジラのバブルネットフィーディングをイメージしています。
プレイヤーはクジラになり捕食することが目的です。魚の群れは一匹一匹に群れっぽく動くようなシュミレーション処理を行います。
最終的にはNetCodeを用いてオンラインプレイにも対応します。

群れの表現

さて、このようなゲームの難しい点の一つとして挙げられるのが、群れの表現で、今回はBoidsというアルゴリズムを用います。これは各個体が群れに対して

  • 分離(仲間に近づきすぎないようにする)

  • 整列(周りの個体と周りと同じ方向・速度で進みたがる)

  • 結合(周りの個体の中心に行きたがる)

という3つの動作を定義することで群れっぽい動きを再現できます。このアルゴリズムは重いので通常のMonobehaviourなどを用いた実装では、重く、マルチスレッドにするもの実装が大変ですが、C# Job Systemというものを使うことで簡単に、そして1フレーム(16ms)以内で計算できるように出来ます。

そのため、本記事では

  • ECS(Entity Component System)を用いてBoids(群れのアルゴリズム)を実装

  • Burstでマルチスレッド化

を行い、内容を実践的にご紹介します。ECS、パッケージ等についても適宜解説を行っています。
(DOTSの概要は「Unity ハイパフォーマンスゲーム制作入門 Vol.1」を御覧ください)

環境構築

環境構築を行いますが、Unityのインストールは完了しているものとして進めます。(まだの方はこちら👉Unity ハイパフォーマンスゲーム制作入門 Vol.1-2

使用バージョン:

  • Unity 2020.3.315f2(Unity 2019.3.b11以上2021.x未満であれば大丈夫です)

プロジェクトの作成

まずは、プロジェクトを新規作成していきましょう。
(Preview版のパッケージの表示を行っていない方はWindow > Package Manager で Packages タブを開き、 Advanced > Show preview packages にチェックを入れます。)

以下のパッケージをインストールしましょう。一覧にない場合はAdd package from git URL...にパッケージ名(com.unity.xxx)を入力してインストールできます。

  • Entities 0.17.0(com.unity.entities)

  • Hybrid Renderer 0.11.0(​​com.unity.rendering.hybrid)

  • Unity Physics 0.6.0(com.unity.physics)

以上で準備は完了です。

ECS(Entity Component System)の世界へ

Boids(群れのアルゴリズム)を実装

早速作っていきましょう。以下から必要なアセットをダウンロードして、Assets以下に配置してください。(Prefabs、Materialsフォルダ)

次に共有データを格納するスクリプトを作成します。まず、Assets以下にScriptsフォルダを作成しましょう。次に、Create→C# Scriptでスクリプトを作成し、Param.csに変更します。以下の内容をコピーしてください。

using UnityEngine;
 
[CreateAssetMenu(menuName = "Boid1/Param")]
public class Param : ScriptableObject
{
    public float initSpeed = 2f;
    public float minSpeed = 2f;
    public float maxSpeed = 5f;
    public float neighborDistance = 1f;
    public float neighborFov = 90f;
    public float separationWeight = 5f;
    public float wallScale = 5f;
    public float wallDistance = 3f;
    public float wallWeight = 1f;
    public float alignmentWeight = 2f;
    public float cohesionWeight = 3f;
}

終わったら、Create -> Boid1 -> Paramでパラメーターを作成しましょう。(名前は何でも大丈夫です)

では早速、ECSを使った実装に入っていきます。まず、ECSとは何でしょうか?

ECS(Entity Component System)とは

データ指向設計で作られた新しいUnityのコンポーネントシステムが、Entity Component System(ECS)です。ECSはそのままですが、Entity、Component、Systemの三つの要素からなります。

以前のコンポーネントシステム(MonoBehaviour)に馴染みのある方は、Systemが振る舞いを定義するもの、Componentがデータを置いておく場所、Entityはゲームオブジェクトとして理解してください。

これらは別々に実装され、Entityに複数種類のデータ(Component)を設定できます。そして、システムは一定のComponentを持つEntityに対して処理を実行することができます。

例えば float RadiansPerSecond を持つComponentがあり、それが特定のオブジェクト(Entity)に設定されていた場合・・・

Entities.ForEach((ref Rotation rotation, in RotationSpeed_ForEach rotationSpeed) =>
{
    rotation.Value = ...

というようなシステムを定義することができ、デフォルトで設定されているRotationというCompenentに対してその値(rotation.Value)を操作することによってそのオブジェクト回転を制御することが出来ます。

ここからは実際に、スクリプトを作りながら理解していきましょう。
まず、魚群のシュミレーションをする為、魚一匹一匹に必要なパラメータを考えます。今回は Velocity という速度や方向を保存しておくパラメータと、進んでいく方向を決めるパラメータの Acceleration を考えます。パラメータはできるだけ最小単位にばらしたほうがメモリ効率が良いので、分けて定義します。
以下の2つのスプリクトを作成します。

using Unity.Entities;
using Unity.Mathematics;
 
[GenerateAuthoringComponent]
public struct Velocity : IComponentData
{
    public float3 Value;
}
using Unity.Entities;
using Unity.Mathematics;
 
[GenerateAuthoringComponent]
public struct Acceleration : IComponentData
{
    public float3 Value;
}

上記がComponentになります。コンポーネントはすべて IComponentData という interface を継承した構造体になります。

[GenerateAuthoringComponent] というのを忘れないでください。これがあると自動でエディタ上のゲームオブジェクトにアタッチできるようなコードを生成してくれます。(Authoring Componentといい、ECSから従来のコンポーネントへ変換してくれるものです)
このように基本的にECSと既存のコンポーネントとでは互換性がない点が注意点です。
ちなみに、以下がIComponentDataの定義ですが、特に何かがあるわけではありません。内部的には単なる型情報が利用されてコンポーネントを区別しているという感じです。

namespace Unity.Entities
{
    public interface IComponentData
    {
    }
}

また、Velocity、Accelerationコンポーネントで、見慣れないfloat3という型が出てきましたが、これはUnity.Mathematicで定義されているものです。

Unity.Mathematics

Unity.Mathematicsは数学的な処理を、シェーダーのように記述できる型と関数を提供していて、後に出てくるBurst Compiler を利用することで CPUに最適化されたSIMD を使った効率的なコードへと変換してくれます。

基本的に Component で使える型は、Blittable型(int、floatなど)になります。

Unity.MathematicはBurstCompiler、ECSといった最適化のための厳しい条件下での数学的な記述に用いられることを想定したパッケージです。

Componentのアタッチ

さて、次にゲームオブジェクトにComponentをアタッチしてみましょう。ダウンロードした、Boidというプレハブをタブルクリックして開きます。先ほど作成したVelocityとAccelerationのスプリクトをインスペクタにドラッグ&ドロップして、アタッチします。

これで、ゲームオブジェクトにComponentを設定できました。(コードから行う場合は CreateArchetype や CreateEntity、AddComponent で追加できます)

Entityの生成

では次に、このプレハブをEntityへ変換して指定された個数だけ生成するスプリクトを記述します。
以下のスクリプトを作成しましょう。

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
 
public class Bootstrap : MonoBehaviour 
{   
    public GameObject Prefab;
    public static Bootstrap Instance { get; private set; }
    public static Param Param { get { return Instance.param; } }
    [SerializeField] public int boidCount = 100;
    [SerializeField] float3 boidScale = new float3(0.1f, 0.1f, 0.3f);
    [SerializeField] Param param;
    BlobAssetStore blobAssetStore;
 
    void Awake()
    {
        Instance = this;
        blobAssetStore = new BlobAssetStore();
    }
 
    void Start()
    {
        var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, new BlobAssetStore());
        var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, settings);
        var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        var random = new Unity.Mathematics.Random(853);
 
        for (int i = 0; i < boidCount; ++i)
        {
            var instance = entityManager.Instantiate(prefab);
            var position = transform.TransformPoint(random.NextFloat3(1f));
 
            entityManager.SetComponentData(instance, new Translation {Value = position});
            entityManager.SetComponentData(instance, new Rotation { Value = quaternion.identity });
            entityManager.SetComponentData(instance, new NonUniformScale { Value = new float3(boidScale.x, boidScale.y, boidScale.z) });
            entityManager.SetComponentData(instance, new Velocity { Value = random.NextFloat3Direction() * param.initSpeed });
            entityManager.SetComponentData(instance, new Acceleration { Value = float3.zero });
        }
    }
 
    void OnDrawGizmos()
    {
        if (!param) return;
        Gizmos.color = Color.green;
        Gizmos.DrawWireCube(Vector3.zero, Vector3.one * param.wallScale);
    }

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

注目するのは void Start() の中身で、流れとしては・・・

  • GameObjectConversionUtility.ConvertGameObjectHierarchyでPrefabをEntityに変換

  • EntityManagerの取得

  • EntityManager.InstantiatでEntityを生成

  • EntityManager.SetComponentDataで座標(Translation)や回転値を設定

といったところでしょうか。座標もTransformではなくTranslationなのは、ECS専用のComponentだからです。他にも拡大(Scale)はNonUniformScaleというComponentになっています。

ちなみに、OnDrawGizmo() はデバッグ用です。シミュレーションの範囲を描画できるようにを利用しています。

それでは、空のゲームオブジェクトを作成して、名前を変更し、上記のスクリプトをアタッチします。PrefabにはBoidを、Pramには作成したものを設定しましょう。

再生ボタンを押すとBoidが沢山生成されます。(動かないので面白みに欠けますね)

Systemの作成

このままでは面白くないので、ここからは
魚の魚の動きをシュミレーションするようなコードを書いていきましょう。ここでSystemの作成にチャレンジします。
以下のスクリプトを作成します。

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Physics;
 
public class BoidsSimulationSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref Translation translation, ref Rotation rot, ref Velocity velocity, ref Acceleration accel) =>
        {
            var dt = Time.DeltaTime;
            var v = velocity.Value;
            v += accel.Value * dt;
            var dir = math.normalize(v);
            var speed = math.length(v);
            v = math.clamp(speed, Bootstrap.Param.minSpeed, Bootstrap.Param.maxSpeed) * dir;
 
            translation = new Translation { Value = translation.Value + v * dt };
            rot = new Rotation { Value = quaternion.LookRotationSafe(dir, new float3(0, 1, 0)) };
            velocity = new Velocity { Value = v };
            accel = new Acceleration { Value = float3.zero };
        });
    }
}

SystemはSystemBaseというクラスを継承します。また OnUpdate() の中 Entities.ForEach() の引数で、どのComponentを持つEntityを対象とするのかを指定できます。この例では:Translation、Rotation、Velocity、Acceleration をすべて持つEntity全てに対して処理を実行します。またその後ろのカッコに処理を記述していきます。

処理自体は、translationでその一匹の魚に対する座標設定、回転値、velocityはそのままaccelは初期化するような処理をしています。さらにスピードも制御していて、一定以下一定以上にならないようにしています。あとはQuaternion.LookRotationでモデルの回転値を計算しているくらいです。
さて、再生してみましょう、accel が計算されてないのでこのように等速直線運動します。

実はこのSystemはワールドという単位で紐づくようになっていて、ワールドに登録すると処理できるようになります。
しかし、実は現在利用しているデフォルトのワールドでは、システムは自動で登録されます。なのでもう実行すれば動きます。知らないとちょっと気持ち悪く感じる挙動ですが、デフォルトのワールドではプロトタイピングを早くするためか、このような仕組みが用意されています。

ワールド

ワールドは 以前登場したEntityManager(Entity、Componentを管理)、Systemを包含しているものです。
ワールドはいくつでも作成できますが、ワールド同士の直接的な干渉は出来ません。

アーキタイプ

先程の説明で Componentを持つEntityを対象とするのかを指定 とありましたが、実はこれをアーキタイプと呼びます。アーキタイプとはその名の通りEntityの雛形、エンティティ内のコンポーネントの組み合わせを指します。

Entityとはコンテナのようなもので、EntityにComponentを追加したり削除する事で、Entityの振る舞いを作ることが出来るようになっていますが・・・。

実はEntityにはIDが設定されているだけです。Entity ID = 2と要求すれば必要なComponentに簡単にアクセス出来るという感じです。この辺りはEntityManagerが管理してくれています。

Component自体は配列になっていて、メモリにある程度まとまっているので一気にキャッシュに乗せて一気に処理するといった事が可能になっています。(実際はもっと複雑)

その配列を管理するのがチャンクと呼ばれるものです。

チャンク

Componentを格納するメモリ空間を管理するもので、同じアーキタイプのEntityしか格納しないようになっています。

メモリ割り当ては、16KB のチャンク単位で行われます。各チャンクに含まれるのは、単一のアーキタイプのEntityのComponentデータだけです。

そして、各アーキタイプには、そのアーキタイプのEntityが格納されているチャンクのリストを持っていて、そのチャンクをすべてループ処理し、各チャンク内でComponentデータの読み取りと書き込みを行います。

このような構造にしておくと、多くの場合マルチスレッドで処理しやすく、ECS コンポーネントを操作するコードは 100% 近いコア使用率で実行されます。

まとめると、Componentを連続的にメモリに並べて、キャッシュミスを減らし、処理速度を向上させ、マルチスレッドでも処理しやすい、といったところでしょうか。

かなり脱線しましたが、システムの作成の方に戻っていきます。

仮想の壁を作る

次は、Boid達を範囲内におさめるための仮想の壁に対して、壁に近づけば近づくほど離れる方向(壁の内側方向)の力を受けることにして accel を計算していきます。以下を BoidsSimulationSystem に追記します。

public class WallSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref Translation pos, ref Acceleration accel) =>
        {
            var scale = Bootstrap.Param.wallScale * 0.5f;
            var thresh = Bootstrap.Param.wallDistance;
            var weight = Bootstrap.Param.wallWeight;
 
           accel = new Acceleration
            {
                Value = accel.Value +
                    GetAccelAgainstWall(-scale - pos.Value.x, new float3(+1, 0, 0), thresh, weight) +
                    GetAccelAgainstWall(-scale - pos.Value.y, new float3(0, +1, 0), thresh, weight) +
                    GetAccelAgainstWall(-scale - pos.Value.z, new float3(0, 0, +1), thresh, weight) +
                    GetAccelAgainstWall(+scale - pos.Value.x, new float3(-1, 0, 0), thresh, weight) +
                    GetAccelAgainstWall(+scale - pos.Value.y, new float3(0, -1, 0), thresh, weight) +
                    GetAccelAgainstWall(+scale - pos.Value.z, new float3(0, 0, -1), thresh, weight)
            };
        }).WithoutBurst().Run();
    }
    private float3 GetAccelAgainstWall(float dist, float3 dir, float thresh, float weight)
    {
        if (dist < thresh)
        {
            return dir * (weight / math.abs(dist / thresh));
        }
        return float3.zero;
    }
}

さて、2 つシステムが出来ました。実行順としては WallSystem で加速度を更新してから BoidsSimulationSystem で処理して欲しくなります。この場合はシステムにアトリビュートをつけて制御することが出来ます。以下を追記します。

[UpdateBefore(typeof(BoidsSimulationSystem))]
public class WallSystem : SystemBase
{
    ...

これでWallSystemがBoidsSimulationSystemより前に実行されるようになります。実行すると・・・

壁にとどまるようになったのが分かりますね。さて、ここでC# Job Systemによる並列処理への対応をしていきたいと思います。

C# Job System

実はこれまでに作成したSystem達は全てメインスレッドで実行されており(WithoutBurst().Run() で気づいていたかとは思いますが…)、かつコードの最適化もされておらず ECS 化した恩恵をあまり受けることが出来ていません。

ECS 化することによる真の恩恵は、並列化を行う Job System の対応を行いやすい点と、SIMD 等の最適化を最大限効かせることが可能な Burst Compiler を簡単に利用できる点にあります。

Job Systemはこれまでも使えたスレッドなどの代わりに ジョブ を作成してマルチスレッドコードを記述できます。

論理コア(CPU)ごとに 1 つのワーカースレッドがあり、そこで作成したコードを動かしてくれます。そのためCPUコアより多くのスレッドを作成すること(CPU リソースの競合)を回避できます。

計算を安全に行うために、Job内部で安全に利用できる型には制約があり、コピーできるのは blittable型のみです。そして、各ジョブに操作の必要なデータのコピーを送信することでスレッドセーフを実現します。

それでは実際に作成していきます。

ジョブ化

それではまず、BoidsSimulationSystem をJob化させていきます。これは簡単で .WithoutBurst().Run() を .ScheduleParallel() に書き換えるだけでOKです。ScheduleParallel() とすると複数スレッド、Schedule() とすると単一スレッドのみで動作させることができます。
Entities.ForEachの中では参照型が使えないため、以下のようにします。
(.WithName()は処理に名前を付け、プロファイラーで見たときに分かりやすいようにしています)

public class BoidsSimulationSystem : SystemBase
{
    protected override void OnUpdate()
    {
        var dt = Time.DeltaTime;
        var minSpeed = Bootstrap.Param.minSpeed;
        var maxSpeed = Bootstrap.Param.maxSpeed;
        
        Entities
            .WithName("MoveJob")
            .ForEach((ref Translation translation, ref Rotation rot, ref Velocity velocity, ref Acceleration accel) =>
            {
                var v = velocity.Value;
                v += accel.Value * dt;
                var dir = math.normalize(v);
                var speed = math.length(v);
                v = math.clamp(speed, minSpeed, maxSpeed) * dir;

                translation = new Translation { Value = translation.Value + v * dt };
                rot = new Rotation { Value = quaternion.LookRotationSafe(dir, new float3(0, 1, 0)) };
                velocity = new Velocity { Value = v };
                accel = new Acceleration { Value = float3.zero };
            }).ScheduleParallel();
    }
}

ジョブのスケジュール

Schedule を呼び出すと、適切な時点で実行するためのジョブキューにジョブを加えます。一旦スケジュールすると、ジョブを中断することはできません。Schedule はメインスレッドからのみ呼び出すことができます。

Scheduleをしても、実際にはJobは実行されません。Jobを開始するにはCompleteを呼び出すと行われます。

また、ForEach() の中のラムダは1回呼び出されるだけではなく、その対象となるEntityに対して1回呼び出されるという点に注意です。(つまり並列化が可能)

複数のジョブ

次の WallSystem もジョブ化してみましょう。こちらは少し工夫が必要です。ジョブはまとめることができるので、まずは、BoidsSimulationSystem に WallJob を作成しましょう。ついでに、public class WallSystem … は削除します。
ジョブの作成方法は、WallJob という IJobChunk を実装した構造体を作成します。これは先程のEntity.ForEachよりも柔軟に処理を書く事ができる方法です。その構造体内の Execute() メソッド内に具体的な処理を書きます。

public class BoidsSimulationSystem : SystemBase
{
    private struct WallJob : IJobChunk
    {
        public ComponentTypeHandle<Acceleration> AccelerationTypeHandle;
        [ReadOnly] public ComponentTypeHandle<Translation> TranslationTypeHandle;
        [ReadOnly] public float scale;
        [ReadOnly] public float thresh;
        [ReadOnly] public float weight;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var chunkAccele = chunk.GetNativeArray(AccelerationTypeHandle);
            var chunkTrans = chunk.GetNativeArray(TranslationTypeHandle);
            for (var i = 0; i < chunk.Count; i++)
            {
                var trans = chunkTrans[i];
                chunkAccele[i] = new Acceleration
                {
                    Value = chunkAccele[i].Value +
                        GetAccelAgainstWall(-scale - trans.Value.x, new float3(+1, 0, 0), thresh, weight) +
                        GetAccelAgainstWall(-scale - trans.Value.y, new float3(0, +1, 0), thresh, weight) +
                        GetAccelAgainstWall(-scale - trans.Value.z, new float3(0, 0, +1), thresh, weight) +
                        GetAccelAgainstWall(+scale - trans.Value.x, new float3(-1, 0, 0), thresh, weight) +
                        GetAccelAgainstWall(+scale - trans.Value.y, new float3(0, -1, 0), thresh, weight) +
                        GetAccelAgainstWall(+scale - trans.Value.z, new float3(0, 0, -1), thresh, weight)
                };
            }
        }
…

WallJob を作成しました。基本的な流れは、チャンクごとにArchetypeChunkがExecute()関数に渡され、そのチャンクに格納されている配列を直接更新します。ちなみに IJobEntityBatch でも同様に書けます(エンティティベースで処理するか、チャンクベースで処理するかの違いしかありません)
そして、読み取りしかしなければ [ReadOnly] を付けましょう。(書き込みのみなら [WriteOnly])自動的にジョブを効率化してくれます。

そして OnUpdate() に、こんな感じで追記します。ここでどのコンポーネントを含むチャンクを対象にするのかの指定と、ジョブを呼び出してスケジュールします。

    ...
    protected override void OnUpdate()
    {
        var wall = new WallJob
        {
            AccelerationTypeHandle = GetComponentTypeHandle<Acceleration>(),
            TranslationTypeHandle = GetComponentTypeHandle<Translation>(true),
            scale = Bootstrap.Param.wallScale * 0.5f,
            thresh = Bootstrap.Param.wallDistance,
            weight = Bootstrap.Param.wallWeight,
        };
        this.Dependency = wall.ScheduleParallel(wallQuery, this.Dependency);
        ...

ジョブの Schedule を呼び出すとJobHandleを返すので、JobHandleをパラメーターとして Schedule に渡すことで、別のジョブとの依存関係を表現できます。Entities.ForEach(またはJob.WithCode)を使う場合ではthis.Dependencyに勝手に依存関係が指定されますが、自分で作ったジョブはその限りではないため、こちら側から指定しています。(詳しくはこちらのマニュアル参照
次に wallQuery を定義します。OnCreate() でEntityQueryの設定をしましょう。これは特定のComponentを持つEntityを探してこれると言ったようなものです。GetEntityQuery で検索条件を指定します。ここでは Translation と Acceleration を持つチャンクを処理の対象とします。

public class BoidsSimulationSystem : SystemBase
{
    EntityQuery wallQuery;

    protected override void OnCreate()
    {
        wallQuery = GetEntityQuery(typeof(Translation), typeof(Acceleration));
    }

    …

これで主なジョブ化は完了です。次にBurstに対応させていきたいと思います。

Burst

Job Systemと共に使うと高速化できます。LLVMというコンパイル基盤(コンパイラを作るためのフレームワーク)を使い最適化してくれて、Unity.Mathematicsを使う場合はSIMDで高速化してくれます。SIMDとは、1つの命令で複数の計算を行う仕組みのことで、例えば、4次元ベクトル同士の加算を実行する場合32ビットのレジスタ幅では4回の加算命令が必要になるが、128ビットのレジスタで一回で済むようになる、というものです。

また、メモリエイリアスも考慮するらしいです。
サポートれている型は基本的に、プリミティブ型、ベクトル型(Unity.Mathematics)、列挙型、構造体などで、マネージドなものはサポートされていません。(stringなど)
制御は通常のC#制御フローはほどんどカバーしていますが、例外処理がthow以外できません。

Burstに対応させる方法は簡単です。先程のジョブの直前に [BurstCompile] をつけるだけです。

    [BurstCompile]
    private struct WallJob : IJobChunk
    {
        ...

これで対応できました。MoveJob に関してはEntities.ForEachで書いていたためすでにBurst対応されています。(されていなかった場合は、.WithBurst() を付けてみてください)

ここまでで基礎的なところは全てできたので、後はBoidシュミレーションの方を実装していきましょう。基本的な移動処理と、仮想の壁を設けるところまでは進みました。次は近隣の個体からの影響を考えていきます。

ここまで実装した BoidsSimulationSystem はこちら👇

カラム

ISystemBase

ISystemBaseはEntities 0.17で追加された、メインスレッドの更新をバーストコンパイルできる構造体ベースのシステムを作成するためのインターフェースです。これによりシステムはクラスではなく、構造体となりさらなる高速化が期待できます。
このパートで実装した BoidsSimulationSystem をISystemBaseで書き直してみるのも良いでしょう。

SystemBase
ISystemBase(10ms以上高速化)

Vol.2-2はこちら👇

この記事が気に入ったらサポートをしてみませんか?