EditorUtility で生成した Widget のプロパティを設定する

こんにちは、葉乃音 (HANON) と申します。

普段は **Shiden** という Unreal Engine 5 用ノベルゲームエディタプラグインの開発をしています。([現在ベータ版を GitHub で公開中です。](https://github.com/HANON-games/Shiden))

https://x.com/HanonHeartKnows/status/1819994349071114684

今回はこのプラグインを作る過程で直面した小ネタを。

## 悩んだこと

Shiden にはインゲームやエディタ上で使用する**アセットをテンプレートから複製する機能**があります。

たとえばセーブ画面を自作したい時、こんな感じに選択をすると...
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/717de429-797d-2952-3224-aded8bfe6d40.png" width=70%>

こんな感じで必要なアセットがテンプレートから複製されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/fa574595-c52e-6a61-a101-7b9c7f0ae83e.png)

本プラグインにおけるセーブ画面はメインの `SaveMenu` クラスと各セーブスロットの `SaveSlot` クラスの2つからなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/b6280af3-2e18-31cf-9f46-664968db32af.png)

そして、**これらには `SaveMenu` クラスから `SaveSlot` クラスへの依存関係が存在します**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/0b9d41ca-8b8d-12d7-8304-023d9c88f32e.png)

さて、テンプレートから各々のアセットを複製するだけでは、これらの依存関係までは設定されないので、どうにか設定してあげる必要があります。

ですが、**どうやったら生成したばかりの Widget の設定を変更できるのでしょうか？** もちろんユーザに手動で設定してもらうのもナシではないですが、出来れば避けたい...。

ここで小一時間悩むことになりました...。

## 下準備

とりあえず参照関係の自動設定を実現するために、出来ることをやっていきましょう。

まず `SaveMenu` のテンプレート (これは単なる Widget の Blueprint です) を開き、 `SaveSlot` クラスを参照できるよう、任意の User Widget クラスを指定できる変数 `SaveSlotClass` を用意しました。

つまり `SaveMenu` のテンプレートを複製した際 (以降、複製したものを `MySaveMenu` クラスとします)、**この `SaveSlotClass` 変数に同時生成した `SaveSlot` クラス** (以降、`MySaveSlot` クラスとします) **を自動設定する**のがゴールになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/eae75634-ba79-6d58-f66c-7de228a3a737.png)

テンプレートから複製する処理と参照関係を自動設定する処理は EditorUtilityBlueprint に書いていきます。

テンプレートからの複製は `Duplicate Asset` ノードを使って簡単に行えます。ここで注意したいのは、**複製したオブジェクトそのものはあくまで BlueprintWidget であることです**[^1]。

今回やりたいのは `UBlueprintWidget` オブジェクトのプロパティの値変更ではなく、**`MySaveMenu` クラスの `SaveSlotClass` 変数のデフォルト値を変更すること**。そのため、`Load Blueprint Class` ノードの引数に複製したオブジェクトのパスを指定して、Blueprint に紐づいている `MySaveMenu` クラスを取ってくる必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/22b7b297-21ac-284d-c459-2f276568a082.png)

あとは取得した `MySaveMenu` クラスのデフォルト値を変更するだけです。しかしここからが厄介です。なぜなら、**Blueprint ではクラスのデフォルト値を変更することはできないから**です。

こんな感じの `Get Class Defaults` というデフォルト値を取得するノードはありますが、デフォルト値を変更するノードは Blueprint にはありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/b844c1fe-d4ab-0c37-44d9-7630ba4fde59.png)

したがって、ここからはコードの出番です。

## 解決策1: C++ を使用する

C++ を使用する場合はこんな感じのコードを書くことで実現できます。

TargetClass から DefaultObject を取得し、SetObjectPropertyValue() によって `PropertyName` 変数の値を変更しています。

```cpp
void SetClassProperty(const UClass* TargetClass, const FName PropertyName, UClass* PropertyValue)
{
	FProperty* Prop = TargetClass->FindPropertyByName(PropertyName);
	const FClassProperty* ClassProperty = CastField<FClassProperty>(Prop);
	void* ValuePtr = ClassProperty->ContainerPtrToValuePtr<void>(TargetClass->GetDefaultObject());
	ClassProperty->SetObjectPropertyValue(ValuePtr, Value);
}
```

EditorUtility のほうはこんな感じ。CreateAsset ノードは自作ですが、やっていることは既存ファイルとファイル名を被らないようにしつつテンプレートから複製しているだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/c4eeff97-3458-819b-23b5-0b195763047c.png)


## 解決策2: Python を使用する

`Execute Python Script` ノードを使用するのも一つの手です。
C++ のコードを書かなくて済むのでサクッと済ませたい場合はこちらが良いでしょう。

UE5 には Python Editor Script Plugin というプラグインがデフォルトで入っており、これを有効にすることで python を使用できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/03314c70-9529-4699-5d99-f02b9f143f93.png)

EditorUtility はこんな感じになります。`Execute Python Script` ノードは Detail から引数を設定することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929802/26f64e1f-12cd-ffaf-55b0-ab04d665c0c9.png)

スクリプトの中身はこんな感じです。

```python
import unreal

bp_save_menu_default_object = unreal.get_default_object(save_menu_class)
bp_save_menu_default_object.set_editor_property("SaveSlotClass", save_slot_class)
```

やっていることは `SaveMenu` クラスの default object を取得して、set_editor_property() で `SaveSlot` クラスをセットしているだけです。

## まとめ

Unreal Engine 5 のエディタ拡張は機能が豊富かつ優秀ですし、Blueprint だけでも大体どうにかなりますが、それだけで実現できない個所は、今回みたく C++ や python も積極的に使っていきたいですね！

明日は @T_Sumisaki さん、@tukigaselio さん、@Szr_Asakura さん、@uxtuno_ さんです！
どんな記事になるのか楽しみです！

[^1]: ちょっとややこしいのですが、エディタ上で目にする Widget のアセットは `UWidgetBlueprint` が特定の `UUserWidget` 継承クラスと関連づいた形で出来ています。