ECS事始め

# はじめに

発表されてからだいぶ経つがまだ正式版のリリースされていないECS。
ただ、最近v0.50までアップデートされた模様。
v1.0まであと少しとなっていそう？なこのあたりでそろそろ触っておこうと考えその作業ログを残す。

# この記事では何をするか

1. Installation & setup guideに従いECS環境を構築
1. サンプルコードをざっくり読解
1. 簡単なコードを記述してみる

# 環境

MacBook Pro (13-inch, M1, 2020)
macOS Monterey
Unity 2020.3.30f1[^1]
[^1]: 2022.06.25現在のECSインストールガイドに準拠します。Intelチップでないため動作は怪しいかも

# インストール

>　Unity Editor version
>　You must use Unity Editor version 2020.3.30 with Entities 0.50.

とのことなのでそれに従う。

## マニフェストの編集

manifest.jsonに下記を追記する。
```
"com.unity.entities": "0.50.1-preview.2"
```
なお、PackageManagerの"Add package　from　git　url"から"com.unity.entities"を入力でも追加し、ダウングレードするでもOK。

コンパイルが走り、特にエラーも表示されず完了。

## Rendererの追加

描画にはHybrid Rendererを使用するとのこと。
```
"com.unity.rendering.hybrid"
```

# サンプルを動かす

ひとまずHybrid Renderer のサンプルを動かしてみる。
[Runtime Entity Creation](https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@0.50/manual/runtime-entity-creation.html) に記載の通り、作成時に必要なComponentはRendererUtilityを使用して追加する。

![10ヶ並べた結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124220/200b3c8b-95af-7007-1a5f-a7d1767c61c9.png)

１０個並べ、表示できることを確認。

## ざっくり読解

なお、少し　Runtime Entity Creation　のコードそのものからは変更しています。
おおよそは同じです。

``` csharp:code
public class EntityHandler : MonoBehaviour
{
    public Mesh mesh;
    public Material material;
    public int entityCount;

    // Example Burst job that creates many entities
    [BurstCompatible]
    public struct SpawnJob : IJobParallelFor
    {
        public Entity Prototype;
        public EntityCommandBuffer.ParallelWriter Ecb;

        public void Execute(int index)
        {
            // プロトタイプとなるEntityからクローンし、新たにEntityを生成.
            // Entityの生成や変更等々はメインスレッドで実行する必要があるため、
            var e = Ecb.Instantiate(index, Prototype);
            // 必要なComponentは前もって設定されているため、それぞれのEntityに対し個別の処理を実行することが可能。
            // ここではindexに応じてTransformを変更している。
            Ecb.SetComponent(index, e, new LocalToWorld { Value = ComputeTransform(index) });
        }

        public float4x4 ComputeTransform(int index)
        {
            // indexに応じて適当に座標をずらす
            const int xMax = 100;
            var x = index % xMax;
            var z = index / xMax;
            return float4x4.Translate(new float3(x, 0, z));
        }
    }

    void Start()
    {
        CreateEntity();
    }

    void CreateEntity()
    {
        // デフォルトのWorldを取得
        var world = World.DefaultGameObjectInjectionWorld;
        // 対象のWorldに存在するEntityの管理はすべて、EntityManagerを通して行う
        var manager = world.EntityManager;

        // スレッドセーフなコマンドバッファ。後ほど、Job内でEntityを生成するコマンドを積むために用意
        // Jobで使用するためTempJobで生成
        EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);

        // 変更されうるHybridRenderのコンポーネント一式が正しくEntityに追加されるよう、
        // RenderMeshDescriptionを生成
        var desc = new RenderMeshDescription(
            this.mesh,
            material,
            shadowCastingMode: ShadowCastingMode.Off,
            receiveShadows: false);

        // 空のEntityから、必要なComponentを追加して作りたいEntityを作成する。
        // RenderMeshDescriptionを使う関係上、ArcheTypeを先に作成することは難しいと思われる。
        // なおEntityManagerのAPIを使用してEntityを生成するのは、一般的には最も効率が悪いとのこと
        var prototype = manager.CreateEntity();
        
        // 対象のEntityに、HybridRendererが要求するComponentをAddする
        RenderMeshUtility.AddComponents(
            prototype,
            manager,
            desc);
        // と、LocalToWorldを追加
        manager.AddComponentData(prototype, new LocalToWorld());

        // 事前に用意しておいたプロトタイプとなるEntityを、Jobを通してランタイムで大量に生成する。
        var spawnJob = new SpawnJob
        {
            Prototype = prototype,
            Ecb = ecb.AsParallelWriter(),
        };
        // Jobをスケジュール.
        // JobQueueあたりのバッチ数はサンプル通り128。公式のScriptingReferenceによれば、単純なJobなら32-128推奨とのこと
        var spawnHandle = spawnJob.Schedule(entityCount, 128);
        
        // サンプルなので即時待ちでOK
        spawnHandle.Complete();

        // EntityCommandBufferに溜めていた処理を一気に実行
        ecb.Playback(manager);
        ecb.Dispose();
        manager.DestroyEntity(prototype);
    }
}
```

# もう少しいじってみる

Systemを追加し、y軸を時間でSin波に沿って動かす。
問題なく動いていそう。
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124220/21b71e76-2425-f36c-421e-c7212a1fd111.gif)

``` csharp:code
using System;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

public partial class SinHeightSystem : SystemBase
{
    protected override void OnUpdate()
    {
        var time = Convert.ToSingle(Time.ElapsedTime);
        Entities.ForEach((ref Translation translation) =>
        {
            var pos = translation.Value;
            translation.Value.y = 5 * math.sin(math.PI * (time + (pos.x + pos.z) * 0.025f));
        }).ScheduleParallel();
    }
}
```

# v0.51に上げてみる

v0.51がリリースされたため、上述のプロジェクトをそのままUnity2021.3.4f1に放り込んで見る。
![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124220/0bc6a176-636d-932d-9849-3278fd37539c.gif)
良い感じ。（2020でのStatsはSceneViewも同時に描画していたためfpsが出ていませんでした）

packageの主要な差分は以下の通り：
```
-    "com.unity.entities": "0.50.1-preview.2",
+    "com.unity.entities": "0.51.0-preview.32",
-    "com.unity.rendering.hybrid": "0.50.0-preview.44",
+    "com.unity.rendering.hybrid": "0.51.0-preview.32",
```

# 終わりに

- DOTSの重要パッケージの一つ、Entitiesについて触れた
- ざっと動かすにあたり、どういった手順を踏めば動くのかの感触は得られた

次はもう少し実践的に組み込んで、使用感をチェックしたいところです。

# 参考資料

こちらの記事を大いに参考にさせていただきました。
https://qiita.com/simplestar/items/17b0886be0170f79aa2e

https://unity.com/roadmap/unity-platform/dots

https://docs.unity3d.com/Packages/com.unity.entities@0.50/manual/index.html

https://docs.unity3d.com/ja/2018.4/Manual/JobSystem.html

https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@0.50/manual/index.html

https://learning.unity3d.jp/4968/

https://learning.unity3d.jp/3266/