[Unity] 現在(Entities 0.50)のECS環境でシンプルな Boids Simulation を書く

ずいぶん前に Unity ECS の勉強をしたときに[hecomiさんのBoidsシミュレーション](http://tips.hecomi.com/entry/2018/12/23/200817)を題材に使わせていただいたのですが、UnityのDOTS環境がどんどん変わっていって現行環境に合わせて大規模なリファクタリングが必要になったので、その際の情報を備忘録としてまとめます。

# 2022/4/16 追記
現在の実装でだいぶ構造が変わったので設計のお話として新しく記事を書きました。
[[Unity] Entities 0.50 環境で Boids Simulation を再設計した話](https://qiita.com/kawai125/items/6746e0590382d678041f)

# 環境 (2022/3/24 時点)
- Unity 2020.3.31f
  - Hybrid Renderer 0.50-preview.24
  - Entities 0.50.0-preview.24

# 当方の実装結果
[GitHub](https://github.com/kawai125/Boids-ECS-hecomi)

# 現在のECSの書き方

## ▽Prefab Entity の準備

```C#
public class Bootstrap : MonoBehaviour
{
    public static Bootstrap Instance { get; private set; }
    public static Param Param {  get { return Instance.param; } }

    // Inspector で Prefab を設定しておく
    [SerializeField] GameObject prefab_obj;

    // Prefab Entity 保持用の変数
    private Entity prefab_entity;

    // Boid の全体数の管理用
    private int n_boid;

    void Awake()
    {
        Instance = this;
    }

    public void Start()
    {
        var world = World.DefaultGameObjectInjectionWorld;
        var manager = world.EntityManager;

        // convert prefab_obj -> prefab_entity
        prefab_entity = GameObjectConversionUtility.ConvertGameObjectHierarchy(
            prefab_obj,
            GameObjectConversionSettings.FromWorld(world, null)
        );

        // add user defined component
        manager.AddComponent<BoidPrefabType>(prefab_entity);  // Prefabであることを示す空のComponent
        manager.AddComponent<Scale>(prefab_entity);
        manager.AddComponent<Velocity>(prefab_entity);
        manager.AddComponent<Acceleration>(prefab_entity);
        manager.AddComponent<NeighborsEntityBuffer>(prefab_entity);

        n_boid = 0;
    }
}
```

たびたび `Hybrid Renderer` が使用する `Component` が追加されているようで、自力で `ArchType` を組み立てるやり方だと、適当にアップグレードしたときに描画されなくなります。
また、 `Hybrid Renderer` も設定(V2を使うか否か)によって動かない場合があり、原因の特定に手間取りました。

公式サンプルもそうですが、Hybrid ECS用のAPIを活用してGameObjectから必要な `ArchType` を構築すれば上記の仕様変更にも自動的に追従できることになり、ほんの少しリスクが減ります。

PrefabなのでSystemの操作対象に紛れ込まないよう、空のコンポーネント `BoidPrefabType` をつけています。

## ▽Entityの生成、破棄

```C#

public class Bootstrap : MonoBehaviour
{
    public static Bootstrap Instance { get; private set; }
    public static Param Param {  get { return Instance.param; } }

    [SerializeField] public float boidScale = 1.0f;
    [SerializeField] public Param param;

    [SerializeField] GameObject prefab_obj;
    private Entity prefab_entity;

    private int n_boid;
    private Unity.Mathematics.Random random;

    /* 略 */

    void UpdateBoidNum(int n_tgt)
    {
        if (n_tgt < 0) return;

        var manager = World.DefaultGameObjectInjectionWorld.EntityManager;

        int n_diff = n_tgt - n_boid;

        if (n_diff > 0)
        {
            Debug.Log($"update boids num: add {n_diff} boids.");

            var scale = this.boidScale;
            var initSpeed = this.param.initSpeed;

            for (int i = 0; i < n_diff; i++)
            {
                var entity = manager.Instantiate(prefab_entity);

                // BoidPrefabType を BoidType に付け替え
                manager.RemoveComponent<BoidPrefabType>(entity);
                manager.AddComponent<BoidType>(entity);

                // 適当に値を設定
                manager.SetComponentData(entity, new Translation { Value = this.random.NextFloat3(1f) });
                manager.SetComponentData(entity, new Rotation { Value = quaternion.identity });
                manager.SetComponentData(entity, new Scale { Value = scale });
                manager.SetComponentData(entity, new Velocity { Value = this.random.NextFloat3Direction() * initSpeed });
                manager.SetComponentData(entity, new Acceleration { Value = float3.zero });
            }
        }
        if (n_diff < 0)
        {
            int n_delete = -n_diff;

            Debug.Log($"update boids num: remove {n_delete} boids.");

            var entity_query = manager.CreateEntityQuery(new EntityQueryDesc
            {
                All = new[]
                {
                    ComponentType.ReadOnly<BoidType>()
                }
            });
            var entities = entity_query.ToEntityArray(Allocator.Temp);
            manager.DestroyEntity(new NativeSlice<Entity>(entities, n_tgt));

            entities.Dispose();
        }

        n_boid = n_tgt;
    }
}
```

Entityの生成については元の実装と大して違いはありません。
破棄については、今回は全体の数だけを考えて増減させるため、 `BoidType` に該当するもの全体の情報にアクセスする必要があり、そのために `EntityQuery` の生成 -> 対象となる `NativeArray<Entity>` の取得という流れで処理しています。

このへんは、状況に合わせて[公式サンプル](https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/ECSSamples/Assets/HelloCube)の `SpawnFromMonoBehaviour` , `SpawnFromEntity`, `SpawnAndRemove` あたりを参考にするのがよいでしょう。

## ▽Entities.ForEach によるジョブ実装例

まず、`entity` 自身の情報が主な場合、今回の例でいえば `MoveSystem` の実装は下記のとおり。

```C#
[UpdateAfter(typeof(BoidSystemGroup))]
public partial class MoveSystem : SystemBase
{
    protected override void OnUpdate()
    {
        // パラメータをローカル変数として置いておき、ラムダ式にキャプチャさせる
        float dt = Time.DeltaTime;
        float minSpeed = Bootstrap.Param.maxSpeed;
        float maxSpeed = Bootstrap.Param.maxSpeed;

        // ジョブの定義
        Dependency = Entities.
            WithName("MoveJob").
            WithAll<BoidType>().
            WithBurst().
            ForEach((ref Translation pos, ref Rotation rotate, ref Velocity vel, ref Acceleration accel) =>
        {
            vel.Value += accel.Value * dt;

            var dir = math.normalize(vel.Value);
            var speed = math.length(vel.Value);
            vel.Value = math.clamp(speed, minSpeed, maxSpeed) * dir;

            pos.Value += vel.Value * dt;

            var rot = quaternion.LookRotationSafe(dir, new float3(0, 1, 0));

            rotate.Value = rot;
            accel.Value = float3.zero;
        }).ScheduleParallel(Dependency);
    }
}
```

`float` や `int` など単純な `struct` は適当に`OnUpdate()` 関数のローカル変数においておけば、同じスコープで書くラムダ式でキャプチャしてくれるので簡単に使えます。

ラムダ式の引数は、C#の仕様かEntitiesの仕様かはわかりませんが、
**値渡し(実質 Entity 型のみ)、ref 渡し、in 渡し**
の順番で並べる必要があります。また、`Entity` 型以外を値渡しすると、処理中で書き換える引数に `ref` を、書き換えない引数には `in` をつけるように警告されます。

`Entities` と `ForEach()` の間にある `With**` 関数でエンティティクエリに属性を追加できます。
主なものは[Reference](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.SystemBase.Entities.html)によれば下表のとおり。

|エンティティクエリ|効果|
|:-|:-|
|WithName(string)|Jobの名前を指定。プロファイラやEntityDebuggerでこの名前が使われる|
|WithOutBurst|Burst を無効化。デバッグ時やBurstに対応できない処理に使う (デフォルトではBurstが有効)|
|WithBurst(FloatMode, FloatPrecision, bool)|Burst Compiler に浮動小数点に関する設定とコンパイルのタイミングを指定する|
|||
|WithAll\<T1, T2, T3\>|処理対象のEntityはすべてのComponentTypeを持つ必要がある|
|WithAny\<T1, T2, T3\>|処理対象のEntityはいずれかのComponentTypeを持つ必要がある|
|WithNone\<T1, T2, T3\>|処理対象のEntityはいずれのComponentTypeも含まない必要がある|
|WithChangeFilter\<T1, T2\>|指定されたComponentが前回のSystem実行時に変更されたものだけを処理する|
|WithSharedComponentFilter(ISharedComponentData)|ISharedComponentDataが特定の値のEntityだけを処理する|
|WithEntityQueryOptions(EntityQueryOptions)|[EntityQueryOptions](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.EntityQueryOptions.html)に合致するものだけを処理する|
|WithStoreEntityQueryInField(EntityQuery)| `SystemBase.EntityQuery` に ForEach実行時に作成された EntityQuery を保存する。 対象Entityの全体数など、[EntityQuery](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.EntityQuery.html)のインターフェイスを使いたい場合に用いる|

上表中の `<T1, T2, T3>` は `<T>` または `<T1, T2>` に型パラメータを省略できます。(最大数は表中のとおり)

次に、自身以外の `Entity` の情報も必要な処理の例として、 `CohesionSystem` の実装は下記のとおり。

```C#
[UpdateInGroup(typeof(BoidSystemGroup))]
public partial class CohesionSystem : SystemBase
{
    private struct CohesionDataContainer
    {
        [ReadOnly] public float alignmentWeight;
        [ReadOnly] public ComponentDataFromEntity<Translation> positionFromGrovalEntity;
    }

    protected override void OnUpdate()
    {
        var common_data = new CohesionDataContainer
        {
            alignmentWeight = Bootstrap.Param.alignmentWeight,
            positionFromGrovalEntity = GetComponentDataFromEntity<Translation>(true),
        };

        Dependency = Entities.
            WithName("CohesionJob").
            WithAll<BoidType>().
            WithBurst().
            ForEach(
        (ref Acceleration accel, in Translation pos, in DynamicBuffer<NeighborsEntityBuffer> neighbors) =>
        {
            if (neighbors.Length == 0) return;

            float3 pos_avg = float3.zero;
            float3 pos0 = pos.Value;
            float3 acc = accel.Value;

            for(int i=0; i<neighbors.Length; i++)
            {
                pos_avg += common_data.positionFromGrovalEntity[neighbors[i].entity].Value;
            }
            pos_avg /= neighbors.Length;

            acc += (pos_avg - pos0) * common_data.alignmentWeight;

            accel = new Acceleration { Value = acc };
        }
        ).ScheduleParallel(Dependency);
    }
}
```

ここで、 `Entity` 経由で他の Boid の `Translation` を参照するため、全EntityのTranslationへのアクセサを `SystemBase.GetComponentDataFromEntity<Translation>(isReadOnly)` で取得しています。このSystemでは `Translation` は書き換えないので `isReadOnly = true` です。この場合代入先は `[ReadOnly]` アトリビュートが付加されている必要があり、そのままでは関数内の一時変数には代入できません。
そのため、このデータの代入先として `struct CohesionDataContainer` の中に [ReadOnly] メンバを用意し、 `CohesionDataContainer` を一時変数として作ることでラムダ式にキャプチャさせることができます。

## ▼IJobEntityBatch の例 (非推奨)

`IJobEntityBatch` を使って `CohesionSystem` を書くと下記のようになります。

```C#
[UpdateInGroup(typeof(BoidSystemGroup))]
public partial class CohesionSystem : SystemBase
{
    // チャンク検索のため EntityQuery が必須 (IJobEntityBatch.Schedule()の引数)
    private EntityQuery query;

    protected override void OnCreate()
    {
        base.OnCreate();

        query = GetEntityQuery(new EntityQueryDesc
        {
            All = new[] {
                ComponentType.ReadOnly<BoidType>(),
                ComponentType.ReadOnly<Translation>(),
                ComponentType.ReadOnly<NeighborsEntityBuffer>(),
                ComponentType.ReadWrite<Acceleration>()
            }
        });
    }

    [BurstCompile]
    public struct CohesionJob : IJobEntityBatch
    {
        [ReadOnly] public float alignmentWeight;
        [ReadOnly] public ComponentDataFromEntity<Translation> positionFromGrovalEntity;

        // アクセサのハンドルを受け取るためのメンバ。 var で受け取れないのでひたすら面倒
        [ReadOnly] public ComponentTypeHandle<Translation> translationHandle;
        [ReadOnly] public BufferTypeHandle<NeighborsEntityBuffer> neighborsBufferHandle;
        public ComponentTypeHandle<Acceleration> accelHandle;

        public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
        {
            var pos_array = batchInChunk.GetNativeArray(translationHandle);
            var neighbors_list = batchInChunk.GetBufferAccessor(neighborsBufferHandle);
            var acc_array = batchInChunk.GetNativeArray(accelHandle);

            // チャンク内のイテレーションは自分で書く。
            // 1段ネストが増える分手間だしバグ混入の危険も増える
            for(int i=0; i<batchInChunk.Count; i++)
            {
                var neighbors = neighbors_list[i];
                if (neighbors.Length == 0) continue;

                float3 pos_avg = float3.zero;
                float3 pos0 = pos_array[i].Value;
                float3 acc = acc_array[i].Value;

                for (int j = 0; j < neighbors.Length; j++)
                {
                    pos_avg += positionFromGrovalEntity[neighbors[j].entity].Value;
                }
                pos_avg /= neighbors.Length;

                acc += (pos_avg - pos0) * alignmentWeight;
                acc_array[i] = new Acceleration { Value = acc };
            }

            // ComponentType のアクセサが NativeArray<> を返すので Dispose する必要がある
            pos_array.Dispose();
            acc_array.Dispose();
        }
    }

    protected override void OnUpdate()
    {
        var param = Bootstrap.Param;
        var job = new CohesionJob
        {
            alignmentWeight = param.alignmentWeight,
            positionFromGrovalEntity = GetComponentDataFromEntity<Translation>(true),

            // 必要な ComponentType, BufferType の Handle をいちいち渡す必要がある
            translationHandle = GetComponentTypeHandle<Translation>(),
            neighborsBufferHandle = GetBufferTypeHandle<NeighborsEntityBuffer>(),
            accelHandle = GetComponentTypeHandle<Acceleration>()
        };
        // ここで渡す EntityQuery も自分で組み立てる必要がある
        Dependency = job.ScheduleParallel(query, 1, Dependency);
    }
}
```

上記は `Entities.ForEach` の例と同じ処理ですが、書かなければならないコードの量と複雑さが圧倒的です。
そのため、[公式](https://docs.unity3d.com/Packages/com.unity.entities@0.50/manual/ecs_ijobentitybatch.html)でも
「どうしても`IJobEntityBatch`を使う必要がない限り`Entities.ForEach`を使おう」
と言っています。


# 参考記事
Original implementation:
[Unity で Boids シミュレーションを作成して Entity Component System (ECS) を学んでみた](http://tips.hecomi.com/entry/2018/12/23/200817)

Qiita:
[ECSの簡単な実装と変更点について](https://qiita.com/tibaneko/items/c2230bbe98ca5415f18e)
[既存のUnityプロジェクトをUniversalRenderPipelineに移行する -サードインパクト修復-](https://qiita.com/ELIXIR/items/60e0fbd0a060ac3a698c)

Official:
[Entities](https://docs.unity3d.com/Packages/com.unity.entities@0.50/manual/index.html)
[Hybrid Renderer](https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@0.50/manual/index.html)
[EntityComponentSystemSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples)