本稿では Unreal Engine 5 の C++ を使って複数の子 Editor Utility Widget(エディタユーティリティウィジェット、以下 EUW) を、ひとつの親 EUW にはめ込んで表示する方法について共有します。
EUW のマニアックな使い方に興味があるエンジニア向けです。
動作環境
Unreal Engine 5.4.3 および 5.4.4
Windows 11 Home
Visual Studio 2022
EUW に限らず、UI は親とは別に作った子のパーツを組み合わせて作る方が柔軟性が高くなります。
そうすることで、必要に応じて好きなタイミングで別のパーツに差し替えることができるようになるためです。
これをブループリントでやるのは簡単です。
ブループリントでは誰でも簡単にいじれてしまうので、C++ で作ろうとしたときに参考資料が見つからずに困りました。
上記画像の「Construct 子パーツ名」ノードに該当する関数が C++ 側にはありません。
細かい事は割愛しますが、あれこれ試行錯誤したところ、子の EUW を構築するときは、FSoftObjectPath などを使ってアセットとしてロードする必要があることが分かりました。
FSoftObjectPath pathA(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsA.ChildPartsA'"))); UObject* partsA_widget = pathA.TryLoad();
ネットだと、UGameplayStatics::SpawnObject() でいけるという話も出て来るのですが、うまく行きませんでした。
この関数では引数のチェックをした後に NewObject しているだけなので、NewObject で良さそうに思ったのですが、本稿のケースでは NewObject では対応できません。
親用の EUW を新規作成します。
CanvasPanel(親用)の下に、子を入れるための別のキャンバスパネル(子用)を追加します。
キャンバスパネルを使う理由は、GetLayout / SetLayout 関数を使って EUW のフォームサイズに合わせてレイアウト調整を行えるようにするためです。
子用の EUW を新規作成します。
本稿では3つの子を用意しています。
CanvasPanel の下にボーダーを追加し、子 EUW の表示範囲が分かりやすいよう色を着けています。
EUW に設定する親クラスを C++ で作成します。
Editor Utility Widget はエディタ用の機能なので、エディタモジュールにソースファイルを作成します。
プロジェクト名は EUWTest です。
エディタモジュール用の *.Build.cs に以下のモジュールを追加します。
private でも public でも、どっちでも良いです。
“UMG”
“UMGEditor”
“Blutility”
// Copyright dokuro.moe // EUWTestEditor.h #pragma once #include "CoreMinimal.h" #include "Editor/Blutility/Classes/EditorUtilityWidget.h" #include "Editor/UMGEditor/Public/WidgetBlueprint.h" #include "EUWTestEditor.generated.h" class UCanvasPanel; class UEditorUtilityWidgetBlueprint; // 子 A の EUW UCLASS(BlueprintType) class EUWTESTEDITOR_API UEUWChildPartsA : public UEditorUtilityWidget { GENERATED_BODY() }; // 子 B の EUW UCLASS(BlueprintType) class EUWTESTEDITOR_API UEUWChildPartsB : public UEditorUtilityWidget { GENERATED_BODY() }; // 子 C の EUW UCLASS(BlueprintType) class EUWTESTEDITOR_API UEUWChildPartsC : public UEditorUtilityWidget { GENERATED_BODY() }; // 親の EUW UCLASS(BlueprintType) class EUWTESTEDITOR_API UEUWParentWidget final : public UEditorUtilityWidget { GENERATED_BODY() public: virtual void NativeTick(const FGeometry& Geometry, float DeltaTime) override; private: bool IsPassedNativeTick() const { return this->bPassedNativTick; } bool ConstructChildParts(); void AdjustLayout(); // 親用キャンバスパネルを C++ 側で使えるようにバインド UPROPERTY(EditAnywhere, meta = (BindWidget)) UCanvasPanel* CanvasPanel{nullptr}; UPROPERTY() TArray<UEditorUtilityWidgetBlueprint*> ChildParts; bool bPassedNativTick{false}; FVector2f PrevSize; FVector2f PrevPosition; FVector2f Size; FVector2f Position; };
// Copyright dokuro.moe // EUWTestEditor.cpp #include "EUWTestEditor.h" #include "Components/CanvasPanel.h" #include "Components/CanvasPanelSlot.h" #include "Editor/Blutility/Classes/EditorUtilityWidgetBlueprint.h" void UEUWParentWidget::NativeTick(const FGeometry& Geometry, float DeltaTime) { this->PrevSize = this->Size; this->PrevPosition = this->Position; // 親ウィンドウの表示位置とサイズをキャッシュ this->Size = Geometry.GetLocalSize(); this->Position = Geometry.AbsolutePosition; if (this->PrevSize != this->Size) { // ウィンドウサイズが変わったら子を配置し直す this->AdjustLayout(); } if (!this->bPassedNativTick) { // 子を作成(一回だけ実行) this->ConstructChildParts(); } } bool UEUWParentWidget::ConstructChildParts() { if ( this->IsPassedNativeTick()) { return false; } if (!IsValid(this->CanvasPanel)) { return false; } // パスは適宜修正してください。 FSoftObjectPath pathA(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsA.ChildPartsA'"))); UObject* partsA_widget = pathA.TryLoad(); if (!IsValid(partsA_widget)) { return false; } FSoftObjectPath pathB(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsB.ChildPartsB'"))); UObject* partsB_widget = pathB.TryLoad(); if (!IsValid(partsB_widget)) { return false; } FSoftObjectPath pathC(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsC.ChildPartsC'"))); UObject* partsC_widget = pathC.TryLoad(); if (!IsValid(partsC_widget)) { return false; } this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsA_widget)); this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsB_widget)); this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsC_widget)); TArray<UWidget*> all_children = this->CanvasPanel->GetAllChildren(); if (all_children.Num() != this->ChildParts.Num()) { return false; } TArray<UCanvasPanel*> panels = { CastChecked<UCanvasPanel>(all_children[0]), CastChecked<UCanvasPanel>(all_children[1]), CastChecked<UCanvasPanel>(all_children[2]) }; for (int32 i = 0; i < panels.Num(); i++) { UEditorUtilityWidgetBlueprint* widget_bp = this->ChildParts[i]; widget_bp->CreateUtilityWidget(); UWidget * widget = widget_bp->GetCreatedWidget(); UCanvasPanel* panel = panels[i]; panel->AddChild(widget); } this->AdjustLayout(); this->bPassedNativTick = true; return true; } void UEUWParentWidget::AdjustLayout() { TArray<UWidget*> all_children = this->CanvasPanel->GetAllChildren(); if (all_children.Num() != this->ChildParts.Num()) { return; } TArray<UCanvasPanel*> panels = { CastChecked<UCanvasPanel>(all_children[0]), CastChecked<UCanvasPanel>(all_children[1]), CastChecked<UCanvasPanel>(all_children[2]) }; const TArray<FVector2D> positions = { FVector2D( 0.f, 0.f), FVector2D( 0.f, this->Size.Y * 0.4f + 4.f), FVector2D(this->Size.X * 0.5f + 2.f, this->Size.Y * 0.4f + 4.f) }; const TArray<FVector2D> sizes = { FVector2D(this->Size.X , this->Size.Y * 0.4f), FVector2D(this->Size.X * 0.5f - 2.f, this->Size.Y * 0.6f - 4.f), FVector2D(this->Size.X * 0.5f - 2.f, this->Size.Y * 0.6f - 4.f) }; for (int32 i = 0; i < panels.Num(); i++) { UWidget * widget = this->ChildParts[i]->GetCreatedWidget(); UCanvasPanel * panel = panels[i]; UCanvasPanelSlot* panel_slot = CastChecked<UCanvasPanelSlot>(panel->Slot); UCanvasPanelSlot* child_panel = CastChecked<UCanvasPanelSlot>(widget->Slot); // 子をはめ込む親のキャンバスパネル { FAnchorData layout = panel_slot->GetLayout(); layout.Offsets.Left = positions[i].X; layout.Offsets.Top = positions[i].Y; layout.Offsets.Right = positions[i].X + sizes[i].X; layout.Offsets.Bottom = positions[i].Y + sizes[i].Y; panel_slot->SetLayout(layout); } // 子のキャンバスパネル { FAnchorData layout = child_panel->GetLayout(); layout.Offsets.Left = 0.f; layout.Offsets.Top = 0.f; layout.Offsets.Right = sizes[i].X; layout.Offsets.Bottom = sizes[i].Y; child_panel->SetLayout(layout); } } }
コードの通りですが、子 EUW は親を表示してからロードして表示しています。
EUW を表示中に PartsC を PartsD と入れ替えたい…という要望が出て来ても、上記コードのように処理すれば実現できます。
ポインタの有効性チェックを飛ばしているところがあります。
if (pointer) とか if (IsValid(pointer)) とか
ゲームとして実行するコードや、クラッシュして作業データが失われる可能性のあるツール用のコードなら、もっと厳しくチェックするのですが、動作検証用のコードなのでいちいちチェックしません。
親と全ての子の親クラスを↑のコードで定義したクラスに変更します。