Suutaの秘密基地

動く...動く...

UE4 非同期ローディング画面 試す

概要

前回、画面がカクつかないようにローディング画面をつくったけど、ローディング画面もカクついてしまった。 そこで、UE4無料サンプルプロジェクト<ActionRPG>を参考に非同期ローディング画面を実装してみた。 その中で実装に手こずったことをまとめた。

非同期ローディング画面とは?


非同期ローディング画面については、こちらの記事で詳しいことが書かれています。簡単にいえば、カクつかないローディング画面です。 LoadStreamLevel()がメイン処理の裏でレベルをロードをしてくれるのと同じく、処理を止めずにロード画面を表示してくれます。

やってみる

プロジェクトを作る

【プロジェクト】: C++
【プロジェクト名】: LoadingScreen

f:id:suuta1123:20191207225758j:plain
SourceにAsyncLoadingフォルダを追加して
"AsyncLoading.h"
"AsyncLoading.cpp"
"AsyncLoading.Build.cs" を追加。

私はUE4エディターからC++クラスを生成するのではなく、VisualStudio側から追加しました。
どちらでも良いと思いますが一応。

モジュール

ここが一番理解できなかった所... 非同期ロードはMoviePlayerを使ってロード画面を表示している。
しかし、ただMoviePlayer.hをインクルードしようとしても上手くいかなかった。結論から言うと ビルドする際にモジュールとして登録されてなかったっぽい? まだよく分かっていない...

➀ LoadingScreen.uproject をいじる。

f:id:suuta1123:20191207232804j:plain

デフォルトで生成されたコードに

{
    "Name": "AsyncLoading",
    "Type": "ClientOnly",
    "LoadingPhase": "PreLoadingScreen"
}

を追加する。

② LoadingScreen.Build.cs AsyncLoading.Build.cs を書く。

f:id:suuta1123:20191207235135j:plain

using UnrealBuildTool;

public class LoadingScreen : ModuleRules
{
    public LoadingScreen(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(
            new string[] {
                "Core",
                "CoreUObject",
                "Engine",
                "InputCore",
                "HeadMountedDisplay",
                "AsyncLoading",
                "MoviePlayer",
                "Slate",
                "SlateCore"
            }
        );
    }
}

f:id:suuta1123:20191207235141j:plain

using UnrealBuildTool;

public class AsyncLoading : ModuleRules
{
    public AsyncLoading(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(
            new string[] {
                "Core",
                "CoreUObject",
                "Engine",
                "InputCore",
                "HeadMountedDisplay",
                "AsyncLoading",
                "MoviePlayer",
                "Slate",
                "SlateCore"
            }
        );
    }
}

まんまコピぺする。

※ 2020/3/24 追記

以下の③Generate Visual Studio project filesをする。の工程は
C++ソースの➀・②の”AsyncLoading.cpp”、”AsyncLoading.h” まで作った状態で行ってください。

③ Generate Visual Studio project files をする。

f:id:suuta1123:20191208000215p:plain これでモジュール設定は完了しました。
.Build.csファイルや .uprojectファイルにビルドするモジュール範囲を指定せず、
Generate Visual Studio project filesをするとVisual StudioにAsyncLoadingフォルダが作られないので注意

C++ソース

➀ AsyncLoading.h

f:id:suuta1123:20191208004004j:plain

#pragma once

#include "ModuleInterface.h"
#include "Modules/ModuleManager.h"


class IAsyncLoadingModule : public IModuleInterface
{
public:

    static inline IAsyncLoadingModule& Get()
    {
        return FModuleManager::LoadModuleChecked<IAsyncLoadingModule>("AsyncLoading");
    }

    virtual void StartInGameLoadingScreen(bool bPlayUntilStopped, float PlayTime) = 0;
    virtual void StopInGameLoadingScreen() = 0;
};
② AsyncLoading.cpp

コードが長いので文字だけ

#include "AsyncLoading.h"
#include "SlateBasics.h"
#include "SlateExtras.h"
#include "MoviePlayer.h"
#include "SThrobber.h"


struct FAsyncLoadingScreenBrush : public FSlateDynamicImageBrush, public FGCObject
{
    FAsyncLoadingScreenBrush(const FName InTextureName, const FVector2D& InImageSize)
        : FSlateDynamicImageBrush(InTextureName, InImageSize)
    {
        SetResourceObject(LoadObject<UObject>(NULL, *InTextureName.ToString()));
    }

    virtual void AddReferencedObjects(FReferenceCollector& Collector)
    {
        if (UObject* CachedResourceObject = GetResourceObject())
        {
            Collector.AddReferencedObject(CachedResourceObject);
        }
    }
};


class SAsyncLoadingScreen : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SAsyncLoadingScreen) {}
    SLATE_END_ARGS()

    void Construct(const FArguments& InArgs)
    {
        static const FName LoadingScreenName(TEXT("/Game/UI/Texture/Loading"));
        LoadingScreenBrush = MakeShareable(new FAsyncLoadingScreenBrush(LoadingScreenName, FVector2D(512, 512)));

        FSlateBrush* BGBrush = new FSlateBrush();
        BGBrush->TintColor = FLinearColor(0.034f, 0.034f, 0.034f, 1.0f);

        ChildSlot
        [
            SNew(SOverlay)
            
            + SOverlay::Slot()
            .HAlign(HAlign_Fill)
            .VAlign(VAlign_Fill)
            [
                SNew(SBorder)
                .BorderImage(BGBrush)
            ]
            + SOverlay::Slot()
            .HAlign(HAlign_Center)
            .VAlign(VAlign_Center)
            [
                SNew(SImage)
                .Image(LoadingScreenBrush.Get())
            ]
            + SOverlay::Slot()
            .HAlign(HAlign_Fill)
            .VAlign(VAlign_Fill)
            [
                SNew(SVerticalBox)
                + SVerticalBox::Slot()
                .VAlign(VAlign_Bottom)
                .HAlign(HAlign_Right)
                .Padding(FMargin(30.0f))
                [
                    SNew(SCircularThrobber)
                    .Visibility(this, &SAsyncLoadingScreen::GetLoadIndicatorVisibility)
                    .NumPieces(15)
                    .Period(1.25f)
                    .Radius(40.f)
                ]
            ]
        ];
    }

private:

    EVisibility GetLoadIndicatorVisibility() const
    {
        bool Vis = GetMoviePlayer()->IsLoadingFinished();
        return GetMoviePlayer()->IsLoadingFinished() ? EVisibility::Collapsed : EVisibility::Visible;
    }

    TSharedPtr<FSlateDynamicImageBrush> LoadingScreenBrush;
};


class FAsyncLoadingModule : public IAsyncLoadingModule
{
public:

    virtual void StartupModule() override
    {
        LoadObject<UObject>(nullptr, TEXT("/Game/UI/Texture/Loading"));

        if (IsMoviePlayerEnabled())
        {
            CreateScreen();
        }
    }

    virtual bool IsGameModule() const override
    {
        return true;
    }

    virtual void StartInGameLoadingScreen(bool bPlayUntilStopped, float PlayTime) override
    {
        FLoadingScreenAttributes LoadingScreen;
        LoadingScreen.bAutoCompleteWhenLoadingCompletes = !bPlayUntilStopped;
        LoadingScreen.bWaitForManualStop = bPlayUntilStopped;
        LoadingScreen.MinimumLoadingScreenDisplayTime = PlayTime;
        LoadingScreen.WidgetLoadingScreen = SNew(SAsyncLoadingScreen);
        GetMoviePlayer()->SetupLoadingScreen(LoadingScreen);
    }

    virtual void StopInGameLoadingScreen() override
    {
        GetMoviePlayer()->StopMovie();
    }

    virtual void CreateScreen()
    {
        FLoadingScreenAttributes LoadingScreen;
    }

};

IMPLEMENT_GAME_MODULE(FAsyncLoadingModule, AsyncLoading);

ほとんどActionRPGのコードです。

static const FName LoadingScreenName(TEXT("/Game/UI/Texture/Loading"));
LoadObject<UObject>(nullptr, TEXT("/Game/UI/Texture/Loading"));

パスの文字列は自身のプロジェクトにあったものを指定してください。
ローディング画面の中心に表示される画像です。

③ MyBlueprintFunctionLibrary.h

次は、BPファンクションライブラリを作っておきます。
BPで実際に使う関数です。 ちなみにこれはエディターから生成しました。

f:id:suuta1123:20191208011240j:plain

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"


UCLASS()
class LOADINGSCREEN_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    UFUNCTION(BlueprintCallable, Category = "Loading")
    static void PlayAsyncLoadingScreen(bool bPlayUntilStopped, float PlayTime);

    UFUNCTION(BlueprintCallable, Category = "Loading")
    static void StopAsyncLoadingScreen();
};
④ MyBlueprintFunctionLibrary.cpp

実装です f:id:suuta1123:20191208011237j:plain

#include "MyBlueprintFunctionLibrary.h"
#include "ASyncLoading/AsyncLoading.h"


void UMyBlueprintFunctionLibrary::PlayAsyncLoadingScreen(bool bPlayUntilStopped, float PlayTime)
{
    IAsyncLoadingModule& LoadingScreenModule = IAsyncLoadingModule::Get();
    LoadingScreenModule.StartInGameLoadingScreen(bPlayUntilStopped, PlayTime);
}

void UMyBlueprintFunctionLibrary::StopAsyncLoadingScreen()
{
    IAsyncLoadingModule& LoadingScreenModule = IAsyncLoadingModule::Get();
    LoadingScreenModule.StopInGameLoadingScreen();
}

BPから使ってみる

BP_GameInstance

f:id:suuta1123:20191208143101j:plain

GameInstanceにロードイベントを作り

BP_MoveLevel

f:id:suuta1123:20191208143105j:plain

レベルロードのトリガーとなるActorを作り、
そのActorのオーバーラップイベントでロードイベントを呼び出す。

Name型のリテラルを返すだけのマクロ

f:id:suuta1123:20191208145004j:plain

ちなみにOpenLevel()に渡すLevelNameは、ただレベル名を渡すだけなく、絶対パスを渡してやると検索が早くなるそうだ。

スタンドアローンからプレイ

エディターからは確認できないので、スタンドアローンから起動するのを忘れずに。
f:id:suuta1123:20191208150846j:plain f:id:suuta1123:20191208151048j:plain f:id:suuta1123:20191208151225j:plain

まとめ

結果、非同期ローディングの実装よりもモジュールのビルド関連に手こずった感があった。
大きなプロジェクトの様に広大なレベルを扱っているわけでもなく、ロードが遅いわけでもないので、
変化があまり感じられなかった。規模が小さいうちは簡単にできる他の方法でごまかせるんじゃないかな。 また、UMGでなくSlateなのでビルドしないとロード画面のUIの変更が反映されず、スタンドアローンでないと動作確認できないのが辛かった。