[Unity] Entities 0.50 環境で Boids Simulation を再設計した話

[以前の記事](https://qiita.com/kawai125/items/69e7f4468f015de61556)で現行環境でとりあえずエラーは出ない状態にはなっていましたが、空間分割近傍探索ライブラリのテストアプリとしていじくりまわしているうちにだいぶ面影が変わってしまったので、ECSに関連する部分の設計と実装について簡単にまとめます。

本題の空間分割近傍探索ライブラリのテスト結果はこちらをどうぞ。
[[Unity] DOTSを用いた空間分割アルゴリズムの比較](https://qiita.com/kawai125/items/99bb45165da4db34fb92)

# 環境
- Unity 2020.3.33f
  - Hybrid Renderer 0.50-preview.24
  - Entities 0.50.0-preview.24
  - Collections 1.2.3
  - Burst 1.6.5

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

![default-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131238/940dbaca-70ad-0bfb-612f-49350b82a162.png)


# アプリの設計
アルゴリズム別に性能を評価するため、いちいち再コンパイルするのは地獄なのでランタイムでGUIからパラメータを与えてECS側が動作する必要がありました。またECSから外部のデータへのアクセスはstaticなメンバを通して行うのが楽だったので、MonoBehaviourとECSの間でパラメータとトリガの仲介を行う `***_Bootstrap` クラスを作り、これに対する入出力を通してMonoBehaviourによるGUI部分とECSが連携します。

![design-note-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131238/7004d879-f747-f162-8cf8-cfef70cc602d.png)

## ▼ Boids の生成、破棄
Bootstrapからトリガ送信としているのはBoidsの生成、破棄の指示で、MonoBehaviourとECSのシステム間では実行順序を制御する手段がないため、ECS側のSystemGroupの定義で一番最初に実行されるように指定した`ManagerSystemGroup`に属する生成、破棄用システム`SpawnAndRemoveSystem`でトリガを受け取り、Boidsの数を増減させます。また、Boidsの全体数を示す変数 `static int Bootstrap.BoidsCount` は `SpawnAndRemoveSystem` での生成または破棄が完了した後にBootstrapへ通知します。これは空間分割などを行うシステムでコンテナサイズとしてBoidsの全体数が必要なため、実行順序によっては  

Bootstrap(削除を指示) -> 空間分割(削除後の数で初期化) -> まだ削除されていないEntityのため容量オーバーでエラー

のように異常終了の原因になるため、このような構成としています。

<details><summary><b>ソースコード(折り畳み)</b></summary><div>

```C#: Bootstrap.cs(抜粋)
public class Bootstrap : MonoBehaviour
{
    public static Bootstrap Instance { get; private set; }

    [SerializeField] GameObject prefab_obj;
    private Entity prefab_entity;

    // UI interface
    [SerializeField]
    private UI_controller ui_input;

    private int n_boid;

    private EntityManager entity_manager;
    private Entity _triggerForBoidsSpawner;

    void Awake()
    {
        Instance = this;
    }

    public void Start()
    {
        //--- setup managers
        var world = World.DefaultGameObjectInjectionWorld;
        entity_manager = world.EntityManager;

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

        // add user defined component
        entity_manager.AddComponent<Prefab>(prefab_entity);
        entity_manager.AddComponent<BoidType>(prefab_entity);
        entity_manager.AddComponent<Scale>(prefab_entity);
        entity_manager.AddComponent<Velocity>(prefab_entity);
        entity_manager.AddComponent<Acceleration>(prefab_entity);
        entity_manager.AddComponent<NeighborsEntityBuffer>(prefab_entity);

        entity_manager.AddComponent<Tag_UpdateInteraction>(prefab_entity);
        entity_manager.AddComponent<Tag_ComputeNeighbors_Direct>(prefab_entity);

        n_boid = 0;

        // initialize trigger prefab
        _triggerForBoidsSpawner
            = entity_manager.CreateEntity(typeof(Prefab), typeof(BoidsSpawner));
    }

    public static int BoidsCount { get { return Instance.n_boid; } }

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

        int n_diff = n_tgt - n_boid;

        if (n_diff == 0) return;

        var trigger = entity_manager.Instantiate(_triggerForBoidsSpawner);
        var spawner = new BoidsSpawner
        {
            Prefab = prefab_entity,
            n = n_diff,
            scale = boidScale,
            initSpeed = BoidParams_Bootstrap.Param.initSpeed
        };
        entity_manager.SetComponentData(trigger, spawner);
    }
    public static void UpdateBoidNumComplete(BoidsSpawner spawner)
    {
        Instance.n_boid += spawner.n;
    }

    void Update()
    {
        UpdateBoidNum(ui_input.boidCount);
    }
}
```

```C#: SpawnAndRemoveSystem.cs
[UpdateInGroup(typeof(ManagerSystemGroup))]
public partial class SpawnerAndRemoveSystem : SystemBase
{
    private EntityQuery _boids_query;
    private Random _random;

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

        _boids_query = EntityManager.CreateEntityQuery(new EntityQueryDesc
        {
            All = new[]
                {
                    ComponentType.ReadOnly<BoidType>()
                },
            None = new[]
                {
                    // わざわざ指定しなくてもいいかも
                    ComponentType.ReadOnly<Prefab>()
                }
        });

        _random = new Random(853);
    }

    protected override void OnUpdate()
    {
        Dependency.Complete();

        Entities.
            WithStructuralChanges().  // メインスレッド実行を強制、チャンク構造の変更(EntityManagerの呼び出し)が可能
            WithoutBurst().           // 当然 Burst も使用不可
            ForEach(
            (Entity trigger, in BoidsSpawner spawner) =>
            {
                Dependency.Complete();

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

                    var spawnedEntities = new NativeArray<Entity>(spawner.n, Allocator.Temp);
                    EntityManager.Instantiate(spawner.Prefab, spawnedEntities);

                    float spawn_area = Bootstrap.WallScale * 0.4f;

                    for (int i = 0; i < spawner.n; i++)
                    {
                        var entity = spawnedEntities[i];

                        EntityManager.SetComponentData(entity, new Translation { Value = _random.NextFloat3(-spawn_area, spawn_area) });
                        EntityManager.SetComponentData(entity, new Rotation { Value = quaternion.identity });
                        EntityManager.SetComponentData(entity, new Scale { Value = spawner.scale });
                        EntityManager.SetComponentData(entity, new Velocity { Value = _random.NextFloat3Direction() * spawner.initSpeed });
                        EntityManager.SetComponentData(entity, new Acceleration { Value = float3.zero });
                    }

                    spawnedEntities.Dispose();
                }
                else if(spawner.n < 0)
                {
                    int n_delete = -spawner.n;
                    UnityEngine.Debug.Log($"update boids num: remove {n_delete} boids.");

                    var entities = _boids_query.ToEntityArray(Allocator.Temp);
                    EntityManager.DestroyEntity(new NativeSlice<Entity>(entities, 0, math.abs(n_delete)));
                    entities.Dispose();
                }

                //--- 処理結果を通知
                Bootstrap.UpdateBoidNumComplete(spawner);

                //--- 完了したトリガを消去(忘れると無限に繰り返し実行される)
                EntityManager.DestroyEntity(trigger);

            }).Run();
    }
}
```

```C#: ComponentData.cs(抜粋)
public struct BoidsSpawner : IComponentData
{
    public Entity Prefab;
    public int n;
    public float scale;
    public float initSpeed;
}
```

</div></details>

Prefab Entity には、[以前](https://qiita.com/kawai125/items/69e7f4468f015de61556#prefab-entity-%E3%81%AE%E6%BA%96%E5%82%99)は自分で定義した判別用のタグコンポーネントを付加していましたが、現在は公式が用意している [Entities.Prefab](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.Prefab.html) コンポーネントをタグとして使用しています。

このコンポーネントがついているEntityはEntityQueryの検索対象から除外され、かつ`EntityManager.Instanciate(Entity)`に引数として与えると、[`Entities.Prefab`が自動的に取り除かれたものが生成されます。](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.EntityManager.Instantiate.html#Unity_Entities_EntityManager_Instantiate_Unity_Entities_Entity_)

自作タグの場合はタグコンポーネントのつけ外しも自分で管理する必要があり、またそれに伴うアーキタイプの変更はチャンク間のエンティティの移動を引き起こす可能性があり、パフォーマンスへの悪影響も懸念されます。ここは公式のツールを活用しましょう。
生成自体もひとつひとつ `Instanciate()` するのではなく、必要個数分の `NativeArray<Entity>` を作ってまとめて生成するのが高速です。

ECSとのトリガの送受信にもEntityを利用します。
対象と合致するEntityQueryがあれば全てのEntityを取得できるので、今回は Mono -> ECS の向きにしか送信していませんが、ECSのシステムでトリガEntityを生成 -> Mono 側で当該 `query.ToEntityArray().Length` が0でなければ何か処理、という形で送受信に利用できます。お互いのメンバを直接参照するわけではないのでECSのシステムの実行順序と分離して管理できるところもメリットです。

システムの実行順のデザインについて、今回は乱数を使用するためEntityCommandBufferを利用した並列化はしませんでしたが、デフォルトで用意されているECBのアップデートシステムで毎ループ実行されるものは`SimulationSystemGroup` の Before / End のどちらかです。Entityの生成、破棄が中途半端なタイミングで実行されると事故の元なので、**まず初めにEntityの生成、破棄のタイミングを決めて**、そこからほかの処理の順番を並べていく形で `ComponentSystemGroup`の実行順を指定するのがよいでしょう。

## ▼ Tagコンポーネントによるシステムの動作切り替え
本アプリではNeighborListの探索について、複数の実装を切り替えてそれらの性能の評価、およびデバッグを行うことを目的としています。この探索アルゴリズムの切り替えについて、一つのアルゴリズムにつき一つのシステムを実装し、それらの間をタグコンポーネントの付け替えによって切り替える形で実装しました。システムの実行順は次のようになっています。

![design-note-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131238/41c27a43-899f-faba-bee1-49e182d3ff99.png)

Entityの生成、破棄と同様、変なタイミング(例えばNeighborDetectionSystemGroupのあと)でTagが変更されると後続のシステムが正しく動かなくなってしまうので、GUIからの指示はBootstrapからトリガとして送信され、`ManagerSystemGroup`の`ReplaceTagComponentSystem`が処理を実行します。

<details><summary><b>ソースコード(折り畳み)</b></summary><div>

```C#:TagComponent.cs(抜粋)
public struct Tag_UpdateInteraction : IComponentData { }

public struct Tag_ComputeNeighbors_Direct : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Entity_NeighborList : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Cell_NeighborList : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Cell_Cell : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Combined_CNL : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Combined_CC : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_MergedCell_NL : IComponentData { }

public enum ComputeNeighborsPlan
{
    Direct,

    CellIndex_Entity_NeighborList,
    CellIndex_Cell_NeighborList,
    CellIndex_Cell_Cell,

    CellIndex_Combined_CNL,
    CellIndex_Combined_CC,

    CellIndex_MergedCell_NL,
}

public struct ComputePlanSwicher : IComponentData
{
    public Entity Prefab;
    public EntityQuery Query;
    public ComputeNeighborsPlan RemoveTarget, AddTarget;
}
```

```C#:Bootstrap.cs(抜粋)
public class Bootstrap : MonoBehaviour
{
    private EntityManager entity_manager;
    private Entity _triggerForComputePlanSwitcher;

    public void Start()
    {
        //--- setup managers
        var world = World.DefaultGameObjectInjectionWorld;
        entity_manager = world.EntityManager;

        // initialize trigger prefab
        _triggerForComputePlanSwitcher = entity_manager.CreateEntity(typeof(Prefab), typeof(ComputePlanSwicher));
    }

    public void SwitchComputeNeighborsPlan(ComputeNeighborsPlan old_plan, ComputeNeighborsPlan new_plan)
    {
        var trigger = entity_manager.Instantiate(_triggerForComputePlanSwitcher);
        var swapper = new ComputePlanSwicher
        {
            Prefab = prefab_entity,
            Query = boids_query,
            RemoveTarget = old_plan,
            AddTarget = new_plan,
        };
        entity_manager.SetComponentData(trigger, swapper);
    }
}
```

```C#:ReplaceTagComponentSystem.cs
[UpdateInGroup(typeof(ManagerSystemGroup))]
public partial class ReplaceTagComponentSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities.
            WithStructuralChanges().
            ForEach(
            (Entity trigger, in ComputePlanSwicher swicher) =>
            {
                Dependency.Complete();

                SwitchComputeNeighborsPlan(EntityManager, swicher);

                EntityManager.DestroyEntity(trigger);
            }).Run();
    }

    public static void SwitchComputeNeighborsPlan(EntityManager manager, ComputePlanSwicher swicher)
    {
        var prefab_entity = swicher.Prefab;
        var boids_query = swicher.Query;
        var old_plan = swicher.RemoveTarget;
        var new_plan = swicher.AddTarget;

        switch (old_plan)
        {
            case ComputeNeighborsPlan.Direct:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_Direct>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Entity_NeighborList:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Entity_NeighborList>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Cell_NeighborList:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Cell_NeighborList>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Cell_Cell:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Cell_Cell>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Combined_CNL:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Combined_CNL>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Combined_CC:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Combined_CC>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_MergedCell_NL:
                RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_MergedCell_NL>(manager, boids_query, prefab_entity);
                break;
            default: throw new ArgumentOutOfRangeException(nameof(old_plan));
        }

        switch (old_plan)
        {
            case ComputeNeighborsPlan.CellIndex_Combined_CNL:
            case ComputeNeighborsPlan.CellIndex_Combined_CC:
                AddComponentToBoids<Tag_UpdateInteraction>(manager, boids_query, prefab_entity);
                AddBufferToBoids<NeighborsEntityBuffer>(manager, boids_query, prefab_entity);
                break;
            default:
                break;
        }

        switch (new_plan)
        {
            case ComputeNeighborsPlan.Direct:
                AddComponentToBoids<Tag_ComputeNeighbors_Direct>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Entity_NeighborList:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Entity_NeighborList>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Cell_NeighborList:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Cell_NeighborList>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Cell_Cell:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Cell_Cell>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Combined_CNL:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Combined_CNL>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_Combined_CC:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Combined_CC>(manager, boids_query, prefab_entity);
                break;
            case ComputeNeighborsPlan.CellIndex_MergedCell_NL:
                AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_MergedCell_NL>(manager, boids_query, prefab_entity);
                break;
            default: throw new ArgumentOutOfRangeException(nameof(new_plan));
        }

        switch (new_plan)
        {
            case ComputeNeighborsPlan.CellIndex_Combined_CNL:
            case ComputeNeighborsPlan.CellIndex_Combined_CC:
                RemoveComponentFromBoids<Tag_UpdateInteraction>(manager, boids_query, prefab_entity);
                RemoveBufferFromBoids<NeighborsEntityBuffer>(manager, boids_query, prefab_entity);
                break;
            default:
                break;
        }
    }
    private static void RemoveComponentFromBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
        where T : IComponentData
    {
        manager.RemoveComponent<T>(prefab);
        manager.RemoveComponent<T>(boids_query);
    }
    private static void AddComponentToBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
        where T : IComponentData
    {
        manager.AddComponent<T>(prefab);
        manager.AddComponent<T>(boids_query);
    }
    private static void RemoveBufferFromBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
        where T : IBufferElementData
    {
        manager.RemoveComponent<T>(prefab);
        manager.RemoveComponent<T>(boids_query);
    }
    private static void AddBufferToBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
        where T : IBufferElementData
    {
        manager.AddComponent<T>(prefab);
        manager.AddComponent<T>(boids_query);
    }
}
```

</div></details>

`NeighborDetectionSystemGroup` 内で具体的にどのシステムが実行されるかは `Tag_ComputeNeighbors_***` で指定しています。また、ネイバーリストの構築後に相互作用を計算するか否かは `Tag_UpdateInteraction` で指定しています。

Tagのつけ外しのほか、Combined な Plan ではネイバーリストの構築ではなくその場で相互作用を計算するためにネイバーリストの保存先である `DynamicBuffer` も不要なので外しています。

## ▼ Systemが要求するEntityQueryの定義方法
ECS向けとして設計された `SystemBase.Entities.ForEach()` や `IJobEntityBatch` は `Schedule()` 時に`EntityQuery` を渡すことでSystemのEntityQueryも自動的に設定されるようになっていましたが、これらは Entity でループを回すインターフェイスになっており、それ以外のイテレーションを行いたい場合-例えばセルごとに情報を持つテーブルを使ってセルでイテレーションをしたい場合など-には使えません。ループの長さをプログラマが指定する形、ということで今回は馴染み深い `IJobParallelFor` を使いますが、このときSystemに明示的にEntityQueryを伝える必要があります。

```C#:IJobParallelForを使うSystemの例
public partial class ParallelForSystem : SystemBase
{
    protected override void OnCreate()
    {
        base.OnCreate();

        RequireForUpdate(
            GetEntityQuery(ComponentType.ReadOnly<SomethingTag>(),
                           ComponentType.ReadOnly<SomethingType>(),
                           ComponentType.ReadWrite<SomethingResult>()));
    }
    private ParallelJob : IJobParallelFor
    {
        [ReadOnly]
        public ComponentDataFromEntity<SomethingType> dataFromEntity;

        [NativeDisableContainerSafetyRestriction]
        public ComponentDataFromEntity<SomethingResult> resultFromEntity;

        public void Execute(int i){ /* compute */ }
    };
    protected override OnUpdate()
    {
        int work_length = /* number of task */;
        int batch_size  = /* batch size of parallel job */;
        var job = new ParallelJob
        {
            // RequireForUpdate() に登録したのでシステムが実行され、コンポーネントをとってこれる
            dataFromEntity = GetComponentDataFromEntity<SomethingType>(true);  // read only
            resultFromEntity = GetComponentDataFromEntity<SomethingResult>();
        };
        Dependency = job.Schedule(work_length, batch_size, Dependency);
    }
}
```

Systemの生成時に `SystemBase.RequireForUpdate()` に `EntityQuery` を渡すことで必要なEntityQueryを定義できます。ここで、`SystemBase.RequireForUpdate()` と `SystemBase.Entities.ForEach()` を両方定義すると両方共が内部配列に保存され、 `EntityQuery[] SystemBase.EntityQueries` で参照可能です。

## ▼ GlobalなNativeContainerの置き場所
空間分割マップとして `NativeMultiHashMap` を元に `HashCellIndex` を作りましたが、これを複数のシステムから参照可能にするため `HashCellIndex` 自体はMonoBehaviour側の `CellIndex_Bootstrap` に置き、System側からはstaticメンバを参照する形にしました。この際、システムの実行順序に絡んで2つの留意点があります。

### 〇 SystemによるMonoBehaviourのNativeContainerへの参照
ECSのSystemの処理は　`SystemBase.Dependency` 経由で次々に放り込んでいるようで、staticな場所にNativeContainerを置いて参照する前に `SystemBase.Dependency.Complete()` で先に走っている System を完了させておかないとNativeContainerの安全システムから「今Jobで使用中」と怒られます。

```C#:NeighborDetectionSystem.cs(抜粋)
[UpdateInGroup(typeof(BuildCellIndexSystemGroup))]
public partial class BuildCellIndexSystetm : SystemBase
{
    protected override void OnUpdate()
    {
        // UnityEngine にアクセスする前に Complete() する必要がある。
        Dependency.Complete();

        // 参照を取得
        var cellIndex = CellIndex_Bootstrap.HashCellIndex;
        CellIndex_Bootstrap.InitDomain(Bootstrap.BoidsCount);

        // Jobで利用
        var cellIndexWriter = cellIndex.AsParallelWriter();
        Dependency = Entities.
            WithName("UpdateCellIndexJob").
            WithAll<BoidType>().
            WithNone<Tag_ComputeNeighbors_Direct>().
            WithBurst().
            ForEach(
            (Entity entity, in Translation pos) =>
            {
                cellIndexWriter.TryAdd(pos.Value, entity);
            }).ScheduleParallel(Dependency);

        // 終了処理用 (次節を参照)
        CellIndex_Bootstrap.SetJobHandle(this.GetType().Name, Dependency);

        // 次のシステム (NeighborDetectionSystemGroup) も使うのでここで Complete() してしまう
        // (各システムで初めにComplete()してもよい)
        Dependency.Complete();
    }
}
```

### 〇 Systemに利用されるNattiveContainerの破棄
前節に絡んで、アプリの終了時にMonoBehaviourの破棄とSystemの完了、破棄の順番は不定なので、MonoBehaviourにECS Systemから参照するNativeContainerを置く場合にはコンテナの破棄前に関連するJobをすべて完了させておく必要があります。

なので、コンテナを利用する各 System から `JobHandle` を受け取り、 `Dispose()` 前にすべて `Complete()` させることで終了処理の順番を制御します。

各 System は class として定義されるので、 System ごとの区別は `string class.GetType.Name` を `JobHandle` の識別子として `Dictionary` に登録します。

```C#:CellIndex_Bootstrap.cs(抜粋)
public class CellIndex_Bootstrap : MonoBehaviour
{
    private static CellIndex_Bootstrap Instance;
    private void Awake()
    {
        Instance = this;
    }

    public static HashCellIndex<Entity> HashCellIndex { get { return Instance._cellIndex; } }
    private HashCellIndex<Entity> _cellIndex;

    private Dictionary<string, JobHandle> _handles;
    private bool _allocated;

    void Start()
    {
        _cellIndex = new HashCellIndex<Entity>(Allocator.Persistent);
        _handles = new Dictionary<string, JobHandle>();
        _allocated = true;
    }

    private void OnDestroy()
    {
        Dispose();
    }
    ~CellIndex_Bootstrap()
    {
        Dispose();
    }
    public void Dispose()
    {
        if (_allocated)
        {
            // コンテナの Dispose() 前に関連する Job をすべて完了させる 
            foreach(var handle in _handles.Values) handle.Complete();
            _cellIndex.Dispose();
            _allocated = false;
        }
    }
    // 終了処理用の JobHandle を受け取る
    public static void SetJobHandle(string job_identifier, JobHandle handle) => Instance.SetJobHandleImpl(job_identifier, handle);
    private void SetJobHandleImpl(string job_identifier, JobHandle handle)
    {
        if (_handles.ContainsKey(job_identifier))
        {
            _handles[job_identifier] = handle;
        }
        else
        {
            _handles.Add(job_identifier, handle);
        }
    }
}
```

## ▼ Systemでprivateに利用するバッファ
Systemの外部とコンテナを共有せず、 private に使うだけなら `SystemBase.OnCreate()` と `SystemBase.OnDestroy()` が使えるので普通に Allocate して Dispose() します。

```C#
public partial class ComputeSystem : SystemBase
{
    private NativeList<Data> localBuffer;

    protected override void OnCreate()
    {
        base.OnCreate();
        localBuffer = new NativeList<Data>(Allocator.Persistent);
    }
    protected override void OnDestroy()
    {
        base.OnDestroy();
        localBuffer.Dispose();
    }
    protected override void OnUpdate()
    {
        base.OnUpdate();

        /* use localBuffer in OnUpdate() */
    }
}
```

# その他

## ▽ SystemBase でできること
[公式のSystemBaseのAPI](https://docs.unity3d.com/Packages/com.unity.entities@0.50/api/Unity.Entities.SystemBase.html)を見ると結構いろいろな継承メンバを持っているので System で何ができるのかを考えるときには各継承メンバの仕様も併せて確認するといいと思います。いろいろできるのに `Properties` や `Methods` には書かれていないので当初ほしいAPI (`RequireForUpdate`) が見つからず、途方にくれていました……

## ▽ DOTS向けのUnityEditor機能
Entities 0.50 で Editor 拡張も大幅に改良され、Inspectorがだいぶ見やすくなりました。

- Window->DOTS->System
![Window-DOTS-System-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131238/0106d3d6-34e8-4fc3-b0c3-e15d4ef3432c.png)
`NeighborValidateSystem` (デバッグ用に作ったもの)は `SystemBase.RequireForUpdate()` と `SystemBase.Entities.ForEach()` の両方が定義されているため、EntityQueryが２つ表示されています。このとき生成されている Boids 100個はどちらにも該当するので、 Query1 と Query2 それぞれの EntityCount=100 が合算され Systems Window では200個のEntityが処理されていると解釈されています。


- Window->DOTS->Hierarchy
![Window-DOTS-Hierarchy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131238/d2ae0a5b-d447-2d84-573b-794e433e6cb8.png)
GameObjectのようにDOTS Hierarchyで値を確認できるようになりました。 `Entities.Prefab` タグコンポーネントがついているかどうかもGameObjectのPrefab同様色分けされます。
また、値のフィールドを持たない `ComponentType` は自動的にタグと認識され、 `Tags` タブに表示されます。

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

Previous version:
[[Unity] 現在(Entities 0.50)のECS環境でシンプルな Boids Simulation を書く](https://qiita.com/kawai125/items/69e7f4468f015de61556)

Qiita:
[UnityのEntitiesプロジェクトをビルドする方法](https://qiita.com/_EorF/items/8f3d7625d5246ca31311)
[【Unity】DOTS(ECS)を導入したゲームを作る際に設計してみた話](https://qiita.com/mao_/items/a5548f31de479c6831e4)

Official
[Entities](https://docs.unity3d.com/Packages/com.unity.entities@0.50/manual/index.html)