#1. はじめに
UE4のLevelはPersistentLevelとSubLevelの関係を持っており、起動中にPersistentLevelからSubLevelのLoad、Unloadを制御することができます。しかし、動的な生成、削除をすることはできず、Editor上から予め設定する必要がありました。
最近発覚したのですが、ソースコードをほんの少しだけ拡張するだけでSubLevelを動的に生成、削除することが出来たので、以下に纏めました。
#2. 前提
・GitHubから落とした4.14 Releaseを使用 (4.14-4.21で検証)
コンソールコマンド"stat levels"でLevelの現在の状態を表示した状態での結果を表示しています。以下、4つの操作を行っています。
【操作】
①PersistentLevel"Persistent"からSubLevel"Stage1"を生成
②PersistentLevel"Persistent"からSubLevel"Stage2"を生成
③PersistentLevel"Persistent"から"Stage1"をアンロード
④PersistentLevel"Persistent"から"Stage1"を削除
#4. 方法
BlueprintとEngineのソースをいじります。
上記のBlueprintはgifの操作を行った最終形です。
PersistentLevel"Persistent"に記載されている内容を示しています。
##4.1. Levelの生成
BlueprintNode"Load Level Instance"を使用しています。これは最初から用意されているNodeです(4.13時点では存在していたけどいつからあったんだろう...)。Inputの"LevelName"に予め作成しておいたLevel名を入力します。
##4.2. 生成したLevelのロード/アンロード
4.1.で取得した"ULevelStreamingDynamic"Typeの変数から"Should be Loaded"のフラグを制御しています。
##4.3. Levelの削除
エンジンのカスタマイズをするケース
レベル生成に使用したBlueprintNodeは存在するのですが、削除用のNodeが存在しないので作成、追加します。
以下のソースを追加することで上記の図にある"UnLoad Level Instance"を作成することができます。
●アンロード用関数
void ULevelStreamingDynamic::UnLoadLevelInstance(UObject* WorldContextObject, ULevelStreamingDynamic* UnloadLevel, bool& bOutSuccess)
{
bOutSuccess = false;
UWorld* const World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if(!UnloadLevel)
{
return;
}
UnloadLevel->SetShouldBeLoaded(false);
UnloadLevel->SetShouldBeVisible(false);
UnloadLevel->SetIsRequestingUnloadAndRemoval(true);
bOutSuccess = true;
}
●ヘッダ
UFUNCTION(BlueprintCallable, Category = LevelStreaming, meta = (WorldContext = "WorldContextObject"))
static void UnLoadLevelInstance(UObject* WorldContextObject, ULevelStreamingDynamic* StreamingLevel, bool& bOutSuccess);
エンジンのカスタマイズをしないケース
図のようにSetIsRequestingUnloadAndRemoval()を利用します。
細かい制御を行いたい場合は上記のように拡張するか、もしくはプロジェクト側のカスタム関数で作成する方が便利です。
#5. 補足
Level生成用のBlueprintNode"Load Level Instance"ですが、生成と同時にロード、表示を行う動作となっています。
それは関数内で生成と同時にロード、表示を行うように指定されているためこのような挙動となっています。
「生成だけ最初に行って、ロード、表示は任意のタイミングで行いたい!」という場合は、ノード本体の直値を変更すれば解決します。
●LevelStreaming.cppの1048~1052行目あたり
(各パラメータの説明は省略)
StreamingLevel->bShouldBeLoaded = true;
StreamingLevel->bShouldBeVisible = true;
StreamingLevel->bShouldBlockOnLoad = false;
StreamingLevel->bInitiallyLoaded = true;
StreamingLevel->bInitiallyVisible = true;
#6. おわりに
今回は大きなサイズのSubLevel読み込みやパッケージングでの検証は行ってのでもう少し検証する必要があるかもしれませんが、上記を利用することでPersistentLevelに予めSubLevelとして登録しておく必要がなくなり、PersistentLevelロード時間の短縮や、SubLevelをPersistentLevelから切り離して作業することが可能となり、より開発効率を上げる手助けとなるかと思います。
#7. 追記
※2016/11/27 追記
BlueprintNode"Load Level Instance"は4.12.5の時点では存在しないため4.13から追加されたもののようです。とはいえ、それ以前のバージョンでも同様の処理を移植すれば同じことが実現できると思います。
※2018/5/18 追記
4.19でSoftObjectReferenceでLevelを指定する関数が追加されました。こちらの方が今までのFNameやFStringによるレベル名の指定よりも変更に強くなります。このレベル名の指定で注意すべき点として、FNameやFStringで短いレベル名("TestLevel"など)を利用するとレベルアセットの検索コストがかかります。この問題の解決策はContentからのフルパスでのレベル名を指定することです。しかしSoftObjectReferenceの指定であれば、この検索コストの心配も不要です。
また、UnLoadの関数は4.19でもありません。上記ではEngineコードに直接Unloadの関数を追加していますが、UBlueprintFunctionLibraryを継承したプロジェクト独自のクラスで定義した方がエンジンのバージョンアップに併せたマージコストも減るので安全です。このLoadLevelInstance関数は便利ですが、上記でも記載のようにBlockLoadなどのパラメータが内包されているため、関数の実行と同時に必ずロードを開始してしまいます。非同期で読ませたいことを考慮すると、LoadLevelInstanceの関数をコピーして、独自のクラス内でパラメータを公開するような関数を作成した方が、より使い勝手が良いものになります。
LevelをInstance化することは、LoadStreamLevelでは出来ないLevelの複製ができるという点で優れています。これは"Map1"というLevelを複数個展開することができるということです。
また、自由に生成/削除が出来るということはシーン上に存在するメモリ量を制御する上でもメリットがあります。しかし、LoadStreamLevelはEditor上でPersistentLevelとSubLevelを登録する必要があるのでそれらの関係性が明確であるという点に対して、LoadLevelinstanceはどのシーンでどのレベルを読むかを把握しておかなくてはなりません。よってLoadLevelInstanceで読む必要があるレベルはテーブルなどで一元管理することも設計の上で考慮すべきです。
※2019/1/19 追記
LevelStramingKismetからLevelStramingDynamicに変更された箇所に対応しました。
※2019/9/19 追記
Unload処理について以前より安全な方法を追記しました。
※2020/5/24 追記
Unload処理について古い方の処理を削除して、GetWorldFromContextObjectのDepricated警告を修正しました。 (UE4.24.3)
※2021/3/23 追記
レベルインスタンスの削除にULevelStreaming::SetIsRequestingUnloadAndRemovalが利用できる旨を追記しました。 (UE4.23~)