この記事はUnreal Engine (UE) Advent Calendar 2024の4日目の記事です。
どうもお久しぶりです。とても久し振りの記事を書いております。Advent Calendearの時期なので、記事を久し振りに書くことにしました。
今回はUE5から導入されているレベル運用のシステムであるWorld Partition方式とSub Level方式の2つについての比較を実際に作成したプロジェクトをベースにして紹介したいと思います。
今回のプロジェクトはGitHub上にアップロードしておりますので、以下からDLしてみてください。
環境はUE5.5.0にて作成しております。
レベル運用システム
UE4まではレベル運用システムについてはSub Levelを使ったLevel Streamingでの運用がメインでした。一応World Compositionというシステムがあったのですが、こちらを使っているプロジェクトの話はほとんど聞いたことがありません。UE5でも残っているようですが、現在ほとんどの場合World Partitionに置き換えられています。
それぞれの概要と公式ドキュメントは以下となります。
今回はこれら2つの方式を全く同じ形状をしたレベルにて、比較していきたいと思います。
GitHubからサンプルプロジェクトをDLして開くと、以下のような構成になっています。
- Blueprints → ストリーミングロード用BPアクター
- FirstPerson → ファーストパーソンBPと入力周り
- FirstPersonArms → ファーストパーソンメッシュ
- LevelPrototyping → 地形メッシュ
- Maps → SubLevel用とWorldPartion用のレベル
Mapsの中身を開いてもらうと以下のようになっています。
SubLevel用のファイル
パーシスタントレベル → SubLevelMap
サブレベル → SubLevelsフォルダー
WorldPartition用のファイル
パーシスタントレベル → WorldPartitionMap
レベルインスタンス → LevelInstanceフォルダー
データレイヤー → DataLayerフォルダー
HLODレイヤー → WorldPartitionMap__HLODLayer_XXX ※今回は未使用
内部構成は上記となっていますので、ご確認ください。
プロジェクトを開くと、SubLevelMapを開いた状態でスタートします。
SubLevelMap、WorldPartitionMapどちらも全く同じメッシュ配置ですが、共通しているのは、島が4つの分かれており、それぞれをScene1~Scene4という区分けにしているので、個別にロード、アンロードを行う構成になっています。
ここからそれぞれの中身を解説していきます。
SubLevel方式
まずはSubLevel方式です。これは従来のLevel Streamingを使ったものになります。SubLevel方式では主に2つにストリーミング読み込みがあります。
まず1つ目が『Level Streaming Volume』です。
Level Streaming Volumeはそのボリューム内にカメラが侵入すると、特定のサブレベルのロードが始まります。逆にボリュームから外にでると自動的にアンロードが走ります。入った時にサブレベルの表示も行われますが、外に出てしまうとレベルがアンロードされてしまうので、細かい制御ができません。
こういったケースの場合はBP上でのノードを使ったロードが必要になります。それが2つ目の『Load Stream Level』ノードを使う方法です。
今回のサンプルではパーシスタントレベルのレベルBPと『BP_LoadLevelActor』というBPの2つに同じノードを組んでいます。この中ではロードすると同時にアンロードするための『Unload Stream Level』も含んでいます。
ボリュームへのオーバーラップ時に2つのレベルのロード、アンロードを行うことで、上手く切り替えを行うようにしています。基本的にLevel Streaming Volumeで対応できないケースは全てこちらの方法で読み込み制御を行っていくことになると思います。
今回はそれぞれの島をScene1Map~Scene4Mapというサブレベルにして、最初に立つScene1Mapのみ、ストリーミング方式で『常にロード済み』にしています。他の3つはボリューム内に侵入することで始めて表示されることになります。
基本的にはこれだけ理解しておけばSubLevel方式でのロードとアンロードは使いこなせるはずです。
WorldPartition方式
次はWorldPartition方式です。こちらはWorldPartitionMapを開いていただき、『ウィンドウ』から『World Partitionエディタ』パネルを開いてもらう必要があります。
次にミニマップモードで表示されますが、マウスで領域をドラッグしてロードしたい範囲を選択し、『選択から領域をロード』を選択します。これで全ての島がみえるようになります。
World Partitionは通常のレベルから作成する場合には、一度『レベルを変換』コマンドを実行する必要があります。これは公式ドキュメントにもありますので、上記で紹介していますので参照してください。
実際にPIEで実行するとWorldPartitionによる自動ストリーミングが実行されていることが確認できます。
WorldPartitionではレベルという単位では読み込みません。あくまでも配置しているアクターがカメラからの距離のグリッド内に入ったかどうかで読み込みを判断します。グリッドの表示は『ワールドセッティング』内の『World Partition設定』でオンオフできます。
少し深いところに設定があるので注意してください。『セルのサイズ』を変更することで、ロードするアクターの範囲を広くしたり狭くしたりできます。※最低サイズが1600
また『ロードする範囲』を変更することで、カメラの距離からグリッド内に入ったかの距離を変更することが可能です。
サンプルプロジェクトではかなり小さいですが、それぞれ1600と3200と設定して変化がわかりやすいようにしています。この数値は実際のオープンワールドであれば最低でも10倍から100倍程度になると思います。
ピンク色の線の部分がグリッドでカメラを動かすとロード対象になる範囲が色がピンク色で染まるようになるのでわかりやすいと思います。グリッドから離れると、その中にいるアクターは自動的にアンロードも行われます。
WorldPartitionを使うと、このように自動的にロードとアンロードが行われるため、ロード管理する手間が省かれて、面倒なことを考えることがなくなります。
が、今度は自動的にロード、アンロードされてほしくない場合もあります。その場合にはLevelInstanceとDataLayerを使って管理します。
LevelInstanceとDataLayerを使うことで、実質サブレベルに近い運用が可能です。
LevelInstance & DataLayer
LevelInstanceとDataLayerは本来同時に使う必要はありません。ただし、併用して使うことでよりSubLevelのワークフローに近くなりますので、ここではまとめて解説します。
LevelInstanceは複雑化してきたレベル作成フローを単純化するために、1つのアセット内に複数のアクターを配置して、個別に管理し、BPアクターのようにレベル上にいくつでも配置できるようにする仕組みです。表面上ではレベルそのものに見えるためにLevelInstanceという名前になっていますが、やっているのはロジックのないBPアクターのようなものです。
そしてDataLayerは名前の通りデータをレイヤーのように扱う仕組みです。DataLayerにはアクターをいくつでも格納することが可能です。DataLayer自体の親子構造も可能です。DataLayerはその内部に格納されたアクターを手動で読み込むことができます。つまりWorldPartitionにより、自動で読み込まれてしまう場合もDataLayerで制御が可能になります。
更にDataLayerにはLevelInstanceもアクターなので格納が可能です。つまりLevelInstanceを配置して、レベル編集を行い、それ自体をDataLayerで管理してしまえばほとんどSubLevelのような運用が可能になります。またDataLayerに格納された場合、WorldPartitionは自動的に読み込みを行いません。手動での読み込みが必要になるわけです。
読み込みは『BP_LoadDataLayer』というBPで制御しています。とは言え、やることは以下のことだけです。
『Set Data Layer Runtime State』ノードで『In State』を『Activated』にするだけでロードされます。WorldPartition管理下に入るので、その後は『In State』を『Unlodead』にしない限りは自動的に表示も行われるようになります。ただし、表示の制御はこちらではできません。
それは次で解説する『Is Spatially Loaded』プロパティが非常に重要な役割となります。
Is Spatially Loaded
WorldPartitionで管理可能なアクターには『Is Spatially Loaded』というプロパティが存在します。
このプロパティは非常に複雑でとても重要な役割を持つプロパティです。DataLayerに格納されているか、そうでないかで挙動が変わります。
- DataLayerに格納されずオンの場合 → WorldPartitionにより、自動的にロード、アンロード&表示、非表示される
- DataLayerに格納されずオフの場合 → WorldPartition関係なく、常にロード&表示される
- DataLayerに格納されてオンの場合 → In StateがActivateでロードされ、WorldPartitionにより表示される
- DataLayerに格納されてオフの場合 → In StateがActivateでロードされ、WorldPartition関係なく常に表示される
『In State』というのは『Set Data Layer Runtime State』ノードを使った場合の話です。DataLayerに格納されると、手動でロードしない限り、一切レベル上に表示することができませんので、自分で管理する必要が発生します。
『Is Spatially Loaded』の設定を的確にオフにしておくことで、WorldPartitonに投げっぱなしにならず、好きなタイミングで表示もできるようになるので、SubLevelにほぼ近い管理ができることになります。
おかずさんのポスト。
ちなみに、Spatially Loaded周りは公式ドキュメントにバッチリ書いてる
— おかず (@pafuhana1213) 2022年6月5日
Is Spatially Loadedが有効な場合、DataLayerがActivatedになっても「ストリーミングソースの範囲外なら生成されない」よという感じhttps://t.co/9LdpL18sHC pic.twitter.com/vrtLrqMNu8
これでSubLevelで運用してきていたものをほぼ同じレベルで置き換えることができました。
まとめ
WorldPartitionはSubLevelと違ってパーシスタントレベルが必ず1つしかありません。最初は扱いづらく、1つのレベルしか存在しないので分業もしづらいと感じるかもしれません。
しかしLevelInstanceとDataLayerを組合せればSubLevelでやってきたことがほぼそのまま適用できることがわかります。むしろWorldPartitionによる自動ストリーミング機能の恩恵に授かれる分、楽になるはずです。更に今回は解説していませんが、OneFilePerActor(OFPA)を自動的に使う必要がありますので、同じレベルを触ったりして取り合いになるようなこともありません。
今回配布したプロジェクトは2つのマップを開いて比較すると、どこがどう違うのかすぐに理解できるような作りにしています。最小限の内容しか含めていないので、SubLevelからWorldPartitionに移行したいと考えている人の学習にきっと合うはずです。
ぜひWorldPartitionの学習用にお役立てください!