【UE5】GameplayAttribute を BP のみで使う

### 追記

[UE Tokyo .dev #3](https://ue-community-tokyo.connpass.com/event/318594/) に本記事と同じテーマで登壇しました。
その際に使用したスライドでは、本記事で紹介するプラグインを含めた4つの方法についてまとめているので、こちらもぜひ参考にしてください。

https://www.docswell.com/s/suramaru517/58G78Y-gameplay-attribute-bp

# 1. はじめに

UE5では、GameplayAbilitySystem の機能が Blueprint のみでも使えるようになってきました。
しかし、GameplayAttribute やその利用が前提の GameplayEffect に関しては、現状 Blueprint のみで使うことはできません。
（[GASCompanion](https://www.unrealengine.com/marketplace/ja/product/gas-companion) や [九里江めいくさんのプラグイン](https://youtu.be/NzuQdkJX9dI) のように、C++ を書かずに GameplayAttribute を定義する方法はありますが、C++ を使わずに定義する方法は現状ないはずです。）

本記事では、そんな我々 BP 信者に優しくない GameplayAttribute を、無理やり BP のみで定義する方法を紹介します。

::: note warn
本記事は半分**ネタ**です。実用性はそんなにありません。
:::

# 2. 方法

## 2.1. プラグインをダウンロードする

以下のリンクより GameplayAttributeForBP プラグインをダウンロードして、プロジェクトの Plugins フォルダ（なければ作る）にぶっこんでください。

https://github.com/suramaru517/GameplayAttributeForBP.git

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/689a9676-e870-f6c8-f993-551399bb9f65.png)

::: note warn
本プラグインは **UE 5.1** で作成しました。パッケージにおける動作は未検証です。
:::

## 2.2. AttributeSet に変数を追加する

プラグインをビルドしてエディタを起動したら、`Plugins/GameplayAttributeForBP Content/AttributeSets/BP_GABPAttributeSet`を開き、任意の GameplayAttributeData 型の変数を追加します。

サンプルの変数として "Health" が最初からありますが、削除しても構いません。
`Plugins/GameplayAttributeForBP Content/AttributeSets`以下に AttributeSet を継承した Blueprint Class を新規作成し、その中に変数を追加してもよいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/6ca99d82-dc92-db1e-17ec-003d5ed3bfb5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/cb1e7822-ba61-3620-a802-1b2b070178d7.png)

::: note info
Plugins フォルダが表示されない場合は、コンテンツブラウザ右上の設定から「**Show Plugin Content**」にチェックを入れてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/6b8002e3-7d5d-88d0-09b9-066dc5ff3c6c.png)
:::

## 2.3. GABP ボタンをオンにする

適当なアセットエディタを開き、ツールバーの左の方にある "GABP" と書かれたボタンをクリックしてオンにします。
GABP ボタンがオンの時は、2.2. で追加した GameplayAttributeData が Attribute の一覧に表示されるようになります。

::: note alert
GABP ボタンは、Attribute の一覧を使う時のみオンにしてください。
GABP ボタンをオンにしたままにすると、**高確率でエディタがクラッシュします**（理由は後述）。
:::

* GABP ボタンが**オフ**の時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/0995b971-8abc-bed4-f930-31b2d590c6ea.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/95e72440-cc8d-fb12-b918-5ab26c9757db.png)

* GABP ボタンが**オン**の時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/f203d250-2385-a8b9-c984-4bd42a831f17.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/4c9e95c4-0c42-558f-f64b-d739034c59a9.png)

## 2.4. 実装する

例として、以下の画像の GameplayEffect を使い、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/ebb512c8-4d70-1c87-104c-1af184848e69.png)

以下の画像のBPを実行してみると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/966b3334-34b5-0ff9-cf2d-df6a465d44c1.png)

GameplayAttribute が正しく機能していることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239989/33ce3e8f-3351-1cdc-5e7e-4019e7a67975.png)

# 3. 解説

## 3.1. 問題点と解決策

BP のみで GameplayAttribute が使えない理由として、BP の AttributeSet に定義した GameplayAttributeData が Attribute の一覧に表示されないという問題がありました。

そこで、エンジンソースにある Attribute の一覧の表示に関する処理を見てみます。

```SGameplayAttributeWidget.cpp
TSharedPtr<FAttributeViewerNode> SAttributeListWidget::UpdatePropertyOptions()
{
	...

	// Gather all UAttribute classes
	for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
	{
		UClass *Class = *ClassIt;
		if (Class->IsChildOf(UAttributeSet::StaticClass()) && !Class->ClassGeneratedBy)
		{
            ...

			for (TFieldIterator<FProperty> PropertyIt(Class, EFieldIteratorFlags::ExcludeSuper); PropertyIt; ++PropertyIt)
			{
                ...

                TSharedPtr<FAttributeViewerNode> SelectableProperty = MakeShareable(new FAttributeViewerNode(Property, FString::Printf(TEXT("%s.%s"), *Class->GetName(), *Property->GetName())));
				PropertyOptions.Add(SelectableProperty);
			}
		}
    ...
```

ここでは、`TObjectIterator` を用いてロード済みの BP と C++ の全 `UClass` の中から、`UAttributeSet` の子クラスかつ C++ クラス (`ClassGeneratedBy == nullptr`) である `UClass` を収集し、その `UClass` のプロパティを Attribute の一覧に追加しています。

つまり、Attribute の一覧に BP の AttributeSet に定義した GameplayAttributeData を表示させるには、その AttributeSet の BP をロードし、C++ クラスに偽装 (`ClassGeneratedBy = nullptr;`) すればよいということです。

## 3.2. プラグインの実装

2.1. で紹介した GameplayAttributeForBP プラグインでは、GABP ボタンが押された時に `FARFilter` を用いて特定のパス下にある BP をロードし、その BP の中から `UAttributeSet` の子クラスを収集しています。

収集した `UAttributeSet` の子 BP クラスは、GABP ボタンがオンになった時に C++ クラスに偽装し、GABP ボタンがオフになった時に偽装を解除します。

```GABPToolBarButton.cpp
// When GABP button is pressed
void UGABPToolBarButton::ToggleActivation()
{
	// Set ARFilter
	FARFilter ARFilter;
	ARFilter.PackagePaths.Add(FName(TEXT("/GameplayAttributeForBP/AttributeSets")));
	ARFilter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
	ARFilter.bRecursivePaths = true;
	ARFilter.bRecursiveClasses = true;

	// Load Blueprints
	TArray<FAssetData> Assets;
	IAssetRegistry::Get()->GetAssets(ARFilter, Assets);

	TArray<TSubclassOf<UObject>> AttributeSetClasses;

	// Collect child Blueprint classes of UAttributeSet
	for (const FAssetData& Asset : Assets)
	{
		const UBlueprint* Blueprint = Cast<UBlueprint>(Asset.GetAsset());
		const TSubclassOf<UObject> Class = Blueprint->GeneratedClass;

		if (Class && Class->IsChildOf<UAttributeSet>())
		{
			AttributeSetClasses.Emplace(Class);
		}
	}

	bIsActivated ? Deactivate(AttributeSetClasses) : Activate(AttributeSetClasses);
}

// When GABP button is turned on
void UGABPToolBarButton::Activate(const TArray<TSubclassOf<UObject>>& AttributeSetClasses)
{
	// Disguised as a C++ class
	for (const TSubclassOf<UObject>& AttributeSetClass : AttributeSetClasses)
	{
		ClassGeneratedBys.Emplace(AttributeSetClass, AttributeSetClass->ClassGeneratedBy);
		AttributeSetClass->ClassGeneratedBy = nullptr;
	}

	bIsActivated = true;
	Data.Icon = ActiveIcon;
}

// When GABP button is turned off
void UGABPToolBarButton::Deactivate(const TArray<TSubclassOf<UObject>>& AttributeSetClasses)
{
	// Remove C++ class disguise
	for (const TSubclassOf<UObject>& AttributeSetClass : AttributeSetClasses)
	{
		UObject** ClassGeneratedBy = ClassGeneratedBys.Find(AttributeSetClass);

		if (*ClassGeneratedBy)
		{
			AttributeSetClass->ClassGeneratedBy = *ClassGeneratedBy;
		}
	}

	ClassGeneratedBys.Empty();

	bIsActivated = false;
	Data.Icon = InactiveIcon;
}
```

ただし、BP を C++ クラスに偽装すると、その間その BP は無効なクラスとして認識されてしまいます。
これは、クラスの生成の原因となった BP を指す `ClassGeneratedBy` に、偽装のために無理やり `nullptr` を代入したためです。

何らかのタイミングでこの無効なクラスのロードが発生すると、ぬるぽでエディタがクラッシュします。
GABP ボタンをオンのままにすると高確率でエディタがクラッシュするというのは、そういうことです。

::: note info
我々 BP 信者は、命懸けで GameplayAttribute を使わなければならないのです。
:::

# 4. おわりに

命を懸けなくてもいい方法があれば教えてください。

# 5. おまけ

`UEditorUtilityToolMenuEntry` を使うと、簡単に（BP のみでも）ツールバーボタンが作れるのでおすすめです。

また、プラグインでは `EditorPerProjectUserSettings.ini` を上書きできず、エディタ起動時に EditorUtilityBlueprint を実行するように登録する通常の方法が使えなかったため、C++ でゴリ押しています。
`GameplayAttributeForBP.cpp` に実装があるので、そちらもぜひ参考にしてみてください。