エンジニアっぽくなりたい

UnityやUnrealEngine 4などでなにか役に立ちそうなことを発信していく。

UE4 エディター処理でプログレスバーを表示する

エディター系処理で結構長い時間がかかる処理をするときに、プログレスバーとか出して、今これくらいだよーって表示したくなることありますよね。
例えばエディターでレベルとか開くときに以下のような表示がされたりします。
f:id:naoxgames:20220411233815p:plain

こういうのを自分で操作するときも使いたい!

ということでやってみました。




目標

エディター処理でプログレスバーを表示してみる。
ついでに「キャンセル」とか、今処理中のファイル名とかも表示してみる。


環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2


調べる

よくわからないので調査する。
とりあえず確定でプログレスバーが表示される操作を探して、その処理を追いかけてみます。

始めに画像を貼ったように「レベルを開く」と絶対表示されます。

いつものように「エディタの環境設定」→「一般」→「未分類」→「UIの拡張点の表示」にチェックを入れて再起動します。
f:id:naoxgames:20220411234748p:plain

ファイルから「レベルを開く」を見ると「OpenLevel」と記述されているので、コードから「OpenLevel」を探します。
f:id:naoxgames:20220411235057p:plain


このあたりの調査は過去に以下の記事で行いました。
naoxgames.hatenablog.jp

この時の調査結果でFileHelpers.cppの

bool FEditorFileUtils::LoadMap(const FString& InFilename, bool LoadAsTemplate, bool bShowProgress)

の処理を通ることがわかっています。

明らかに怪しい「bShowProgress」という引数を追ってると以下の処理に入る様子

GEditor->Exec( NULL, *LoadCommand );


この処理である「UEditorEngine::Exec」の中を、「FEditorFileUtils::LoadMap」の「LoadCommand」を元に追うと、
UEditorEngine::HandleMapCommand
UEditorEngine::Map_Load

int32 bShowProgress = 1;
FParse::Value(Str, TEXT("SHOWPROGRESS="), bShowProgress);
~
FScopedSlowTask SlowTask(100, LocalizedLoadingMap, bShowProgress != 0);
SlowTask.MakeDialog();

SlowTask.EnterProgressFrame(10, FText::Format( NSLOCTEXT("UnrealEd", "LoadingMapStatus_CleaningUp", "{0} (Clearing existing world)"), LocalizedLoadingMap ));

というコードにたどり着きます。

「FScopedSlowTask」がプログレスバーの処理をやってくれているようです。

これを調べてみるといろいろ出てくる。
それらを参考に目標の要件を満たしてみる。


やってみる

今回は以下の記事で対応したレベルを開いて一括変換を行う、という処理に対してプログレスバーを付けてみようと思います。
naoxgames.hatenablog.jp

過去の内容と今回対応する内容を混ぜたものが以下の感じです。
内容は、すべてのレベルを開いて、すべてのActorの「ActorHiddenInGame」をtrueにするというもの。

MyBlueprintFunctionLibrary.h

#pragma once

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

#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS(BlueprintType)
class PROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
    
public:
    UFUNCTION(BlueprintCallable)
    static void SetAllHiddenForAllLevels();

    UFUNCTION(BlueprintCallable)
    static void OpenLevelForEditor(const FAssetData AssetData);

    UFUNCTION(BlueprintCallable)
    static void SaveCurrentLevel();
};



MyBlueprintFunctionLibrary.cpp

#include "MyBlueprintFunctionLibrary.h"
#include "FileHelpers.h"
#include "AssetRegistryModule.h"
#include "Misc/ScopedSlowTask.h"
#include "Kismet/GameplayStatics.h"
#include "Editor.h"


void UMyBlueprintFunctionLibrary::SetAllHiddenForAllLevels()
{
    FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
    IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

    TArray<FAssetData> AssetDatas;
    AssetRegistry.GetAssetsByClass(UWorld::StaticClass()->GetFName(), AssetDatas);

    FScopedSlowTask LevelTask(AssetDatas.Num());
    //プログレスバー表示
    LevelTask.MakeDialog(true);

    for (int i = 0; i < AssetDatas.Num(); i++)
    {

        //キャンセル処理
        if (LevelTask.ShouldCancel())
        {
            break;
        }

        //プログレスバー更新
        LevelTask.EnterProgressFrame(1, FText::FromString(AssetDatas[i].GetFullName()));

        OpenLevelForEditor(AssetDatas[i]);

        if (!GEditor)
        {
            continue;
        }

        TArray<AActor*> Actors;
        UGameplayStatics::GetAllActorsOfClass(GEditor->GetEditorWorldContext(false).World(),AActor::StaticClass(), Actors);

        for (AActor* Actor : Actors)
        {
            Actor->SetActorHiddenInGame(true);
        }

        SaveCurrentLevel();
    }
}

void UMyBlueprintFunctionLibrary::OpenLevelForEditor(const FAssetData AssetData)
{
    const FString FileToOpen = FPackageName::LongPackageNameToFilename(AssetData.PackageName.ToString(), FPackageName::GetMapPackageExtension());
    const bool bLoadAsTemplate = false;
    const bool bShowProgress = false;
    FEditorFileUtils::LoadMap(FileToOpen, bLoadAsTemplate, bShowProgress);
}

void UMyBlueprintFunctionLibrary::SaveCurrentLevel()
{
    FEditorFileUtils::SaveCurrentLevel();
}



また、プロジェクトの依存関係に以下を追加します。(.Build.cs)

PublicDependencyModuleNames.AddRange(new string[] { "UnrealEd" });



これの「UMyBlueprintFunctionLibrary::SetAllHiddenForAllLevels()」をEditorUtilityWidgetなどで呼び出しましょう。


実行!!!!



という具合で、すべてのレベルを処理する際にプログレスバーを表示して、一つ毎に表示されている文字列を処理中のレベル名にできました。
コードのコメントにも書いてますが、キャンセル用の処理を書くことで、キャンセルボタン押したときの挙動を作成できます。
今回の対応でもキャンセルは有効です。


思ったこと

まぁググればすぐに出るような内容ですが、エンジンから該当の処理見つけt改めてちゃんとやってみましたっていう感じです。
かなり長い処理には、途中でキャンセルしたりできるのは結構有効だと思われます。



正しい対応方法やより良い方法があればご指摘お願いいたします!!!