Entities 1.0を触ってみる [Unity]

# はじめに

今年の秋頃のこと、DOTSの中核をなすパッケージの1つである**Entities**の待望のバージョン1.0がついにリリースされました(プレリリース段階です)。

https://forum.unity.com/threads/experimental-entities-1-0-is-available.1341065/

使ってみたいと思いつつECS関連の技術については殆ど見ているだけだったので、今回は下記の公式のマニュアルを読みながら新しくリリースされた**Entities 1.0**に触ってみようと思います。

https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/ecs-workflow-example.html

このマニュアルの手順に従って、実行時に等時間隔でEntityを生成する仕組みを実装します。

# 1. サブシーンを作る
サブシーンはGameObjectをEntityに変換するために用意されている機能の1つで、ビルド時にシーンごとEntityに変換するということを行うらしいです。
まず任意のシーンを開き、ヒエラルキー上で右クリックから`New Sub Scene/Empty Scene`でサブシーンを作成できます。
![スクリーンショット 2022-12-23 231043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/d06de062-c932-81e2-0d6c-f746f93bddab.png)

# 2. コンポーネントを作る
`IComponentData`インターフェースを実装した構造体`Spawner`を作成します。
これはECSのC(Component)にあたり、データを保持しておくための構造体になります。
実装は以下のとおりです。

```CS:Spawner.cs
using Unity.Entities;
using Unity.Mathematics;

public struct Spawner : IComponentData
{
    public Entity Prefab;
    public float3 SpawnPosition;
    public float NextSpawnTime;
    public float SpawnRate;
}
```

# 3. Spawner Entityを作る

まず、通常通りの`MonoBehaviour`を継承したスクリプトを作成します。
これは、Component(ECS)に渡す値をインスペクタから設定できる形で保持するために用意するスクリプトで、ここでは`SpawnerAuthoring`という名前で作成しています。

```CS:SpawnerAuthoring.cs
using UnityEngine;

public class SpawnerAuthoring : MonoBehaviour
{
    public GameObject Prefab;
    public float SpawnRate;
}
```

次に、`Baker`を継承したクラス`SpawnerBaker`を作成します。
このクラスには`SpawnerAuthoring`からの値取り込み及びComponent(ECS)のアタッチ処理を記述しています。

```CS:SpawnerBaker.cs
using Unity.Entities;

public class SpawnerBaker : Baker<SpawnerAuthoring>
{
    public override void Bake(SpawnerAuthoring authoring)
    {
        AddComponent(new Spawner
        {
            // デフォルトでは、各オーサリングGameObjectはEntityになる。
            // GameObject（またはオーサリングComponent）が与えられると、GetEntityはEntityを検索して返す。
            Prefab = GetEntity(authoring.Prefab),
            SpawnPosition = authoring.transform.position,
            NextSpawnTime = 0.0f,
            SpawnRate = authoring.SpawnRate
        });
    }
}
```

そして、空のGameObjectをサブシーンに追加します。
![スクリーンショット 2022-12-23 211910.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/22d3b242-d6ca-d20d-e212-9bf41e52f3ba.png)

そこに先程作った`SpawnerAuthoring`をアタッチして、`Prefab`プロパティに生成したいプレハブを、`Spawn Rate`プロパティに生成間隔を設定します。
![スクリーンショット 2022-12-23 211942.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/c40bce28-38bb-bbd7-8e12-697d1347178a.png)

# 4. Spawner Systemを作る
`ISystem`インターフェースを実装した構造体`SpawnerSystem`を作成します。

```CS:SpawnerSystem.cs
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;

[BurstCompile]
public partial struct SpawnerSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
    }

    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // Spawnerの全Componentを問い合わせる。
        // このSystemでは、Componentへの読み取りと書き込みを行いたいので、RefRWを使用する。
        // Systemが読み取り専用のみを要するなら、RefROを使用する。
        foreach (RefRW<Spawner> spawner in SystemAPI.Query<RefRW<Spawner>>())
        {
            ProcessSpawner(ref state, spawner);
        }
    }

    private void ProcessSpawner(ref SystemState state, RefRW<Spawner> spawner)
    {
        // 時間経過で次のスポーンを行う。
        if (spawner.ValueRO.NextSpawnTime < SystemAPI.Time.ElapsedTime)
        {
            // Spawnerの位置に新しいEntityを生成する。
            Entity newEntity = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);
            state.EntityManager.SetComponentData(newEntity, LocalTransform.FromPosition(spawner.ValueRO.SpawnPosition));

            // 次にスポーンするまでの時間をリセットする。
            spawner.ValueRW.NextSpawnTime = (float)SystemAPI.Time.ElapsedTime + spawner.ValueRO.SpawnRate;
        }
    }
}
```

ちなみにこれはpartialな構造体で、コンパイル時にコンパイラが以下のようなコードを自動生成します(下記は自分の環境の場合です)。

```CS:SpawnerSystem(自動生成)
#pragma warning disable 0219
#line 1 "Temp\GeneratedCode\Assembly-CSharp"
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;

[global::System.Runtime.CompilerServices.CompilerGenerated]
public partial struct SpawnerSystem : Unity.Entities.ISystem, Unity.Entities.ISystemCompilerGenerated
{
    [Unity.Entities.DOTSCompilerPatchedMethod("OnUpdate_ref_Unity.Entities.SystemState")]
    void __OnUpdate_6E994214(ref SystemState state)
    {
            #line 22 "[Cからのパス]\SpawnerSystem.cs"
            __Container_716274562_0_RW_TypeHandle.Update(ref state);
            #line hidden
            Container_716274562_0.CompleteDependencyBeforeRW(ref state);
            #line hidden
            foreach (RefRW<Spawner> spawner in Container_716274562_0.Query(__query_716274562_0, __Container_716274562_0_RW_TypeHandle))
            {
                #line 24 "[Cからのパス]\SpawnerSystem.cs"
                ProcessSpawner(ref state, spawner);
            }
    }

    [Unity.Entities.DOTSCompilerPatchedMethod("ProcessSpawner_ref_Unity.Entities.SystemState_Unity.Entities.RefRW<Spawner>")]
    private void __ProcessSpawner_230D30CE(ref SystemState state, RefRW<Spawner> spawner)
    {
        #line 31 "[Cからのパス]\SpawnerSystem.cs"
        if (spawner.ValueRO.NextSpawnTime < state.WorldUnmanaged.Time.ElapsedTime)
        {
            #line 34 "[Cからのパス]\SpawnerSystem.cs"
            Entity newEntity = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);
            #line 35 "[Cからのパス]\SpawnerSystem.cs"
            state.EntityManager.SetComponentData(newEntity, LocalTransform.FromPosition(spawner.ValueRO.SpawnPosition));
            #line 38 "[Cからのパス]\SpawnerSystem.cs"
            spawner.ValueRW.NextSpawnTime = (float)state.WorldUnmanaged.Time.ElapsedTime + spawner.ValueRO.SpawnRate;
        }
    }

    #line 41 "Temp\GeneratedCode\Assembly-CSharp"
    readonly struct Container_716274562_0
    {
        public struct ResolvedChunk
        {
            public Unity.Collections.NativeArray<Spawner> item1_NativeArray;
            public Unity.Entities.RefRW<Spawner> this[int index] => new RefRW<Spawner>(item1_NativeArray, index);
        }

        public struct TypeHandle
        {
            Unity.Entities.ComponentTypeHandle<Spawner> item1_ComponentTypeHandle_RW;
            public TypeHandle(ref Unity.Entities.SystemState systemState, bool isReadOnly)
            {
                item1_ComponentTypeHandle_RW = systemState.GetComponentTypeHandle<Spawner>(isReadOnly);
            }

            public void Update(ref Unity.Entities.SystemState systemState)
            {
                item1_ComponentTypeHandle_RW.Update(ref systemState);
            }

            public ResolvedChunk Resolve(Unity.Entities.ArchetypeChunk archetypeChunk)
            {
                var resolvedChunk = new ResolvedChunk();
                resolvedChunk.item1_NativeArray = archetypeChunk.GetNativeArray(ref item1_ComponentTypeHandle_RW);
                return resolvedChunk;
            }
        }

        public static Enumerator Query(Unity.Entities.EntityQuery entityQuery, TypeHandle typeHandle) => new Enumerator(entityQuery, typeHandle);
        public struct Enumerator : global::System.Collections.Generic.IEnumerator<Unity.Entities.RefRW<Spawner>>
        {
            Unity.Entities.EntityQueryEnumerator _entityQueryEnumerator;
            TypeHandle _typeHandle;
            ResolvedChunk _resolvedChunk;
            public Enumerator(Unity.Entities.EntityQuery entityQuery, TypeHandle typeHandle)
            {
                _entityQueryEnumerator = new Unity.Entities.EntityQueryEnumerator(entityQuery);
                _typeHandle = typeHandle;
                _resolvedChunk = default;
            }

            public void Dispose() => _entityQueryEnumerator.Dispose();
            public bool MoveNext()
            {
                if (_entityQueryEnumerator.MoveNextHotLoop())
                    return true;
                return MoveNextCold();
            }

            [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
            bool MoveNextCold()
            {
                var didMove = _entityQueryEnumerator.MoveNextColdLoop(out ArchetypeChunk chunk);
                if (didMove)
                    _resolvedChunk = _typeHandle.Resolve(chunk);
                return didMove;
            }

            public Unity.Entities.RefRW<Spawner> Current
            {
                get
                {
                    _entityQueryEnumerator.CheckDisposed();
                    return _resolvedChunk[_entityQueryEnumerator.IndexInChunk];
                }
            }

            public Enumerator GetEnumerator() => this;
            public void Reset() => throw new global::System.NotImplementedException();
            object global::System.Collections.IEnumerator.Current => throw new global::System.NotImplementedException();
        }

        public static void CompleteDependencyBeforeRW(ref SystemState state)
        {
            state.EntityManager.CompleteDependencyBeforeRW<Spawner>();
        }
    }

    Unity.Entities.EntityQuery __query_716274562_0;
    Container_716274562_0.TypeHandle __Container_716274562_0_RW_TypeHandle;
    public void OnCreateForCompiler(ref SystemState state)
    {
        __query_716274562_0 = state.GetEntityQuery(new Unity.Entities.EntityQueryDesc{All = new Unity.Entities.ComponentType[]{Unity.Entities.ComponentType.ReadWrite<Spawner>()}, Any = new Unity.Entities.ComponentType[]{}, None = new Unity.Entities.ComponentType[]{}, Options = Unity.Entities.EntityQueryOptions.Default});
        __Container_716274562_0_RW_TypeHandle = new Container_716274562_0.TypeHandle(ref state, isReadOnly: false);
    }
}
```

これでEntityを生成する準備ができたので、早速プレイしてみます。

![スクリーンショット 2022-12-23 232345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/925b5510-be3e-88cd-a344-a2104e492207.png)

無事に設定したプレハブ(Sphere)が生成されました。
通常のヒエラルキーではなく`Entities Hierarchy`ビューを使うことで、生成しているEntityが詳しく見られます。

# 触ってみた感想
Entitiesのバージョンが0.1幾つの時にほんの少し触った記憶があるのですが、その時よりもECSを扱うためのGUIの環境がかなり整備されてきているように感じました。
正式リリースまでに色々使ってみたいと思います。