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

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

UE4 各Level内のBP設定値を一括で修正

例えばとあるアクターのメンバ変数名を変更したくなったとします。
ですがC++で変更するとその変数に設定されていた値はリセットされてデフォルトの値になります。
思いつく対応策は一時的に変更前と変更後の変数を共存させて、変更前の値を変更後の変数に代入する、そして変更前の変数を削除 みたいなことをすればできそうです。
ですが、レベルに直接配置され、各レベルで違った設定をしていた場合、どうやってすべてを適切に変更後の変数に設定できるのでしょう。
すべてのレベルを開いて設定しなおす!が通ればそれがいいかもしれませんが、実際はそうもいきません。
今回はそんな時にどうしたらいいのか考えてみました。




目標・要件

各レベルに配置されたBPの設定値を変更する。
この変更処理はエディターから行う。


環境

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


やってみる

作戦としては、
・何かしらの手段で、エディターから任意のレベルを開く
・エディターからレベルに配置されているアクターを取得して変更を加える
・レベルを保存する
・レベルの数分繰り返す
という感じです。

ですがすべてはエディター上という縛りがあり、通常思いつく手段が使えない可能性があります。
頑張ります。

今回の検証は以下の内容を行います。
・複数あるレベルに配置された「AActor」の「ActorHiddenInGame」を「True」にする。
これができたら要件を満たしたとします。
また、対応は「エディターユーティリティウィジェット」で行おうと思います。


エディターユーティリティウィジェットを作成

今回はエディターからの処理を簡単に行いたいのでエディターユーティリティウィジェットを使います。
コンテンツブラウザを右クリックし、「エディターユーティリティウィジェット」を選択します。
f:id:naoxgames:20220214032858p:plain
こんな感じ
f:id:naoxgames:20220214033025p:plain

エディターユーティリティウィジェットを開き、ボタンを配置します。
f:id:naoxgames:20220214033106p:plain

ボタンを選択し、詳細から「イベント」の「onClicked」のところにある「+」をクリックし、クリックイベントを作成します。
f:id:naoxgames:20220214033228p:plain

イベントグラフに以下のようなイベントが作成されます。
f:id:naoxgames:20220214033315p:plain

コンテンツブラウザから作成したエディターユーティリティウィジェットを右クリックし、「エディターユーティリティウィジェット」をクリックします。
f:id:naoxgames:20220214033359p:plain
すると以下のように先ほど設定したボタンが表示されます。
f:id:naoxgames:20220214033445p:plain

このボタンを押すことで、先ほどのボタンのイベントが呼ばれるようになります。


何かしらの手段で、エディターから任意のレベルを開く

とりあえず、パッと思いつくレベル開く系の処理は使えませんでした!!!!
OpenLevel的なやつです。

他の道がないか探したところ、エディターのメニューの「ファイル」に「レベルを開く」があることに気づく。
f:id:naoxgames:20220214033637p:plain
この処理をパクろう!!!!

ということで、エンジンからこの処理を探します。

まずはUIの拡張点を表示します。
「編集」→「エディタの環境設定」→「拡張点」で検索→「UIの拡張点の表示」にチェックを入れます。
f:id:naoxgames:20220214033745p:plain
エンジンの再起動をしてください。

再度「レベルを開く」を確認すると、緑色の文字で「OpenLevel」と表示されているのがわかります。
f:id:naoxgames:20220214033955p:plain
※私は始めから設定していたので前のスクショと見た目変わってません。

このキーワードをエンジンコードから探します。
「"Open Level"」と検索したところ以下がヒットしました。
FileHelpers.cpp

OpenAssetDialogConfig.DialogTitleOverride = LOCTEXT("OpenLevelDialogTitle", "Open Level");


この処理付近を調査すると、この後のダイアログを開く処理の引数に、レベル選択時のコールバックを設定しています。

ContentBrowserModule.Get().CreateOpenAssetDialog(OpenAssetDialogConfig,
                                                     FOnAssetsChosenForOpen::CreateStatic(&FLocal::OnLevelsSelected, OnLevelsChosen),
                                                     FOnAssetDialogCancelled::CreateStatic(&FLocal::OnDialogCancelled, OnLevelPickingCancelled));

この「OnLevelsChosen」はこの処理が書かれている関数の引数に入ってきたものなので、この処理あたりにブレイクを張って実際に「レベルを開く」を実行してみます。

呼び出し履歴から追うと、
FEditorFileUtils::LoadMap()で行っていることがわかります。

さらに追っていくと
FEditorFileUtils::LoadMap(引数あり版)で処理している様子。

この処理を同じように使えば自分でもエディター上でレベル開けるのでは?ということで試してみます。

以下のようなクラスを作成します。(今回はBlueprintFunctionLibraryで行います。)

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 OpenLevelForEditor(const FAssetData AssetData);
};



MyBlueprintFunctionLibrary.cpp

#include "MyBlueprintFunctionLibrary.h"
#include "FileHelpers.h"

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



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

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


これでエディター上でレベルを開く準備ができました。
エディターユーティリティウィジェットのイベントで以下のように処理を作成します。
f:id:naoxgames:20220214035325p:plain
内容としては、AssetRegistryを利用してレベルのアセット(Worldというクラスらしい)を取得、それらを上記で作成した処理を使いレベルを開きます。
PrintStringは画面に文字は表示されませんが、アウトプットログで確認できるので使用しています。

これによってエディターでレベルを開けるようになりました。


エディターからレベルに配置されているアクターを取得して変更を加える

GetAllActorsOfClassを使いたい!!!
そうするとエディター上でワールドを取得する処理が必要になります。
いろいろ調べてみると、取得する方法がありそうです。
「編集」→「プラグイン」から以下のプラグインを有効にします。
f:id:naoxgames:20220214040050p:plain
f:id:naoxgames:20220214035951p:plain

すると「GetEditorWorld」が使えるようになります。
f:id:naoxgames:20220214040113p:plain

先ほどエディターユーティリティウィジェットで作成したレベルを開く処理の続きで以下のように処理を追加します。
f:id:naoxgames:20220214040511p:plain

これにより、各レベルを開き、Actorを取得し、変更を加えることができました。


レベルを保存する

先ほどのレベルを開く方法があったFileHelpers.cppで「Save」とかで検索すると「SaveCurrentLevel」というそれっぽい関数を見つけました。
実際にメニューの「ファイル」→「現在のレベルを保存」を選択するとこの処理を通ることもわかります。

なので、これを使おう!
ということで、先ほどの処理に追加します。
f:id:naoxgames:20220214041133p:plain
そのままノードでいけるので、これでOKです。


実行!!!!

実行!!!!
すると、各レベルを開き、アクターを取得して編集し、すべてのアクター編集後にレベルを保存、という流れができました。
動画やスクショで示すものがありませんが、お試しいただけるとわかります。

今回は「ActorHiddenInGame」というパラメーターを対象にしましたが、これによってとある値を判定して、それに応じたパラメーターを別の変数に与える(変換する)ようなことにも使えます。
また、今回はすべてのレベルを開いてますが、名前やフォルダとかである程度判別できるならそうしたほうがよいかと思います。なんだかんだ全レベル開くのはプロジェクトによっては大変かと。。。
あとは保存処理、今回のは変更対象のアクターがいなくても保存する感じになってるので、不要ならスキップしましょう。
という感じかなと。


思ったこと

これがあればレベルの中を特定の条件で一括変更できると思われるので、非常に有用かと。
今回は「一括変更」をメインにしましたが、チェック機能としても利用できるかと思われます。
とはいえ対象のレベルをすべて開くので、場合によってはめちゃくちゃ重いと思われるので、実際実用的なのかはその場合次第・・・・?



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