Entity Component System を作って学ぶ

この記事は[Unity #2 Advent Calender 2018](https://qiita.com/advent-calendar/2018/unity2) 21日目の記事です。

先にECSを紹介された方がいたが、多忙につき取り上げられず。申し訳ないです。

#  そもそもECSとは？
Entity Component Systemの略です。

引用：[Unity-Technologies/EntityComponentSystemSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples)
>Offers a better approach to game design that allows you to concentrate on the actual problems you are solving: the data and behavior that make up your game. It leverages the C# Job System and Burst Compiler enabling you to take full advantage of today's multicore processors. By moving from object-oriented to data-oriented design it will be easier for you to reuse the code and easier for others to understand and work on it

>The Entity Component System ships as an experimental package in 2018.1 and later, and we’ll continue to develop and release new versions of the package in the 2018.x cycle. It is important to stress that the Entity Component System is not production ready

**意訳**

- C#JobSystem と　BurstCompilerを使って、今日のメモリ並列処理技術をフル活用できますよ。
- オブジェクト志向から移行することで、コードが使い回し安くなります。どう動いているかが他人に理解しやすくなります。
- ECSはUnity2018.1以降から試験的にリリースされています。まだ製品版じゃありませんよ。

→ GameObjectの上位互換になりうる新しい技術ということかな？
[BurstCompiler]()と組み合わせれば飛躍的にパフォーマンスが向上するようです。

他記事にも沢山説明があるので、より詳しく知りたい方は以下を読んでみるといいかも。
[【Unity】Unity 2018のEntity Component System（通称ECS）について（１）](http://tsubakit1.hateblo.jp/entry/2018/03/25/180203)

>まずECSとは何か？ECSは 何か凄い機能 というよりは、コンポーネント志向に変わる新しいアーキテクチャパターンです。
要するに、(目標としては)GameObject / Componentを差し替えるモノです。
 

>ECSはGameObject / MonoBehaviourと較べて、膨大なオブジェクトを処理しやすい設計になっています。ECSはUnityのコンポーネント志向と用語的には似ており、ECSはUnity的な視点から言えば「コンパクトな GameObject & Monobehaviourのようなモノ」とも言えなくはないです。

今後エンジニアとして生きていくなら、絶対勉強しておかないとダメ！といった記事も目に入り、今回はこのネタを選びました。
と言っても、他の記事を読んでもふんわりとしか分からず、初級者向けに、何かに例えながら紹介させてもらいます。

# ECS用語

1. Entity
 - 「ComponentSystemが動作する為に要求するComponentData各種(Group)」を持つデータの単位
 - インスタンスに該当。速いGameObjectのようなもの。
2. ComponentData
 - 処理(ComponentSystem)で使われるデータ
 - MonoBehaviorで言うところの**データ部分**
3. ComponentSystem
 - Groupのデータが揃っている際に動作する処理の内容
 - MonoBehavior で言うところの**振る舞い部分**
4. Group
 - 処理(ComponentSystem)を動作させるために必要となるデータ(ComponentData)各種の定義
 - 必要なComponentDataはこれとこれです的な事を決めている。要求リスト



# 導入

<img width="333" alt="スクリーンショット 2018-12-02 16.07.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/24e678a5-aca5-8325-4707-58e33a7384e8.png">


- **PlayerSettings**から、**Scripting Runtime Version**を**.NET 4.x**にします。
- **PlayerSettings**から、**unsafeコード**を許可する
- **Package Manager**から**Entities**をインストールします。


# 公式ECSサンプルを見てみた

[Unity-Technologies/EntityComponentSystemSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples)

を**git clone**。動作環境は**Unity2018.2.3f1**

　サンプルの中には**PureECS**（現行のGameObjectやComponentを使用しないECSのみでの実装）、**HybridECS**（現行のComponent等とECSを組み合わせたハイブリットと呼ばれる実装）と色々なものがありました。

## 試しにBoidExampleを再生してみた。

<img width="324" alt="スクリーンショット 2018-11-17 17.13.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/b87b21a9-21de-10eb-55fd-e23aa09dee9b.png"><img width="325" alt="スクリーンショット 2018-11-17 17.13.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/c8e7730e-22f4-fd38-f547-445ad2d7990f.png">

魚の大群が現れました。すごい...(語彙力)

<img width="284" alt="スクリーンショット 2018-11-17 17.57.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/b20aaa77-8259-8a92-eee7-b0ee62996c31.png">

ヒエラルキーは上記のように組まれてます。この中で、**BoidFishSpawner**と**生成する魚(prefab)**にECSが実装されていました。
**BoidFishSpawner**には、魚を生成するためのComponentDataが、**生成する魚(prefab)**にはその魚を制御するComponentDataがアタッチされています。

## ComponentData の中身を見てみる
魚の挙動は複雑だったので、魚を生成する方のComponentDataだけ紹介します。


Boidコンポーネントデータには、**魚群の生成範囲**、**魚の数**、**魚プレハブ**を持たせています。
これらをComponentSystemに渡し、生成するという事ですね。

``` Boid.cs
using System;
using Unity.Entities;
using UnityEngine;

namespace Samples.Common
{
    [Serializable]
    public struct SpawnRandomInSphere : ISharedComponentData
    {
        public GameObject prefab;
        public float radius; //魚群の生成範囲
        public int count;     //魚の数
    }

    public class SpawnRandomInSphereComponent : SharedComponentDataWrapper<SpawnRandomInSphere> { }
}
```

**Positionコンポーネントデータ**は、 float3 Value を持つのみですね。
座標など、生成位置以外にも使える**汎用的なComponentDataは、使いまわして使用**してます。

例えばここでは、生成位置以外にも、移動する座標（魚の挙動）にもPositionコンポーネントデータが使われていました。

```Position.cs
using System;
using Unity.Entities;
using Unity.Mathematics;

namespace Unity.Transforms
{
    /// <summary>
    /// If Attached, in local space (relative to parent)
    /// If not Attached, in world space.
    /// </summary>
    [Serializable]
    public struct Position : IComponentData
    {
        public float3 Value; //生成位置情報
    }

    [UnityEngine.DisallowMultipleComponent]
    public class PositionComponent : ComponentDataWrapper<Position> { }
}
```


## ComponentSystem の中身を見てみる
上述の**ComponentData**(SpawnRandomInSphereComponent)は**ComponentSystem**(SpawnRandomInSphereSystem)にて使われています。

どうやらこの中で、ComponentSystemの他、**Groupの定義**もしているようです。

```SpawnRandomInSphereSystem.cs
using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace Samples.Common
{
    public class SpawnRandomInSphereSystem : ComponentSystem
    {
        struct SpawnRandomInSphereInstance
        {
            public int spawnerIndex;
            public Entity sourceEntity;
            public float3 position;
#pragma warning disable 649
            public float radius;
#pragma warning restore 649
        }

        ComponentGroup m_MainGroup;

        // ここで要求するComponentDataを決めています。
        protected override void OnCreateManager()
        {
            m_MainGroup = GetComponentGroup(typeof(SpawnRandomInSphere), typeof(Position));
        }

        protected override void OnUpdate()
        {
            var uniqueTypes = new List<SpawnRandomInSphere>(10);

            EntityManager.GetAllUniqueSharedComponentData(uniqueTypes);

            int spawnInstanceCount = 0;
            for (int sharedIndex = 0; sharedIndex != uniqueTypes.Count; sharedIndex++)
            {
                var spawner = uniqueTypes[sharedIndex];
                m_MainGroup.SetFilter(spawner);
                var entities = m_MainGroup.GetEntityArray();
                spawnInstanceCount += entities.Length;
            }

            if (spawnInstanceCount == 0)
                return;

            var spawnInstances = new NativeArray<SpawnRandomInSphereInstance>(spawnInstanceCount, Allocator.Temp);
            {
                int spawnIndex = 0;
                for (int sharedIndex = 0; sharedIndex != uniqueTypes.Count; sharedIndex++)
                {
                    var spawner = uniqueTypes[sharedIndex];
                    m_MainGroup.SetFilter(spawner);
                    var entities = m_MainGroup.GetEntityArray();
                    var positions = m_MainGroup.GetComponentDataArray<Position>();

                    for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
                    {
                        var spawnInstance = new SpawnRandomInSphereInstance();

                        spawnInstance.sourceEntity = entities[entityIndex];
                        spawnInstance.spawnerIndex = sharedIndex;
                        spawnInstance.position = positions[entityIndex].Value;

                        spawnInstances[spawnIndex] = spawnInstance;
                        spawnIndex++;
                    }
                }
            }

            for (int spawnIndex = 0; spawnIndex < spawnInstances.Length; spawnIndex++)
            {
                int spawnerIndex = spawnInstances[spawnIndex].spawnerIndex;
                var spawner = uniqueTypes[spawnerIndex];
                int count = spawner.count;
                var entities = new NativeArray<Entity>(count,Allocator.Temp);
                var prefab = spawner.prefab;
                float radius = spawner.radius;
                var spawnPositions = new NativeArray<float3>(count, Allocator.Temp);
                float3 center = spawnInstances[spawnIndex].position;
                var sourceEntity = spawnInstances[spawnIndex].sourceEntity;

                // 魚の生成位置
                GeneratePoints.RandomPointsInSphere(center,radius,ref spawnPositions);

                // 魚の生成
                EntityManager.Instantiate(prefab, entities);

                for (int i = 0; i < count; i++)
                {
                    var position = new Position
                    {
                        Value = spawnPositions[i]
                    };
                    EntityManager.SetComponentData(entities[i],position);
                }

                EntityManager.RemoveComponent<SpawnRandomInSphere>(sourceEntity);

                spawnPositions.Dispose(); // NativeArrayは必ず破棄処理が必要
                entities.Dispose();
            }
            spawnInstances.Dispose();
        }
    }
}
```

# 自作サンプル作ってみた

公式サンプルを眺めてみたが、なんとなくしか分からない。やはり習うより慣れろだろうということで、簡単なサンプル作ってみようと思います。

決められた長方形の範囲内にCubeを生成するものを作ります。

動作環境につきましては、公式と同じです。(Unity2018.2.3f1)

## まずComponentDataを用意する

まずは**生成するためには、生成する物（Prefab）、生成数、生成範囲、生成場所が必要**なので、以下の２つのComponentDataをSpawnerにアタッチします。

<img width="557" alt="スクリーンショット 2018-12-02 16.51.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/54323374-387b-58b6-448b-3efdc30b2310.png">


```CubeComponent.cs
using UnityEngine;
using Unity.Entities;
using System;

namespace Samples.Common
{
    [Serializable]
    public struct Cube : ISharedComponentData
    {
        public GameObject prefab; // プレハブを持たす
        public float halfWidth; // 生成する範囲の半径
        public bool spawnLocal;
        public int count; // 生成数
    }

    public class CubeComponent : SharedComponentDataWrapper<Cube> { }
}
```

Positionコンポーネントデータについては、先ほど使った魚の生成位置と同じのを使い回します。

```Position.cs
using System;
using Unity.Entities;
using Unity.Mathematics;

namespace Unity.Transforms
{
	[Serializable]
	public struct Position : IComponentData
	{
		public float3 Value;
	}
}
```


## ComponentSystemを作成する。
データを元にどのような挙動をす流のか決める**CompoonentSystem**を作成します。

```CubeSystem.cs
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Collections;

namespace Samples.Common
{
    public class CubeSystem : ComponentSystem
    {
        // 要求リスト一覧
        struct Group
        {
            [ReadOnly]
            public SharedComponentDataArray<Cube> Spawner;
            public ComponentDataArray<Position> Position;
            public EntityArray Entity;
            public readonly int Length;
        }

        [Inject]
        Group m_Group;

        protected override void OnUpdate()
        {
            while (m_Group.Length != 0)
            {
                var spawner = m_Group.Spawner[0];
                var sourceEntity = m_Group.Entity[0];
                var center = m_Group.Position[0].Value;

                var entities = new NativeArray<Entity>(spawner.count, Allocator.Temp);
                EntityManager.Instantiate(spawner.prefab, entities);

                var positions = new NativeArray<float3>(spawner.count, Allocator.Temp);

                if (spawner.spawnLocal)
                {
                    GeneratePoints.RandomPointsInSquare(new float3(), spawner.halfWidth, ref positions);
                    for (int i = 0; i < spawner.count; i++)
                    {
                        var position = new Position
                        {
                            Value = positions[i]
                        };
                        EntityManager.SetComponentData(entities[i], position);

                        // Spawn Attach
                        var attach = EntityManager.CreateEntity();
                        EntityManager.AddComponentData(attach, new Attach
                        {
                            Parent = sourceEntity,
                            Child = entities[i]
                        });
                    }
                }
                else
                {
                    GeneratePoints.RandomPointsInSquare(center, spawner.halfWidth, ref positions);
                    for (int i = 0; i < spawner.count; i++)
                    {
                        var position = new Position
                        {
                            Value = positions[i]
                        };
                        EntityManager.SetComponentData(entities[i], position);
                    }
                }

                entities.Dispose();
                positions.Dispose();

                EntityManager.RemoveComponent<Cube>(sourceEntity);

                // Instantiate & AddComponent & RemoveComponent calls invalidate the injected groups,
                // so before we get to the next spawner we have to reinject them  
                UpdateInjectedComponentGroups();
            }
        }
    }
}
```

100x100の正方形内に、50,000個のCubeがビッシリ生成されました！
MonoBehaviorだったらこの時点で激重なのでは...？
<img width="922" alt="スクリーンショット 2018-11-25 15.01.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/f40fe66e-441e-d2f7-ef9d-75b60b6bfd3a.png">

## さらに振る舞いを追加してみる

これでは公式サンプルのままで寂しいので、もう少し手を加えます。
生成した Cube Prefab に**波のような挙動を追加してみます。**

Cubeの挙動なので、**Prefabに持たせるコンポーネントデータが必要**です。
今回は従来からある**PositionComponent**(コンポーネントデータ)を使い回し、アタッチします。
また、**UpDownコンポーネントデータを用意**します。

```UpDownComponent.cs
using Unity.Entities;

namespace Samples.Common
{
    public struct UpDown : ISharedComponentData{ }

    public class UpDownComponent : SharedComponentDataWrapper<UpDown>{}

}
```

<img width="293" alt="スクリーンショット 2018-12-02 15.52.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/256717/1466d036-afeb-66e5-43dd-ced771f4e051.png">


さて、**ComponentData**を用意し、**Prefab**へアタッチしたら、今度は**ComponentSystem**を作成します。

```UpDownSystem.cs
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

namespace Samples.Common
{
    public class UpDownSystem : JobComponentSystem
    {
        /* IJobParallelForインターフェースを使うと、
         * 複数のスレッドに分散させて配列要素を並列で実行することができます。*/

        [BurstCompile]
        struct MoveUpdown : IJobParallelFor 
        {
            // CubeにPositionComponentがアタッチされているか確認しましょう
            public ComponentDataArray<Position> positions; 
            public float time;

            /// <summary>
            /// ジョブの処理内容
            /// </summary>
            public void Execute(int i)
            {
                positions[i] = new Position
                {
                    Value = new float3(positions[i].Value.x,
                                       Mathf.Sin(time+(positions[i].Value.x + positions[i].Value.z)),
                                      positions[i].Value.z)
                };
            }
        }

        ComponentGroup m_MoveUpDownGroup;

        /* 要求リストを作成。
         * ここではUpDownコンポーネントデータと
         * Positionコンポーネントデータを要求するという事 */
        protected override void OnCreateManager()
        {
            m_MoveUpDownGroup = GetComponentGroup(
                ComponentType.ReadOnly(typeof(UpDown)),
                typeof(Position));
        }

        // ジョブを実行する。
        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            var moveUpDownJob = new MoveUpdown
            {
                positions = m_MoveUpDownGroup.GetComponentDataArray<Position>(),
                time = Time.realtimeSinceStartup
            };

            // Scheduleメソッドの第2引数にはバッチ数を指定します。
            var moveUpDownJobHandle = moveUpDownJob.Schedule(m_MoveUpDownGroup.CalculateLength(),
                                                             64,
                                                             inputDeps);
            return moveUpDownJobHandle;
        }
    }
}
```

こんな感じで再生できました！

![gif_animation_001.gif](https://qiita-image-store.s3.amazonaws.com/0/256717/45affdae-5dda-42cc-0d09-ad2597c77628.gif)

...従来のGameObjectとMonoBehaviorなら激重間違いなし

# まとめ・感想

**ECSは下記のように使えば動くと分かった**
<ul><li>動かしたいものに**ComponentDataをアタッチ**する<br>→ComponentDataの記述方法は、例えばScriptableObjectにキャラクターのパラメーターを持たすイメージに近いかも！</li><li> ComponentDataは同じものであれば**使い回す**</li><li> **ComponentSystem**にて必要なComponentDataを決め、ジョブを実行する。</li></ul>

ブログ等の説明を読んで分からなかったのですが、実際手を動かしてみると案外シンプルだなと思いました。
他、Unity JobSystemとBurst Compilerについて、今後勉強しなくてはと思いました。


私は常日頃３Dシューティングゲームを作りたいと考えていて、弾丸や敵のスポーンにECSを活用してみる予定です。


明日は[Unity #2 Advent Calender 2018](https://qiita.com/advent-calendar/2018/unity2) 22日目、投稿者は[chroskeさん](http://chroske.hatenablog.com/)です！

# 参考
- [UnityのECS(Entity Component System)で雪の結晶を降らせる](https://qiita.com/orange_lily1127/items/77310e31e64485280377)
- [Unity-Technologies/EntityComponentSystemSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples)
- [【Unity】Unity 2018のEntity Component System（通称ECS）について（１）](http://tsubakit1.hateblo.jp/entry/2018/03/25/180203)
- [【Unity】Unity 2018のEntity Component System（通称ECS）について（２）](http://tsubakit1.hateblo.jp/entry/2018/04/02/233106)
- [【Unity】ECS + JobSystemで10万個のドカベンロゴをアニメーションさせてみた](https://qiita.com/mao_/items/4951c4bf0c82de98b5d6)
- [【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~Entity Component System 編~](https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018-training-daycjobsystem-ecscpu-entity-component-system-1)
- [Unity:はじめてのECS](https://qiita.com/simplestar/items/17b0886be0170f79aa2e)
- [IJobParallelForで並列処理させるジョブを作ってみる](https://www.shibuya24.info/entry/jobsystem2)
- [Unity JobSystemをとりあえずやってみる序](https://www.shibuya24.info/entry/jobsystem1)