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

こういうのを自分で操作するときも使いたい!
ということでやってみました。
目標
エディター処理でプログレスバーを表示してみる。
ついでに「キャンセル」とか、今処理中のファイル名とかも表示してみる。
環境
・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2
調べる
よくわからないので調査する。
とりあえず確定でプログレスバーが表示される操作を探して、その処理を追いかけてみます。
始めに画像を貼ったように「レベルを開く」と絶対表示されます。
いつものように「エディタの環境設定」→「一般」→「未分類」→「UIの拡張点の表示」にチェックを入れて再起動します。

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

このあたりの調査は過去に以下の記事で行いました。
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などで呼び出しましょう。
実行!!!!
自分で作ったエディター処理にプログレスバーを表示してみた。(すべてのレベルに処理する的なことをしてます。) pic.twitter.com/v5qBtikxBR
— なお@エンジニアっぽくなりたい (@naoyengineer) 2022年4月12日
という具合で、すべてのレベルを処理する際にプログレスバーを表示して、一つ毎に表示されている文字列を処理中のレベル名にできました。
コードのコメントにも書いてますが、キャンセル用の処理を書くことで、キャンセルボタン押したときの挙動を作成できます。
今回の対応でもキャンセルは有効です。
思ったこと
まぁググればすぐに出るような内容ですが、エンジンから該当の処理見つけt改めてちゃんとやってみましたっていう感じです。
かなり長い処理には、途中でキャンセルしたりできるのは結構有効だと思われます。
正しい対応方法やより良い方法があればご指摘お願いいたします!!!