Help us understand the problem. What is going on with this article?

レベルにまつわるエトセトラ

More than 1 year has passed since last update.

この記事は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"がレベル名になります。

  • そのアセットを右クリックして、「Refernce Viewer」を起動して、上部のアドレスをコピー
    image.png

後者の方が確実です。

参照について気をつける点がもうひとつ。パッケージにする際に、名前でロードされているレベルはどこからも参照されていることにならないため、パッケージに入らない事があります。参照をつくる方法はいくつかありますが

  • 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から目的のレベルを探すコードはこんな感じになります。

FindLevel.cpp
ULevelStreaming* FindSubLevel(UWorld* world, const FName levelName)
{
    for (ULevelStreaming* sublevel : world->GetStreamingLevels())
    {
        if (sublevel->GetWorldAssetPackageFName() == levelName)
        {
            return sublevel;
        }
    }
    return nullptr;
}

ゲームビルドで実行される場合はこれでも良いのですが、先程ちょっと触れたようにPIE実行時には実際のレベル名が変わってしまいます。PIEでも機能するようにするには次の様に修正します。

FindLevel.cpp
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を除去して照合します。

 目的のレベルが見つかったら、表示状態を変更します。

SetLevelVisible.cpp
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実行のことを考えないで良いところが違います。

FindEditorLevel.cpp
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;
}

 レベルが見つかったら表示を切り替えますが、こちらもエディタ上ではちょっと違います。

SetLevelVisibleInEditor.cpp
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

実際にやってみる

 先程のプラグインをコンパイルし、プロジェクトに追加しました。

 毎度おなじみサンテンプル
image.png

 もともとのライトを移動したサブレベルと、違う方向と色のライトを置いたサブレベルを作成し、ライティングシナリオに設定しました。

image.png

 こんな構成です
image.png

実行時のサブレベル切り替え

 レベルブループリントにキー入力でレベル切り替えする処理を入れて
image.png

 実行した結果
ezgif.com-video-to-gif.gif

 実はこの方法でレベルを切り替えても瞬時に切り替わるわけではなく、切り替え途中の状況が見えてしまうので実際のゲームで使用する場合はフェードを入れるなどしてごまかす必要があるかと思います。
 よく見ると一瞬、LIGHTING NEEDS TO REBUILTのメッセージも表示されています。

エディタのサブレベル切り替え

 次はエディタでのサブレベル切り替えです。アクター継承のBPをレベルに配置して、コンストラクションスクリプトとBlueprint Scriptでお手軽ツールを作ってみました。

 こんな構造体を用意して
image.png

 アクター継承のBPを作成

 変数
image.png

 コンストラクションスクリプト
image.png
レベルの一覧を取得しておきます。

 イベントグラフ
image.png
ボタンが押されたらイベントが発生してレベルの表示状態を切り替えます。
LevelSet1~LevelSet3には表示状態にしたいレベルを登録しておき、それ以外のレベルは非表示になります。

 このBPアクターをレベルに置いて
image.png

 Switch Set1とSwitch Set2をクリックすることでレベルの表示状態を切り替えることができます。

 Switch Set1をクリック
image.png

image.png

 Switch Set2をクリック
image.png

image.png

 みたいな感じで切り替わります。サンプルプラグインのContentにこのBPを入れてあるので参考にしてください。

 できればこの辺、コマンドラインからも指定できるようにして、複数のライティングシナリオのライトビルドをJenkinsとかで自動化したいところです。
 サンプルプラグインの中にライトビルドを実行するコマンドも入れてありますが、ライトビルドが終了した事を検知する方法をまだ調べてないので、その辺がわかれば複数のライトシナリオのライトビルドを自動化できそうです。
 Pythonプラグインから制御するとかも模索中です。

おまけ

PlayerStartを複数置いて、どれからスポーンするか選択したい

 GameModeやGameModeBaseのBPクラスを作成して、Choose Player Startをオーバーライドしてその中で選択してやれば任意のPlayerStartを選択できます。

image.png

 こんな感じで常にPoint3というタグを持つPlayer Startにスポーンするようにできます。
image.png

 実際のゲームでGetAllActorsを使うのは避けたいので、あらかじめテーブルを作っておくなどの工夫は必要です。
 この関数の返り値はActorクラスなので、Player Startで無くても、任意のActorの位置にスポーンさせることも可能です。

まとめ

 最近の仕事で関わったレベル関係のネタをまとめてみました。
 今まであまり触ってこなかった部分なので今更だったり誤りがあるかもしれませんがその辺はご容赦ください。
 ライティングシナリオは非常に有用なんですが、運用が煩雑になりがちですよね。

 明日は九谷 美生さんの記事です。お楽しみに。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした