UnityECS サブシーンのロードとアンロード

# ECSStreaming
UnityECSではサブシーンごとにシーンのロード、アンロードができるようになっています。
今回はこの機能を使って、カメラの視推台を自分で作成し、そのカメラの見える範囲のシーンをロードし、見えなくなったらシーンをアンロードするようなシステムを作っていきます。

# カメラの準備
Unityでは視推台を作るのにカメラを使うと簡単に取得できます。
```csharp
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
```
そして、シーンの範囲がその視推台に入っているかどうかの確認も以下でできます。
```csharp
var bound = AABBExtensions.ToBounds(sectionData.BoundingVolume);
bool isPlane = GeometryUtility.TestPlanesAABB(planes, bound);
```
今回はこれを使ってECSで簡単に視推台カリングによるシーンのロード、アンロードを実装していきます。
`Plane[]`はマネージドな構造体のようなので、BurstやJob化することはできなく、`ISystem`ではなく`SystemBase`で実現しています。

# ECSのサブシーンの構造
ECSのシーンのバウンディングボックスについてですが、これは、サブシーンから取得することはできません。
サブシーンをオーサリングしたとき、その下の階層にセクションを作ることができるのですが、バウンディングボックスはこのセクション単位で取得する形となります。
セクションは何もしなければ1ですが、ユーザーがサブシーンにセクションを追加するごとに増えていきます。

そしてシーンのロードは当たり前ですがサブシーン単位です。
ですので、サブシーンをforeachしてサブシーンの`Entity`を取得しつつ、その中のセクションのデータを取得して、カリングするかを判断していく形となります。

具体的には以下の通り
1. `SceneReference`でforeachを回す(サブシーンのEntity取得)
1. 取得した`SceneReference`エンティティから`ResolvedSectionEntity`Bufferを取得
1. Bufferから`SectionEntity`を取り出す(セクションのEntity取得)
1. `SectionEntity`から`SceneSectionData`コンポーネントを取得
1. `SceneSectionData`に`BoundingVolume`があるので、これで内外判定をおこなう

以上のように少し回りくどい方法でシーンのロード、アンロードを行います。

# ロードしているかしていないか
アンロードする際、何も考えずエリアに入っていないシーンをアンロードしていると、まだロードもしていないシーンをアンロードしてますよと怒られてしまいます。
それを防ぐために以下の`タグ`を用意しました。
```csharp
public struct IsLoad : IComponentData
{}
```
これをロードしたEntityに`AddComponent`して、アンロードするときはこの`IsLoad`コンポーネントがあるかどうか判断し、ある場合のシーンについてアンロードを走らせるという処理を行います。

# System
以下がそのシステムになります。
```csharp
partial class SceneLoadSystem : SystemBase
{
    protected override void OnUpdate()
    {
        // カメラの視推台を取得
        Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
        // ロードするシーン、ロードしないシーンを追加する用のList
        NativeList<Entity> loadScene = new NativeList<Entity>(Allocator.Temp);
        NativeList<Entity> unloadScene = new NativeList<Entity>(Allocator.Temp);

        // AddComponentに必要なEntityCommandBuffer
        EntityCommandBuffer ecb = SystemAPI.GetSingletonRW<EndSimulationEntityCommandBufferSystem.Singleton>().ValueRW.CreateCommandBuffer(World.Unmanaged);
        // SceneReferenceに対してforeach
        foreach (var (sceneReference, entity) in SystemAPI.Query<SceneReference>().WithEntityAccess())
        {
            // ResolvedSectionEntity Bufferを取得
            var sectionBuffer = SystemAPI.GetBuffer<ResolvedSectionEntity>(entity);
            // Bufferを配列化
            var sectionEntities = sectionBuffer.ToNativeArray(Allocator.Temp);
            // Bufferに入っているもの(セクション)について取り出す
            for (int i = 0; i < sectionEntities.Length; i += 1)
            {
                // セクションのエンティティを取得
                var sectionEntity = sectionEntities[i].SectionEntity;
                // セクションのエンティティからSceneSectionDataを取得
                var sectionData = SystemAPI.GetComponent<SceneSectionData>(sectionEntity);
                // SceneSectionDataからバウンディングボックスを取り出し、比較するようにboundに加工
                var bound = AABBExtensions.ToBounds(sectionData.BoundingVolume);
                // カメラにセクションのboundが入ってるか判定
                bool isPlane = GeometryUtility.TestPlanesAABB(planes, bound);

                // カメラの範囲内にセクションがあった場合、ロードするリストに格納、ロードしましたよのコンポーネントを追加
                if (isPlane)
                {
                    loadScene.Add(sectionEntity);
                    ecb.AddComponent<IsLoad>(entity);
                }
                else
                {   // カメラ範囲内になくて、すでにロードされていた場合、アンロードのリストに格納、ロードしましたタグを削除
                    if (EntityManager.HasComponent<IsLoad>(entity))
                    {
                        unloadScene.Add(sectionEntity);
                        ecb.RemoveComponent<IsLoad>(entity);
                    }
                }
            }
            sectionEntities.Dispose();
        }
        // 上記でリストに詰めたものをそれぞれ処理
        // ここで距離によってソートも可能(それ用のコンポーネントを付ける必要があるが)
        foreach (var loadEntity in loadScene)
        {
            // シーンのロード
            SceneSystem.LoadSceneAsync(World.Unmanaged, loadEntity);
        }
        foreach (var unloadEntity in unloadScene)
        {   // シーンのアンロード
            SceneSystem.UnloadScene(World.Unmanaged, unloadEntity);
            
        }
        loadScene.Dispose();
        unloadScene.Dispose();
    }
}
```

これでカメラ内に入っているシーンをロードしたりアンロードできるようになりました。

# 今後に向けて
やはりBurstやJobシステムで高速化がやりたいので、カメラのデータを元に、構造体でPlaneを自分で作り、内外判定も独自実装で行えるようになりたいなと思いました。
特にforeachのところはシーンごとに内外判定を行ってくれれば高速でシーンロードやアンロードができそうですよね。