この記事はUnreal Engine 4 (UE4) Advent Calendar 2018の14日目の記事です。
記事の概要
この記事ではUE4のレベルの運用について、ちょっとイレギュラーな運用も含めてまとめてみました。
使用したUE4のバージョンは4.21です。
レベルとは?
まあ、今更語ることでも無いですが、レベルとは要するにマップのことです。メッシュやキャラクター、様々なActorを配置する場所と考えても良いかと。UE4ではゲームのシーンやフェーズなどレベル単位で作成して切り替えるのが基本です。
まずはレベルについて基本的なところを解説してみたいと思います。
パーシスタントレベルとサブレベル
レベルにはパーシスタントレベルとサブレベルがあります。UE4ではパーシスタントレベルを切り替えることでゲームの内容を遷移させます。パーシスタントレベルは常にひとつだけ読み込まれて、複数同時に存在できません。
(例外としてSeamless Travel時のTransition Levelというものがありますが、レベル遷移時のみの例外です。)
パーシスタントレベルは複数のサブレベルを持つことができます。サブレベルは個別にストリーミングで読み込んだり破棄したりすることができます。広大なマップを同時に読み込むだけのメモリが無い場合に、ゲームの進行中に移動範囲に応じて読み込んだり破棄したりといった使い方をしたり、ライティングシナリオを切り替えて、同じマップのライティング構成を変えて昼と夜を切り替えたりもできます。
レベルストリーミング
まずはレベルストリーミングの話から。
UE4ではレベルを読み込む際に、非同期に読み込むことでゲームの動きを止めずにレベルを読み込む機能があります。これをレベルストリーミングと言います。この機能を使えばローディング画面をアニメーションさせながらステージを読み込んだり、移動先のマップを先読みすることでシームレスに移動することができたりします。
もちろん良いことばかりではなく、十分なメモリが無いと読み込めないし、読み込み処理中は負荷がかかるので重たい処理と並行すると処理落ちしたりします。読み込みにはある程度の時間がかかるので読み込み開始したレベルが実際に表示されるまでには時間がかかります。なので「まだ見えていないけどもうすぐ見える領域」を読み込むなど、きちんとした設計が不可欠です。
ライティングシナリオ
UE4にはライティングシナリオという機能があります。詳しくは公式のドキュメントを見てもらうのが良いのですが、ざっくり言うと同じレベルだけどライト環境が違うものをライトマップを切り替えることで使いまわせる仕組みです。
サブレベルの表示切り替え
エディタ上ではレベルウィンドウでレベルの表示を切り替えることができます。これ、けっこう実際のゲームでもやりたくないですか?
ゲーム内でのサブレベルの切り替えは基本的にはBlueprintからのLoad/Unloadで行います。基本的にレベルはレベルのアセット名で指定します。オブジェクトではなく、名前でアクセスする必要があります。内部的にはName型なので文字列というわけではありません。
レベルの参照
サブレベルだけでなく、パーシスタントレベルも基本的に名前で参照します。Openで開くときも、Seamless Travelで移動するときも名前です。
この場合の名前はアセットのフルパス名で指定しましょう。レベル名だけでも探してくれますが、次のような問題があります。
- 同じ名前のレベルが複数ある場合は最初に見つかったものをロードする
- パッケージの場合はパッケージ内のアセットを全部検索して一致するレベルを検索する
この検索が曲者で、パッケージに膨大なアセットが含まれている場合、検索に非常に時間がかかる場合があります。PCビルドでは気付きにくいのですがコンソール機のCPUは非常に貧弱です。実際に関わったあるタイトルでは検索に30秒もかかっていたケースがありました。ロード時間が長いと思っていたら大半はレベルのロード前の検索時間でした。Nameの照合はIDを比較するだけで一瞬ですが、文字列での検索はNameから文字列に変換してアセット名を取り出して照合を繰り返すためかなり冗長な処理になります。面倒でもレベル名はかならずフルパスで指定しましょう。
アセットのフルパス名を取得する方法としては、次のようなやり方があります。
-
そのアセットを右クリックして、「リファレンスをコピー」を実行し、クリップボードにコピーされた文字列のシングルクオートで囲まれた文字列の最後のピリオド以下を消去。
World'/Game/Maps/Lighting2.Lighting2'の場合は、最後のピリオド以下を削除し、"/Game/Maps/Lighting2"がレベル名になります。
後者の方が確実です。
参照について気をつける点がもうひとつ。パッケージにする際に、名前でロードされているレベルはどこからも参照されていることにならないため、パッケージに入らない事があります。参照をつくる方法はいくつかありますが
- iniファイルに記述
- cook時に指定
- 参照テーブルを作っておく
などです。僕の場合はゲームのスタートアップ時に読み込まれるマップに間接参照テーブルを作成しておきます。こうしておけば、cook時にはそのマップだけ指定しておけば必要なマップが全てcookされてパッケージに入ります。
エディタ実行時のレベル名
エディタ上でゲームを実行することをUE4ではPlay in Editor、PIEと言います。PIE実行の際にレベルの名前がエンジン側で変更されるケースがあります。なぜそんな事になるかというと、エディタ上でプレイヤー数を複数にしてゲームプレイをした場合に、同じレベルが複数作成されるため、区別する必要があるからだと思います。これが少々曲者で、読み込む時に指定したレベル名と実際のレベル名が一致しないのです。ゲームビルドでは問題ないのですが、ゲームは動くのにエディタではうまく動かないなんてことが起きたりします。
アウトプットログを見ると
PIE: /Temp/UEDPIE_1_Untitled_1 0.298のプレイ イン エディタの開始時間
と出ていたりします。マップ名はUntitled_1なのですが、この"UEDPIE_1_"というのがエンジン側で付加されたプレフィクスです。
実行時にサブレベルの表示状態を切り替える
サブレベルの表示切り替えはBlueprint上からLoad Stream Levelで読み込んで、Unload Stream Levelで破棄します。
実際にゲームを作成していると、特定のサブレベルを一時的に非表示にしたり、あらかじめ読み込んでおいて必要なときに表示したくなります。こういった場合はどうすれば良いでしょう。残念ながらBlueprintにはそのような機能は用意されていないようです。シーケンサーからは制御できるみたいですが、確認していません。
そこでエンジンのソースを調べてみました。ここからはC++のお話。
ULevelとULevelStreaming
エンジン内部ではレベル自体はULevelというクラスなのですが、そのレベルの読み込みや管理にはULevelStreamingというクラスが使用されています。WorldはULevelを直接所有しておらず、ULevelStreamingのリストで所有しています。実際のレベル名もULevelStreamingクラスが持っています。
Worldから目的のレベルを探すコードはこんな感じになります。
ULevelStreaming* FindSubLevel(UWorld* world, const FName levelName)
{
for (ULevelStreaming* sublevel : world->GetStreamingLevels())
{
if (sublevel->GetWorldAssetPackageFName() == levelName)
{
return sublevel;
}
}
return nullptr;
}
ゲームビルドで実行される場合はこれでも良いのですが、先程ちょっと触れたようにPIE実行時には実際のレベル名が変わってしまいます。PIEでも機能するようにするには次の様に修正します。
ULevelStreaming* FindSubLevel(UWorld* world, const FName levelName)
{
for (ULevelStreaming* sublevel : world->GetStreamingLevels())
{
#if WITH_EDITOR
if (world->IsPlayInEditor())
{
if (world->RemovePIEPrefix(sublevel->GetWorldAssetPackageFName().ToString()) == levelName.ToString())
{
return sublevel;
}
continue;
}
#endif
if (sublevel->GetWorldAssetPackageFName() == levelName)
{
return sublevel;
}
}
return nullptr;
}
RemovePIEPrefixでレベル名からPIEのクライアントごとに付けられたPrefixを除去して照合します。
目的のレベルが見つかったら、表示状態を変更します。
void SetLevelVisible(const UObject* WorldContextObject, const FName levelName, bool bVisible)
{
ULevelStreaming* sublevel = FindLevel(WorldContextObject, levelName);
if (sublevel)
{
sublevel->SetShouldBeVisible(bVisible);
}
}
こんなふうにC++で実装すればレベルの表示を切り替えられそうです。
関数名がSetShouldBeVisibleなことからも類推できますが、すぐに表示状態を切り替えてくれるわけではなく、非同期に処理されて、数フレーム後に切り替わったりするので注意が必要です。
エディタ上でサブレベルの表示状態を切り替える
今度はエディタ上でBlueprintからサブレベルの表示切替を行う方法です。エディタ上では先程の方法では表示が切り替わってくれません。レベルウィンドウの表示も同期してくれません。
「実行時に切り替えられれば良いじゃない」と思うかもしれませんが、エディタの表示を切り替えたい大きな理由があります。
- ライティングシナリオを使う場合に、ライトベイクのたびにレベルの表示を切り替えるのが煩わしい
- 作業の種類によって表示にしておきたいレベルが違う。配置のときはメッシュ系のActorが置かれたレベルだけにしたい、レベルデザイン作業時はコリジョンも表示したいなど。
- 上記の様に実行時にレベル表示を切り替える場合、それぞれの状況をエディタで簡単に確認したい
など。
ということでエディタの表示を切り替える方法です。
まず対象のレベルを探すところです。
だいたい同じですが、実行時のWorldではなく、エディタのWorldを使用するのと、PIE実行のことを考えないで良いところが違います。
ULevelStreaming* FindEditorLevel(const FName levelName)
{
#if WITH_EDITOR
UWorld* world = nullptr;
if (GEditor)
{
world = GEditor->GetEditorWorldContext().World();
for (ULevelStreaming* sublevel : world->GetStreamingLevels())
{
if (sublevel->GetWorldAssetPackageFName() == levelName)
{
return level;
}
}
}
#endif
return nullptr;
}
レベルが見つかったら表示を切り替えますが、こちらもエディタ上ではちょっと違います。
void SetLevelVisibleInEditor(const FName levelName, bool bVisible)
{
#if WITH_EDITOR
ULevelStreaming* sublevel = FindEditorLevel(levelName);
if (sublevel)
{
if (sublevel->HasLoadedLevel())
{
ULevel* levelObject = sublevel->GetLoadedLevel();
EditorLevelUtils::SetLevelVisibility(levelObject, bVisible, false);
}
}
#endif
}
エディタ上でのレベルの表示状態はULevelStreamingではなく、それが所有しているULevelに対して行います。
細かい処理はEditorLevelUtilsというヘルパークラスがやってくれます。
サンプルプラグインを作ってみました。
Blueprintとコンソールコマンドから実行できるようにしてあります。
https://github.com/dgtanaka/ue4_advcal2018
実際にやってみる
先程のプラグインをコンパイルし、プロジェクトに追加しました。
もともとのライトを移動したサブレベルと、違う方向と色のライトを置いたサブレベルを作成し、ライティングシナリオに設定しました。
実行時のサブレベル切り替え
レベルブループリントにキー入力でレベル切り替えする処理を入れて
実はこの方法でレベルを切り替えても瞬時に切り替わるわけではなく、切り替え途中の状況が見えてしまうので実際のゲームで使用する場合はフェードを入れるなどしてごまかす必要があるかと思います。
よく見ると一瞬、LIGHTING NEEDS TO REBUILTのメッセージも表示されています。
エディタのサブレベル切り替え
次はエディタでのサブレベル切り替えです。アクター継承のBPをレベルに配置して、コンストラクションスクリプトとBlueprint Scriptでお手軽ツールを作ってみました。
アクター継承のBPを作成
コンストラクションスクリプト
レベルの一覧を取得しておきます。
イベントグラフ
ボタンが押されたらイベントが発生してレベルの表示状態を切り替えます。
LevelSet1~LevelSet3には表示状態にしたいレベルを登録しておき、それ以外のレベルは非表示になります。
Switch Set1とSwitch Set2をクリックすることでレベルの表示状態を切り替えることができます。
みたいな感じで切り替わります。サンプルプラグインのContentにこのBPを入れてあるので参考にしてください。
できればこの辺、コマンドラインからも指定できるようにして、複数のライティングシナリオのライトビルドをJenkinsとかで自動化したいところです。
サンプルプラグインの中にライトビルドを実行するコマンドも入れてありますが、ライトビルドが終了した事を検知する方法をまだ調べてないので、その辺がわかれば複数のライトシナリオのライトビルドを自動化できそうです。
Pythonプラグインから制御するとかも模索中です。
おまけ
PlayerStartを複数置いて、どれからスポーンするか選択したい
GameModeやGameModeBaseのBPクラスを作成して、Choose Player Startをオーバーライドしてその中で選択してやれば任意のPlayerStartを選択できます。
こんな感じで常にPoint3というタグを持つPlayer Startにスポーンするようにできます。
実際のゲームでGetAllActorsを使うのは避けたいので、あらかじめテーブルを作っておくなどの工夫は必要です。
この関数の返り値はActorクラスなので、Player Startで無くても、任意のActorの位置にスポーンさせることも可能です。
まとめ
最近の仕事で関わったレベル関係のネタをまとめてみました。
今まであまり触ってこなかった部分なので今更だったり誤りがあるかもしれませんがその辺はご容赦ください。
ライティングシナリオは非常に有用なんですが、運用が煩雑になりがちですよね。
明日は九谷 美生さんの記事です。お楽しみに。