目次
※この記事にサンプルプロジェクトはありません。
プラグイン等に関しては記事内のリンクからダウンロードしてください。
→Github:kinnajichan
この記事は、Unreal Engine Meetup Connect – Vol.2 – UETipsLT編にて講演した、「UE5版 EditorUtilityWidgetについてのあれこれ」のスライドの内容を記事にしたものです。
スライドだと検索性が低くなるため、記事としても投稿いたします。
EUWについての記事
※これらの記事の内容をある程度読むことでより理解が深まるかと思います。
•公式ドキュメント:
エディタ ユーティリティ ウィジェット
•わかりやすく説明してる記事:
UE5:Editor Utilityを活用したツール制作術 第1回(CGWorld:とんこつ様&キンアジより)
•基礎は大体書いてある(古いけど):
【UE4】Editor Utility Widgetについてのあれこれ(アンナプルナ様より)
•EUWに関する記事一覧:
Unreal Engine Editor Utility Widget | エディタ拡張(UE5攻略リンク様より)
•過去に公開したEUWTips:
EditorUtilityWidgetPetitDeepdive(Docswell:キンアジより)
UE5のEUWアップデートTips
ObjectMixer(UE5.2~)
UE5.2から、EUWにてObjectMixer
というWidgetが使えるようになりました。
独自のクラスフィルタリングやアクターが持つプロパティをObjectMixer
Widget上に直接表示させるようなカスタマイズされたOutlinerを独自に作成することができます。
※UE5.2だと意図通りに動かない機能があるため、UE5.3~使うのがオススメです。
詳しい説明:【UE5】BPだけで独自のOutlinerを作っちゃおう!【★★☆~★★★☆】(キンアジより)ObjectMixer
を使用したプラグイン:KAJiraUtility(Github:キンアジより)
EUWの複数起動(UE5.2~)
UE5.1までは、EUWのウィンドウは同じクラスで複数立ち上げることをC++を用いなければできませんでしたが、UE5.2からはUEditorUtilitySubsystem::SpawnAndRegisterTabWithID
にて複数立ち上げることができます。
ちなみに、UE5.2から追加されたEUWのEditorにあるRunUtilityWidget
ボタンと右クリメニューのRunEditorUtilityWidget
&SpawnAndRegisterTab
ノードは別枠(Asset名から自動的に作られるID)なので、IDを指定した起動方法でなくても2つまでは同じクラスのWindowを起動できます。
変数表示の自動化(UE5.2~)
UE5.2から、ProjectSettings
→Engine
→UserInterface
にあるAuthorizeAutomaticWidgetVariableCreation
というプロパティが追加されています。
UE5.1までは、Button
などのWidgetは新しくDesignerウィンドウ上で追加した場合に自動的にIsVariable
のチェックが有効になっていましたが、UE5.2からはAuthorizeAutomaticWidgetVariableCreation
を有効にしないと自動的にチェックが入らないようになっています。
※通常のWidgetBlueprintと共通の設定です。
EUWのデバッグ(UE5.2~)
UE5.2から、EUWがプレイ中でなくてもブレークポイントによるブループリントのデバッグができるようになりました。これに伴い、以下2つのプロパティがEditorUtilityWidgetBlueprint
に追加されています。
参考:[UE5] UE5.2以降、EditorUtilityWidgetがエディタ実行中に動作しなくなった時に確認すべきこと ( Is Enabled in PIE プロパティ )
(おかず様より)
IsEnabledinPIE
IsEnabledinPIE
を有効にすることで、PIE中でもEUWが動作するようになります。(デフォルトで無効)
※UE5.1まではデフォルトでPIEでも使用できたので、バージョンアップに伴いPIEで使用ができなくなった場合はこの設定を確認してください。
IsEnabledinDebugging
ブレークポイントで動作が停止している場合にデフォルトだとEUWは動作しなくなります。
→IsEnabledinDebugging
を有効にすることで、ブレークポイントで動作が停止している場合でもEUWが動作するようになります。
※プレイ中、非プレイ中のどちらにも適用されます。
EUW用のWidget(UE5.3~)
UE5.3から、EUW用にEditorUtilityButtonやEditorUtilityEditableText等の汎用的なWidgetクラスが用意されました。
機能としてのアップデートではなく、Runtime用のWidgetクラスと分けることで、UE5.2まではRuntime用のWidgetクラスにEUW用のStyle(見た目)が記載されていたのがなくなり内部的な実装の切り分けが行いやすいような設計になりました。
TemplateSelector(UE5.3~)
UE5.2までは、デフォルトで新規にEUWを作成する際はRootとなるパネルは必ずCanvasPanel
となっていましたが(WidgetBlueprintと共通の設定が使われていた)UE5.3からデフォルトでRootとなるWidgetを選択できるTemplateSelector
がEditorUtilityWidgetBlueprint
用にも実装されています。
なお、TemplateSelectorで表示されるWidgetClassはProjectSettings
→Editor
→EditorUtilityWidgets(Team)
→WidgetClassesToHide
による表示しないWidgetClassの影響を受けません。
特に問題があるわけではないのですが、意図せずRuntime用のWidgetを使用してしまう可能性がある点に注意しましょう。
Templateを選ばせずにUE5.2以前と同じようにする方法
→以下のようなコードをProject等のStartupModule()
に記載し、UseWidgetTemplateSelector
をFalse
にします。
//追加記述が必要なコードのみ記載
//※build.csのPrivateDependencyModuleNamesに"Blutility"を追加が必要
#include "EditorUtilityWidgetProjectSettings.h"
#include "PropertyEditorModule.h"
void FKAMyProjectModule::StartupModule()
{
FPropertyEditorModule& PropertyModule =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomClassLayout(
UEditorUtilityWidgetProjectSettings::StaticClass()->GetFName());
}
WidgetBlueprintからの変換(UE5.3~)
UE5.3から、通常のWidgetBlueprint
をEditorUtilityWidgetBlueprint
に変換することができるようになりました。(WidgetBlueprint
を右クリック→AssetActions
→ConverttoEditorUtilityWidget
から変換可能)
ちなみに、このようなコードをプロジェクトのソース等に関数として書いて実行すれば、EditorUtilityWidgetBlueprint
から通常のWidgetBlueprint
へ逆変換できます。(UEditorUtilityLibrary::ConvertToEditorUtilityWidget
を少し改良しただけ)
//必要なコードのみ記載
//※build.csのPrivateDependencyModuleNamesに"Blutility"を追加が必要
#include "EditorUtilityWidgetProjectSettings.h"
#include "PropertyEditorModule.h"
void UKAEditorUtilityBlueprintLibrary::ConvertToEUWBPToWBP(UEditorUtilityWidgetBlueprint* EUWBP)
{
if (!EUWBP || !EUWBP->IsA<UEditorUtilityWidgetBlueprint>()){
return;
}
FName BPName = EUWBP->GetFName();
UObject* Outer = EUWBP->GetOuter();
EObjectFlags Flags = EUWBP->GetFlags();
TArray<struct FEditedDocumentInfo> OriginalEditedDocuments = EUWBP->LastEditedDocuments;
EUWBP->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors | REN_SkipGeneratedClasses | REN_ForceNoResetLoaders);
TArray<UObject*> Children;
GetObjectsWithOuter(EUWBP, Children, false);
UWidgetBlueprint* WBP = NewObject<UWidgetBlueprint>(Outer, BPName, Flags);
if (WBP->WidgetTree)
{
WBP->WidgetTree->Rename(nullptr, GetTransientPackage(), REN_ForceNoResetLoaders | REN_DontCreateRedirectors);
WBP->WidgetTree = EUWBP->WidgetTree;
}
for (UObject* Child : Children)
{
Child->Rename(nullptr, WBP, REN_DontCreateRedirectors | REN_SkipGeneratedClasses | REN_ForceNoResetLoaders);
}
UEngine::FCopyPropertiesForUnrelatedObjectsParams Params;
Params.bPerformDuplication = true;
Params.bNotifyObjectReplacement = false;
Params.bPreserveRootComponent = false;
UEngine::CopyPropertiesForUnrelatedObjects(EUWBP, WBP);
WBP->LastEditedDocuments = OriginalEditedDocuments;
WBP->GeneratedClass->ClassGeneratedBy = WBP;
check(WBP->GeneratedClass == nullptr || WBP->GeneratedClass->GetOuter() == WBP->GetOuter());
TMap<UObject*, UObject*> OldToNew;
OldToNew.Add(EUWBP, WBP);
TArray<UObject*> Targets = { EUWBP, GetTransientPackage() };
FArchiveReplaceObjectRef<UObject> ReplaceReferencesInRoot(WBP, OldToNew);
FFindReferencersArchive Archive(WBP, Targets);
check(Archive.GetReferenceCount(EUWBP) == 0);
Children.Reset();
GetObjectsWithOuter(WBP->GetOuter(), Children);
for (UObject* Child : Children)
{
FArchiveReplaceObjectRef<UObject> ReplaceReferences(Child, OldToNew);
FFindReferencersArchive Archive2(Child, Targets);
check(Archive2.GetReferenceCount(EUWBP) == 0);
}
}
Assetのサムネ表示用Widget(UE5.3~)
UE5.3から、アセットのサムネイルをEUW上で表示できるAssetThumbnailWidgetが追加されました。
使用例:UE5:Editor Utilityを活用したツール制作術 第2回:アセットのプロパティを設定・取得するツール(CGWorld:とんこつ様より)
ThumbnailLabel
の色はHintColorAndOpacity
で設定されており、デフォルトでOpacity
が0になっていて見えないので、Asset名等を表示したい場合はHintColorAndOpacity
の設定を見直してみてください。
その他EUWTips
簡単に実装できるキーイベント
EUWで機能するショートカットキーのようなものを、FUICommandInfo
等のC++実装をする必要なく簡単に実装できます。
//※一度EUW内の何かしらのWidgetをクリックしないと有効化されない。
//自動的に有効にしたい場合は、EventConstruct等で以下のようなコードをBPに公開し実行することで可能。
//※build.csのPrivateDependencyModuleNamesに"Slate","SlateCore","Blutility"を追加が必要
#include "Framework/Application/SlateApplication.h"
#include "EditorUtilityWidget.h"void UKAEditorUtilityBlueprintLibrary::SetEUWFocus(UEditorUtilityWidget* FocusWidget)
{
if (FocusWidget)
{FSlateApplication::Get().SetAllUserFocus(FocusWidget->TakeWidget(), EFocusCause::SetDirectly);}
}
各種設定の保存
EUWを使用していると、DetailViews
等で使用したプロパティを保存したくなる場合が多いですが、
プレイ中でなくてもSaveGameObject
は使用できるため、エディタ用のデータを保存する際に有効です。
または、変数のConfigVariable
を有効にして、SavedのEditor.ini
に保存するのも手っ取り早いです。
//※Configの場合はEUWインスタンス等の保存するインスタンスに対してSaveConfigをする必要がある↓はSaveConfigをBPに公開するためのコード例
void UKAEditorUtilityBlueprintLibrary::SaveConfig(UObject* InObject)
{
if (InObject) { InObject->SaveConfig(); }
}
不必要に編集されるPersistentLevel
EUWが起動する事によって生成されるEUWインスタンスは「Editor上で開いているWorld」に所属します(これによって、GetAllActorsOfClass
等でエディタ上のアクター等へのアクセスが容易にできるようになっています)
→詳しくは【UE4】EditorUtility上のWorldContextについて【★★☆】(キンアジより)
通常の起動方法で起動したEUWのインスタンスはTransient
(一時的)フラグが自動的に付与されるため、内部の変数をDetailViews
等で変更しても他に影響を与えませんが、EUWをOuterとして生成したObjectやCreateWidget
等で生成したサブウィジェットとしてのEUW等には明示的にTransient
フラグをつけないと、そのオブジェクトに変更が加わった場合に
PersistentLevelが変更された扱いになってしまいます。
//UEditorUtilityWidgetBlueprint::CreateUtilityWidgetから抜粋(EUWのOuter(親)はEditor上のWorld)
if (UWorld* World = GEditor->GetEditorWorldContext().World())
{
CreatedUMGWidget = CreateWidget<UEditorUtilityWidget>(World, WidgetClass);
…
解決方法として、WidgetやObject等のインスタンスを生成する場合は、他に影響を与えないTransient
なObjectをOuterにするか、Transient
フラグを追加するようにしましょう。
→TransientなOuterにする場合、ブループリントではConstructObjectFromClass
ノードでOuterにEditorUtilitySubsystem
あたりを接続しておくと無難です。
→インスタンスをTransient
にするにはC++定義のクラスであればUCLASS
の指定子
にTransient
を追加すればよいですが、Blueprintクラスの場合はSetFlags
関数を
ブループリントに公開し実行することで、Transient
なフラグを設定してあげると解決できます。
//Transient指定子
UCLASS(Transient)
class UEditorUtilityTestObject : public UEditorUtilityObject
//SetFlagsでTransientフラグの追加(BP関数への公開例)
void UEditorUtilityTestLibrary::AddTransientFlag(UObject* InObject)
{
if (InObject)
{
InObject->SetFlags(RF_Transient);
}
}
レベルを開くと再生成されるEUW
ULevelEditorSubsystem::LoadLevel
関数等で別レベルを開くようなツールをEUWで作成することがある場合、前ページで説明した所属ワールドの都合によりレベルが開かれたタイミングで新しいEUWのインスタンスが生成され、開いているEUWのウィンドウで表示されているものは自動的に置き換えられてしまうので注意が必要です。
また、レベル移動前の古いEUWインスタンスは、どのWorldにも所属しなくなるため、WorldContext
を使用したLatentノード(例えばDelayノード)のCallbackが返ってこなくなります。レベルを開いた後はWorldContext
に依存しないノードで処理をする等の工夫が必要です。
コンテンツブラウザで実行されるPreConstructイベント
通常のUserWidgetでもそうですが、EventPreConstruct
ノードはWidgetインスタンスを生成する前からDesignerウィンドウ上等でも実行されるため、非常に便利なノードとなっています。
しかし、 EventPreConstruct
ノードはコンテンツブラウザ上でWidgetClassにマウスカーソルをあわせたタイミングでも毎フレーム実行されてまいます。
また、 EventPreConstruct
で値のセットやレベルの移動等を行ってしまうと意図せず実行されてしまうので、そのような処理はEventConstruct
ノードに書くようにしましょう。
//CustomWidgetMeny.h//ModuleはToolMenus,Blutility,UMG,Slate,SlateCoreを追加
#pragma once
#include "CoreMinimal.h"
#include "EditorUtilityObject.h"
#include "EditorUtilityWidget.h"
#include "CustomWidgetMeny.generated.h"
UCLASS()
class KAEDITORUTILITYPLUGIN_API UCustomWidgetMeny : public UEditorUtilityObject
{
GENERATED_BODY()
public:
UPROPERTY()
TSubclassOf<UEditorUtilityWidget> WidgetClass;
//Widget用MenuEntryをToolMenusに追加
UFUNCTION(BlueprintCallable, Category = "KAEditorUtility")
void AddEUWMenyEntry(FName InOwner, FName InName, TSubclassOf<UEditorUtilityWidget> InWidgetClass);
TSharedRef<SWidget> GetCustomMenuWidget(
const FToolMenuContext& ToolMenuContext,
const FToolMenuCustomWidgetContext& CustomWidgetContext,
FName MenuName);
};
//CustomWidgetMeny.cpp
#include "CustomWidgetMeny.h"
#include "Subsystems/UnrealEditorSubsystem.h"
void UCustomWidgetMeny::AddEUWMenyEntry(
FName InOwner,
FName InName,
TSubclassOf<UEditorUtilityWidget> InWidgetClass)
{
UToolMenus* ToolMenus = UToolMenus::Get();
FToolMenuEntry Entry(InOwner, InName, EMultiBlockType::MenuEntry);
UToolMenu* OwnerMenu = ToolMenus->FindMenu(InOwner);
Entry.MakeCustomWidget.BindUObject(this, &UCustomWidgetMeny::GetCustomMenuWidget, InName);
Entry.Type = EMultiBlockType::Widget;
Entry.WidgetData.bNoIndent = true;
Entry.WidgetData.bNoPadding = true;
WidgetClass = InWidgetClass;
OwnerMenu->AddMenuEntry(NAME_None, Entry);
ToolMenus->RefreshAllWidgets();
}
TSharedRef<SWidget> UCustomWidgetMeny::GetCustomMenuWidget(
const FToolMenuContext& ToolMenuContext,
const FToolMenuCustomWidgetContext& CustomWidgetContext,
FName MenuName)
{
if (!WidgetClass)
{
return SNew(STextBlock).Text(FText::FromString("Invalid Menu Class"));
}
UUserWidget* MakeWidget = CreateWidget<UUserWidget>(
GEditor->GetEditorSubsystem<UUnrealEditorSubsystem>()->GetEditorWorld(), WidgetClass);
if (!IsValid(MakeWidget))
{
return SNew(STextBlock).Text(FText::FromString("Menu Widget Creation Failed"));
}
return MakeWidget->TakeWidget();
}
メニューに直接EUWを追加する
エディター上部のToolbarや右クリックメニューに対してEUWを直接表示することができます。
※EUWである必要はないですが、Editor拡張用としてEUWが一番適しているため。
簡単にエディター上にメニューを登録するプラグイン(試作版)を公開しておりますので、よければ御覧ください。
(プラグイン内にEUWのメニュー化機能もあり)
→KAEditorUtilityPlugin(Github:キンアジより)
EUWを閉じる際の確認処理
EUWのタブを閉じる際に、閉じるかどうかを制御する汎用的な仕組みの実装を行うことができます。
たとえば「とじますか?」のようなポップアップメッセージを出して、キャンセルできるような実装を行いたいときにとても便利です。
※以下のソースはコアな実装しか記載していないため、これだけでは再現できません(複数クラスにまたがったりして収まらないので)
詳しくはKAEditorUtilityPlugin(Github:キンアジより)にプラグイン化されていますので御覧ください。
bool UKA_EditorUtilityAssistSubsystem::BindCanCloseEUWTab(
class UEditorUtilityWidgetBlueprint* EUWBP,
FName TabID,
FOnGetCanCloseEUWTab CanCloseTabDelegate)
{
FName GeneratedTabID = GenerateEUWTabID(EUWBP, TabID);
if (TSharedPtr<SDockTab> DockTab = GetEUWDockTab(EUWBP, TabID))
{
UKA_EUWTabManagerProxy* TabManagerProxy = nullptr;
if (TabManagerProxyList.Contains(GeneratedTabID))
{
TabManagerProxy = TabManagerProxyList[GeneratedTabID];
}
if(!TabManagerProxy)
{
TabManagerProxy = NewObject<UKA_EUWTabManagerProxy>(this);
TabManagerProxyList.Add(GeneratedTabID, TabManagerProxy);
}
if (FAssetRegistryModule* AssetRegistryModule =
FModuleManager::GetModulePtr<FAssetRegistryModule>(TEXT("AssetRegistry")))
{
AssetRegistryModule->Get().OnAssetRemoved().AddUObject(
TabManagerProxy,
&UKA_EUWTabManagerProxy::OnAssetRemoved);
}
TabManagerProxy->GetCanCloseEUWTabDelegate = CanCloseTabDelegate;
TabManagerProxy->TabID = GeneratedTabID;
TabManagerProxy->EUWBPPath = FSoftObjectPath(EUWBP);
DockTab.Get()->SetCanCloseTab(
SDockTab::FCanCloseTab::CreateUObject(TabManagerProxy,
&UKA_EUWTabManagerProxy::GetCanCloseScriptEditorTab));
return true;
}
return false;
}
以上です!