0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 24

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

Last updated at Posted at 2024-12-23

ECSStreaming

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

カメラの準備

Unityでは視推台を作るのにカメラを使うと簡単に取得できます。

Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);

そして、シーンの範囲がその視推台に入っているかどうかの確認も以下でできます。

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取得)
  2. 取得したSceneReferenceエンティティからResolvedSectionEntityBufferを取得
  3. BufferからSectionEntityを取り出す(セクションのEntity取得)
  4. SectionEntityからSceneSectionDataコンポーネントを取得
  5. SceneSectionDataBoundingVolumeがあるので、これで内外判定をおこなう

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

ロードしているかしていないか

アンロードする際、何も考えずエリアに入っていないシーンをアンロードしていると、まだロードもしていないシーンをアンロードしてますよと怒られてしまいます。
それを防ぐために以下のタグを用意しました。

public struct IsLoad : IComponentData
{}

これをロードしたEntityにAddComponentして、アンロードするときはこのIsLoadコンポーネントがあるかどうか判断し、ある場合のシーンについてアンロードを走らせるという処理を行います。

System

以下がそのシステムになります。

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のところはシーンごとに内外判定を行ってくれれば高速でシーンロードやアンロードができそうですよね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?