#ローディング画面の実装
レベルの遷移など大量のアセットをロードするときに何も考えずにアセットをロードすると画面が固まってしまい格好悪いので一般的にはローディング画面を表示し準備が整ったら表示を開始します。
この実装方法はいくつかあります。代表的なものを並べてみましょう。
##1.ローディング用レベルを経由する
- 移動前のレベルからローディング画面に遷移
この時可能であればローディング画面のアセットを事前にロードしておくと素早く移動できます。 - 軽量なローディング画面に遷移
- 次のレベルで必要なアセットのリストを準備しておき、必要なアセットをすべて非同期ロードで読み込む
画面の描画内容に制限はありません。レベルを読み込む場合はレベルを読み込んだ時に作られるUWorldをGCされないようにガードしておく必要があります。 - アセットをすべて読み込み終わったら次のレベルへの移動をリクエストします。
次のレベルで必要なアセットの組み合わせが動的に組み代わり、さらに事前に必要なアセットが明解な場合に有用です。
予めアセットがメモリ上にあればレベルの移動はかなり短縮されます。
非同期のローディングにはAsset Managerを用いたローディングがお手軽でお勧めです。
##2.AsyncLoadingScreenを使う
こちらのプレゼンテーションが詳しいです。
https://www.slideshare.net/EpicGamesJapan/ss-135771323
レベルをロードしている間スレートの描画を別スレッドで行う実装です。
FDefaultGameMoviePlayerが肝です。
レンダリングを行うスレッドが入れ替わるような動作を行うため、レンダリング用APIなどの下で問題が発生するケースが少々見受けられます。
##3.サブレベルのストリーミングを使う
ほぼ空のパーシスタントレベルに移動した後、サブレベルとしてアセットを非同期読み込みします。
公式ドキュメントレベルストリーミングをご参照ください
https://docs.unrealengine.com/ja/Engine/LevelStreaming/index.html
##4.プラットフォームが持っているローディングスクリーンを使う
VRプラットフォームではメジャーな方法です。
僅かなヒッチでも酔いを引き起こしかねないため、厳しくチェックされます。それを通過するために滅茶苦茶お世話になります。
#この記事で紹介したい上記以外の実装方法の一つ
今回ご紹介する実装はAsyncLoadingScreenに近いです。
重要な点としては、
・GameViewportClientにWidget(Slate)を設定して描画し、レベルの移動。
・普通にWidgetを表示してブロッキングロードするとGameThreadが回っていないため画面が止まってしまう!
・なんとかしてローディング中も画面を更新したい・・・!そうだ!OnAsyncLoadingFlushUpdateを使おう!
というところです。
図にするとこんな感じでGameThreadで実行されているFlushLoading(ロードが終わるまで待つ処理)から定期的に呼び出されるデリゲートでSlateを更新していきます。
図の中の赤線がデリゲートの呼び出しのイメージを表しています。
サンプルプロジェクトを公開させていただきますのでご興味のある方はアクセスしてみてください。
#リポジトリ
プロジェクトはgithubにアップロードされています。
https://github.com/wankotank/SimpleLoadingScreen
ローカルに展開してUE4.25で開いてください。
#プロジェクト解説
コードを見てもらうのが手っ取り早いです。重要点だけ解説します。
##まず重要な設定
AsyncLoadingThreadは必須なので必ず設定してください。
##FSimpleLoadingScreenSystem
###ローディング中に呼び出されるデリゲートを登録します。
FSimpleLoadingScreenSystem::FSimpleLoadingScreenSystem( UGameInstance* InGameInstance )
:GameInstance( InGameInstance )
{
FCoreDelegates::OnAsyncLoadingFlushUpdate.AddRaw(this, &FSimpleLoadingScreenSystem::OnAsyncLoadingFlushUpdate);
}
進行状況の取得 GetLoadingProgress
この関数はGetAsyncLoadPercentageを用いてロードの進捗を取り出します。
GetAsyncLoadPercentageは仕様上細かい数字を返しません。
サブレベル毎でのロードの終了判定は正確なので、ある程度サブレベルに分割されているとより細かい進捗がとれるようになります
デリゲート
ロード中に呼び出されるデリゲートの中から定期的にFSlateApplication::Get().Tick()を呼び出します。
このシンプルなローディングの肝です。
OnAsyncLoadingFlushUpdateと名前の通りAsyncLoadingThreadが有効でない場合はこれが呼び出されないので注意してください。
/*このデリゲート関数はロード中に高頻度で呼ばれるので、適切な間隔でスレートの更新を呼ぶようにする*/
void FSimpleLoadingScreenSystem::OnAsyncLoadingFlushUpdate()
{
check(IsInGameThread());
QUICK_SCOPE_CYCLE_COUNTER(STAT_LoadingScreenManager_OnAsyncLoadingFlushUpdate);
const double CurrentTime = FPlatformTime::Seconds();
const double DeltaTime = CurrentTime - LastTickTime;
if (DeltaTime > 1.0f/60.0f )
{
LastTickTime = CurrentTime;
if( bShowing ){
// スレート更新
FSlateApplication::Get().Tick();
{
TGuardValue<int32> DisableAsyncLoadDuringSync(GDoAsyncLoadingWhileWaitingForVSync, 0);
FSlateApplication::Get().GetRenderer()->Sync();
}
}
LastTickTime = CurrentTime;
}
}
##USimpleLoadingScreenLibrary
ブループリントから関数を呼び出すためのブループリントライブラリクラスです。
##GameInstance
FSimpleLoadingScreenSystemはゲームインスタンス内で実体を作成し保持しておく必要があります。
Slate(UMG)の更新時はWorldが無い状態もありえるためGameInstanceを介してシステムにアクセスします。
##エディター内
###レベル内でのShow/Hideの呼び出し
ブループリントで実装しています /Game/BP_TravelToNextLevelWithLoadingScreen を参考にしてください。
このブループリントをレベルにポンと置いています。
#動作例
サブレベル分割して
— Takashi.Suzuki (@wankotank) October 29, 2020
ちょっとコード書き直したら割と良い感じになった。やるじゃん俺。
GetSubLevelsStatus()にDLLリンケージがついてなくて不貞寝するところだったけど歯を食いしばった。 pic.twitter.com/wwo9cPUnI6
#UnrealInsightで動作を確認。
AsyncLoadingThreadが動いている間、GameThreadはGameThreadでしかできない処理をしながらロードが終わるのを待ちますが、
その間に定期的にSlate::Tickが呼び出されていることが確認できます。
#検索用
ローディングスクリーン
LoadingScreen
AsyncLoadingScreen