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
を取得しつつ、その中のセクションのデータを取得して、カリングするかを判断していく形となります。
具体的には以下の通り
-
SceneReference
でforeachを回す(サブシーンのEntity取得) - 取得した
SceneReference
エンティティからResolvedSectionEntity
Bufferを取得 - Bufferから
SectionEntity
を取り出す(セクションのEntity取得) -
SectionEntity
からSceneSectionData
コンポーネントを取得 -
SceneSectionData
にBoundingVolume
があるので、これで内外判定をおこなう
以上のように少し回りくどい方法でシーンのロード、アンロードを行います。
ロードしているかしていないか
アンロードする際、何も考えずエリアに入っていないシーンをアンロードしていると、まだロードもしていないシーンをアンロードしてますよと怒られてしまいます。
それを防ぐために以下のタグ
を用意しました。
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のところはシーンごとに内外判定を行ってくれれば高速でシーンロードやアンロードができそうですよね。