【Unity ECS 2023 のお勉強】サンプルソースを読む (EntitiesSamples)

# 概要
このサンプルを見ながら気になったり調べたりした内容をだらだら書いてるPostです。
備忘がメイン目的なのであんまり深く調べたりはできてません。

https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/f22bb949b3865c68d5fc588a6e8d032096dc788a

今回はこの中の `EntitiesSamples` に関しての記述です。
見出しはその中の `HelloCube` の各ディレクトリの内容です。

# 1. MainThreads
## Spawner / Authoring / Baker
以下が詳しい。GameObjectをEntityの世界に召喚するための基本的な流れになるらしいから最初に知っとくと理解がはかどる。

https://zenn.dev/k41531/articles/5168cc291b4dfa

## RequireForUpdate: ISystemを起動するかどうかの条件指定
RotationSystemにある以下の記述

```c#:HelloCube/1.MainThread/RotationSystem.cs
[BurstCompile]
public void OnCreate(ref SystemState state)
{
    state.RequireForUpdate<Execute.MainThread>();
}
```

`RequireForUpdate<IComponent名>()` を記述することで、SubScene内にそのコンポーネントが存在しないと実行されないようにでき、IComponentをフラグとして用いる運用ができる。

具体的なやり方の一例
```c#:HelloCube/Common/ExecuteAuthoring.cs
public class ExecuteAuthoring : MonoBehaviour
{
    public bool MainThread;
    public bool IJobEntity;
    public bool Aspects;
    public bool Prefabs;
    public bool IJobChunk;
    public bool Reparenting;
    public bool EnableableComponents;
    public bool GameObjectSync;

    class Baker : Baker<ExecuteAuthoring>
    {
        public override void Bake(ExecuteAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);

            if (authoring.MainThread) AddComponent<MainThread>(entity);
            if (authoring.IJobEntity) AddComponent<IJobEntity>(entity);
            // 省略
        }
    }
}

public struct MainThread : IComponentData
{
}

 // 省略
```
SubScene内で以下のように指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/b300bbda-b452-1cfd-8691-a11e0bc97cfb.png)

これはサンプルソースだから一つのクラスでまとめてるんだろうから実際は別の運用にすると思うけど、フラグ管理としてIComponentを利用するケースがあると理解。

## [補足1] Singletonに関して

サンプルにはないんだけど、そういうフラグはSingletonみたいに利用できるのかって気になったので調べた。
結論いうとできる。
https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/components-singleton.html

たとえば、以下のように利用できる。

```c#:HelloCube/1.MainThread/RotationSystem.cs
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
   // Componentの取得
   var mainThread = SystemAPI.GetSingleton<Execute.MainThread>();
   // 以下省略
}
```

共通設定を入れておくとかすると活用できそう。

ただ、よくあるSingletonとは違って `一つしかないのを保証はしない` らしいし、 `存在するかどうかも保証しない` らしい。そこは運用でちゃんとしろ(自分らでちゃんとインスタンスを1個しか入れないようにしろ)ってことみたい。ほかにもいろいろ制約がある。

# 2. IJobEntity

IJobEntityの実行の仕方のサンプル。こうやって引数渡したり処理を外だしできるから再利用しやすくなるよ、くらいの感じなのかな。

## PostTransformMatrix
https://docs.unity3d.com/Packages/com.unity.entities@1.0/api/Unity.Transforms.PostTransformMatrix.html

Google翻訳
> 不均一スケールなどの非アフィン変換効果を実装するために使用されるオプションの変換行列。

うん、なるほどね。

動きとその前のソースコードと名称を見る限りはたぶん `LocalTransform` では Scale は float 値でもっており、y軸にだけ伸ばすというのができないから `PostTransformMatrix` を使ってるみたい。また、名前の通りここで設定した内容は `LocalTransform` のあとに適応されるらしい。

# 3. Aspects
`IAspect` とは複数の `IComponent` をまとめるような機能らしい。

サンプルソース
```c#:HelloCube/3.Aspects/RotationSystem.cs
readonly partial struct VerticalMovementAspect : IAspect
{
    readonly RefRW<LocalTransform> m_Transform;
    readonly RefRO<RotationSpeed> m_Speed;

    public void Move(double elapsedTime)
    {
        m_Transform.ValueRW.Position.y = (float)math.sin(elapsedTime * m_Speed.ValueRO.RadiansPerSecond);
    }
}
```

いきなり沸いてでてきて、どこからも `Add` とかもされてないのに、突然

```c#
foreach (var movement in
         SystemAPI.Query<VerticalMovementAspect>())
{
    movement.Move(elapsedTime);
}
```

みたいに使われるんで「何やつ！？」ってなるけど、struct内の変数

```c#
readonly RefRW<LocalTransform> m_Transform;
readonly RefRO<RotationSpeed> m_Speed;
```

を見てQueryを投げてくれるみたい。


# 4. Prefabs
Singletonが使われてる（作り方あっててよかった）
また、Randomの作り方のサンプルもある。

```c#
// Unlike new Random(), CreateFromIndex() hashes the random seed
// so that similar seeds don't produce similar results.
var random = Random.CreateFromIndex(updateCounter++);
```

というか、Structなのに普通に値を更新するメンバ変数も使うのか。
(そのあたりどうしようかとかなり悩んでたけど、公式サンプルが使ってるならやっていいのかな…まぁ、対象のISystemはゲーム起動中に1インスタンスしか起動されないだろうから問題はないのか）

## CommandBuffer

```c#:HelloCube/4.Prefabs/FallAndDestroySystem.cs
// An EntityCommandBuffer created from EntityCommandBufferSystem.Singleton will be
// played back and disposed by the EntityCommandBufferSystem when it next updates.
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
```

コマンドをひとまとめにし、特定のタイミングで一気に実行させるやり方らしい。

`BeginSimulationEntityCommandBufferSystem` が `CommandBuffer` を実行するタイミングを指定しているらしく、他にも以下が存在している（ループ処理で毎回 `Initialization` -> `Update` -> `PreLateUpdate` が呼ばれる)

* Initialization
    * BeginInitializationEntityCommandBufferSystem
    * EndInitializationEntityCommandBufferSystem
* Update
    * BeginSimulationEntityCommandBufferSystem
    * EndSimulationEntityCommandBufferSystem
* PreLateUpdate
    * BeginPresentationEntityCommandBufferSystem
    * EndPresentationEntityCommandBufferSystem

### 並列処理

上記の場合は直接的な処理になるが、 `ecb.AsParallelWriter()` みたいなやり方でコマンドを設定すると並列に処理が実行されるらしい。

https://zenn.dev/k41531/articles/5168cc291b4dfa#system%E3%81%AE%E6%9C%80%E9%81%A9%E5%8C%96%E3%82%92%E3%81%99%E3%82%8B

ただ、サンプルでは `DestroyEntity` を行っており、削除の処理は並列ではできない。

# 5.IJobChunk

IJobEntityと似ているが、実行時に手動でQueryを渡す必要がある。
かつ、 `state.Dependency` に設定することでJobの実行が保証される（ `IJobEntity` では、 `job.Schedule()` とするだけで保証されていた)

JobChunkに関しては、以下が詳しい
https://tsubakit1.hateblo.jp/entry/2018/10/16/231300
(2018年の記事のため、コードはもうだいぶ変わっている。ただ使い方の概念的なのはとても分かりやすく参考になる）

ちなみに、引数4つあるけど基本は `chunk` 使っておけばOKぽい？

# 6. Reparenting
Entity内の子Entity (Child) の取得などをするためのノウハウ。
`Parent` `Child` `PreviousParent` という `IComponentData` があらかじめ用意されている。
(その中で、 `PreviousParent` は自動で Add / Removeするから気にしなくていいらしい)

また、特定のタイミングではなく任意のタイミングで実行できる `CommandBuffer` についての記述もある。

```c#
var ecb = new EntityCommandBuffer(Allocator.Temp);
// ...コマンド設定
// 最後に実行する
ecb.PlayBack(state.EntityManager);
```

# 7. EnableableComponents
Componentの状態(Enable / Disable) を設定するやり方。
難しいことはやってないから見たらわかる感じ。

`SetComponentEnabled` と `System.Query<>().WithOptions(EntityQueryOptions.IgnoreComponentEnabledState)` を覚えておけばよさそう。

# 8. GameObjectSync
GameObject(+MonoBehavior) と ECSのEntityとの連携をするための方法。
ISystem内で普通に `GameObject.Find` や `GameObject#GetComponent` を呼び出せてる。
あと `IComponentData` を `struct` じゃなく `class` で定義してる。

また、Entity側には実態となる描画オブジェクトは存在せず、Entityの更新結果をGameObjectのTransformに伝える形でモデルを動かしている。
現在ECSではアニメーションをする際に結構面倒だと感じているが、プレイヤーなどアニメーションの多いものならこの方法で自由度が高く行えそう。

ただし、それを呼び出す場合 `[BurstCompile]` は使えないから処理としてはやっぱり遅くなるらしい。

```c#:HelloCube/8.GameObjectSync/DirectoryInitSystem.cs
public void OnUpdate(ref SystemState state)
{
    state.Enabled = false;

    var go = GameObject.Find("Directory");
    if (go == null)
    {
        throw new Exception("GameObject 'Directory' not found.");
    }

    var directory = go.GetComponent<Directory>();

    var directoryManaged = new DirectoryManaged();
    directoryManaged.RotatorPrefab = directory.RotatorPrefab;
    directoryManaged.RotationToggle = directory.RotationToggle;

    var entity = state.EntityManager.CreateEntity();
    state.EntityManager.AddComponentData(entity, directoryManaged);
}
```

`OnUpdate` だけど、 処理の最初で `state.Enabled = false;` としているため1回しか呼ばれない。