UE5 GameFeaturesプラグインについてのメモ

# 概要
UnrealEngine5にて追加されたGameFeaturesプラグインのメモ書きです。
このプラグインは本編との参照なしで機能の追加/削除ができる仕組みとのことです。
サードパーソンプロジェクトに追加するテストをします。


## 更新履歴
| 日付 | 内容 |
|:-----|:----|
|2023/03/02| 初版 |
|2023/03/03| Action追加テストを追記 |


## 参考
以下の記事を参考にいたしました、ありがとうございます。
[UE公式：Game Features と Modular Gameplay](https://docs.unrealengine.com/5.1/ja/game-features-and-modular-gameplay-in-unreal-engine/)
[UE公式：UGameFeaturesSubsystem](https://docs.unrealengine.com/5.1/en-US/API/Plugins/GameFeatures/UGameFeaturesSubsystem/)
[UE5 Game Featuresで簡単に依存関係なしのコンポーネントを作ってみる](https://unrealengine.hatenablog.com/entry/2021/11/28/111821)
[[UE5] プレイヤーの技などをプラグインで実装できる！　GameFeaturesプラグインの紹介](https://historia.co.jp/archives/21145/)
[First Look at Unreal Engine 5 Game Features Plugin](https://denismakesgames.medium.com/first-look-at-unreal-engine-5-game-features-plugin-35fdc1e14f01)


## 環境
Windows10
Visual Studio 2022
UnrealEngine 5.1.1


## 関連ソース
__▼GameFeaturesSubsystem__
"\Engine\Plugins\Experimental\GameFeatures\Source\GameFeatures\Public\GameFeaturesSubsystem.h"

__▼GameFrameworkComponentManager__
"\Engine\Plugins\Experimental\ModularGameplay\Source\ModularGameplay\Public\Components\GameFrameworkComponentManager.h"


# 準備
[編集]->[プラグイン]から`GameFeatures`と`ModularGameplay`を有効にします。（エディタ再起動）
![gamefeature_plugin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/94f7ab43-5bfb-7788-161f-3777ee8d157f.png)

その後、__PrimaryAssetTypesToScanにエントリを追加しますか？__ とメッセージがでるのでそのままクリックすると必要な設定が自動で行われます。


# GameFeaturesプラグインの作成
サードパーソンプロジェクトにプラグインを追加するテストをします。

## 追加するプラグインの準備
[編集]->[プラグイン]のウィンドウ左上の[+追加]から新しいプラグインを追加します。

![add_plugin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/38a2e97b-94f9-2473-003a-60f7303c232e.png)

`ゲーム機能（コンテンツのみ）` を選択します（プラグインにてC++を使用する場合は `ゲーム機能（C++版）` を選択します）。

`TestFeature1`という名前で作成した場合、以下のようにコンテンツブラウザに表示されます。設定用のデータアセットが自動追加されます。
![add_plugin02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/cbb6e06b-87b7-3b81-48a2-4adaed307a71.png)

もし表示されない場合はコンテンツブラウザ右上の設定から[プラグインコンテンツを表示]にチェックを入れます。
![setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/857da9c5-fd96-3e77-552a-1218631f650c.png)


## 追加したプラグインに追加するコンポーネントを作成
追加するコンポーネントをテスト作成します。
以下は、Tick処理にデバッグ文字列表示をしただけのコンポーネント(TestComponent)です。
![TestComponent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/b5cdeb58-a0ef-3eec-3b61-7479d5d5b182.png)


## 追加される側のアクターの修正
追加されるアクターのBP（BP_ThirdPersonCharacter）にて、BeginPlay時に `GameFrameworkComponentManager`クラスの`AddReceiver`で追加コンポーネント受け取りの設定をします。
同時にEndPlay時には `RemoveReceiver`にて受け取り設定を削除します。
![BP_ThirdPersonCharacter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/7d78497a-f114-cfae-9fe6-13a4fc22b6be.png)


## プラグインのデータアセットの設定
プラグイン作成時に自動作成されたデータアセットの設定をします。
[Actions]を[Add Components]に設定し、[Component List]を追加します。
[Actor Class]には追加される側のアクタークラス[BP_ThirdPersonCharacter]、[Component Class]には追加するプラグインにあるコンポーネント[TestComponent]を設定します。

![DataAsset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/9a9a0dbf-925d-2593-89a1-ccdf8984d334.png)


# 動作テスト
テスト作成したコンポーネントの切り替えのテストをします。

## データアセットでの状態切り替え
データアセットの[現在の状態]を[有効]にすることでプラグイン追加されコンポーネントが追加されます。
これを[ロード済み]か[登録済み]にするとプラグインにて追加されたコンポーネントが削除されます。
[インストール済み]にするとプラグイン自体が無効となります。

![Result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/b6e9a147-21d8-ed8c-218a-4cefb3a81480.png)


## コンソールコマンドでの状態切り替え
コンソールコマンドでも切り替えが可能です。

```console コンソールコマンド
[インストール済み]に変更
UnLoadGameFeaturePlugin [PluginName]

[ロード済み]に変更
DeactivateGameFeaturePlugin [PluginName]

[有効]に変更
LoadGameFeaturePlugin [PluginName]

[登録済み]に変更
UnloadAndKeepRegisteredGameFeaturePlugin [PluginName]
```

### その他のコンソールコマンド
**ListGameFeaturePlugins**
GameFeatureプラグインの現在の状態をアウトプットログにリスト出力します。

**ReleaseGameFeaturePlugin [PluginName]**
GameFeatureプラグインを解放します。UnLoadGameFeaturePluginとの違いは設定のデータアセットは残るらしいです。実行するとプラグインの状態は[インストール済み]になります。

**CancelGameFeaturePlugin [PluginName]**
GameFeatureプラグインの状態変更のキャンセルを試みます。


## C++ での状態切り替え
C++での状態変更は `UGameFeaturesSubsystem` にアクセスをして行います。
準備として *.Build.csに`GameFeatures`モジュールを追加します。

```cs:MyProject.Build.cs
public class MyProject : ModuleRules
{
	public MyProject(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] {
			"Core",
			"CoreUObject",
			"Engine",
			"InputCore",
			"GameFeatures"      // ←追加
		});

...省略...

```

`GetPluginURLByName` でプラグインのURLを取得してから対応メソッドで切り替えを行います。
以下C++での状態変更サンプルコード。

```cpp:状態変更サンプルコード
#include "GameFeaturesSubsystem.h"

// プラグインURLを取得
FString _PluginURL;
UGameFeaturesSubsystem::Get().GetPluginURLByName(TEXT("TestFeature1"), _PluginURL);

// 指定プラグインをロードしてアクティブにする（[有効]にする）
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(_PluginURL, FGameFeaturePluginLoadComplete());

// 指定プラグインをデアクティブにする（[ロード済み]にする）
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(_PluginURL, FGameFeaturePluginLoadComplete());

// 指定プラグインをアンロードする（第2引数trueで[登録済み]/falseで[インストール済み]にする）
UGameFeaturesSubsystem::Get().UnloadGameFeaturePlugin(_PluginURL, true);

```

- - -
プラグインの状態の判断ができるメソッドもあります。
`IsGameFeaturePluginInstalled` `IsGameFeaturePluginRegistered` `IsGameFeaturePluginLoaded` はプラグイン状態" __以降__ "の判定になることに注意が必要です。

:::note info
例えば、現在が[有効]状態なら[インストール済み][登録済み][ロード済み]全てで true が返ってきます。
:::

```cpp:状態判断サンプルコード
#include "GameFeaturesSubsystem.h"

// プラグインURLを取得
FString _PluginURL;
UGameFeaturesSubsystem::Get().GetPluginURLByName(TEXT("TestFeature1"), _PluginURL);

// プラグインがインストール状態か（またはそれ以降か）を判断する
bool _Ret1 = UGameFeaturesSubsystem::Get().IsGameFeaturePluginInstalled(_PluginURL);
// プラグインが登録済み状態か（またはそれ以降か）を判断する
bool _Ret2 = UGameFeaturesSubsystem::Get().IsGameFeaturePluginRegistered(_PluginURL);
// プラグインがロード済み状態か（またはそれ以降か）を判断する
bool _Ret3 = UGameFeaturesSubsystem::Get().IsGameFeaturePluginLoaded(_PluginURL);
// プラグインが有効状態かを判断する
bool _Ret4 = UGameFeaturesSubsystem::Get().IsGameFeaturePluginActive(_PluginURL);

```

細かい状態を取得するには `GetPluginState` で取得ができるようです。定義は
"Engine\Plugins\Experimental\GameFeatures\Source\GameFeatures\Public\GameFeatureTypes.h"
にあります。

```cpp:GetPluginStateのサンプルコード
// プラグインのステートマシン状態を取得する
EGameFeaturePluginState _State = UGameFeaturesSubsystem::Get().GetPluginState(_PluginURL);

FString _StateStr = UE::GameFeatures::ToString(_State);
UE_LOG(LogTemp, Log, TEXT("%s"), *_StateStr);
```

# Actionの追加テスト
`UGameFeatureAction`を継承してアクションを追加することによって拡張ができるようです。
テストとしてアクターを生成するアクションを作成してみます。

## Actionクラスの作成
UE5サンプルのLyraGameにある
`LyraStarterGame\Source\LyraGame\GameFeatures\GameFeatureAction_WorldActionBase.h .cpp` を基底クラスに利用します。

```cpp:GameFeatureAction_WorldActionBase.h
#pragma once

#include "Containers/Map.h"
#include "GameFeatureAction.h"
#include "GameFeaturesSubsystem.h"
#include "Misc/AssertionMacros.h"
#include "Misc/CoreMiscDefines.h"
#include "UObject/UObjectGlobals.h"

#include "GameFeatureAction_WorldActionBase.generated.h"

class FDelegateHandle;
class UGameInstance;
class UObject;
struct FGameFeatureActivatingContext;
struct FGameFeatureDeactivatingContext;
struct FGameFeatureStateChangeContext;
struct FWorldContext;

/**
 * Base class for GameFeatureActions that wish to do something world specific.
 */
UCLASS(Abstract)
class UGameFeatureAction_WorldActionBase : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~ Begin UGameFeatureAction interface
	virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~ End UGameFeatureAction interface

private:
	void HandleGameInstanceStart(UGameInstance* GameInstance, FGameFeatureStateChangeContext ChangeContext);

	/** Override with the action-specific logic */
	virtual void AddToWorld(const FWorldContext& WorldContext, const FGameFeatureStateChangeContext& ChangeContext) PURE_VIRTUAL(UGameFeatureAction_WorldActionBase::AddToWorld,);

private:
	TMap<FGameFeatureStateChangeContext, FDelegateHandle> GameInstanceStartHandles;
};
```

```cpp:GameFeatureAction_WorldActionBase.cpp
#include "GameFeatureAction_WorldActionBase.h"

#include "Containers/Array.h"
#include "Containers/IndirectArray.h"
#include "Delegates/Delegate.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFeaturesSubsystem.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_WorldActionBase)

void UGameFeatureAction_WorldActionBase::OnGameFeatureActivating(FGameFeatureActivatingContext& Context)
{
	GameInstanceStartHandles.FindOrAdd(Context) = FWorldDelegates::OnStartGameInstance.AddUObject(this, 
		&UGameFeatureAction_WorldActionBase::HandleGameInstanceStart, FGameFeatureStateChangeContext(Context));

	// Add to any worlds with associated game instances that have already been initialized
	for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
	{
		if (Context.ShouldApplyToWorldContext(WorldContext))
		{
			AddToWorld(WorldContext, Context);
		}
	}
}

void UGameFeatureAction_WorldActionBase::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	FDelegateHandle* FoundHandle = GameInstanceStartHandles.Find(Context);
	if (ensure(FoundHandle))
	{
		FWorldDelegates::OnStartGameInstance.Remove(*FoundHandle);
	}
	
}

void UGameFeatureAction_WorldActionBase::HandleGameInstanceStart(UGameInstance* GameInstance, FGameFeatureStateChangeContext ChangeContext)
{
	if (FWorldContext* WorldContext = GameInstance->GetWorldContext())
	{
		if (ChangeContext.ShouldApplyToWorldContext(*WorldContext))
		{
			AddToWorld(*WorldContext, ChangeContext);
		}
	}
}
```

`GameFeatureAction_WorldActionBase`を継承してアクターを生成するアクションクラス`UGameFeatureAction_ActorTest`を作成します。


```cpp:GameFeatureAction_ActorTest.h
#pragma once

#include <CoreMinimal.h>
#include "./GameFeatureAction_WorldActionBase.h"
#include "GameFeatureAction_ActorTest.generated.h"



USTRUCT(BlueprintType, Category = "GameFeatureAction_ActorTest | ActorSetting")
struct FGFA_ActorSetting
{
	GENERATED_BODY()

public:
	// アクタークラス
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting")
	TSoftClassPtr<AActor> ActorClass;

	// 生成位置
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting")
	FTransform Transform;
};


// GameFeatureActionテスト
UCLASS(meta = (DisplayName = "Add Actor (Test)"))
class MYPROJECT_API UGameFeatureAction_ActorTest final : public UGameFeatureAction_WorldActionBase
{
	GENERATED_BODY()

public:
	// 設定
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting")
	FGFA_ActorSetting ActorSetting;


	// 始末
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext &Context) override;

	// ワールドに出す
	virtual void AddToWorld(const FWorldContext& WorldContext, const FGameFeatureStateChangeContext& ChangeContext) override;


private:
	TWeakObjectPtr<AActor> SpawnedActor;

};
```



```cpp:GameFeatureAction_ActorTest.cpp
#include "GameFeatureAction_ActorTest.h"
#include <Components/GameFrameworkComponentManager.h>
#include "Kismet/GameplayStatics.h"


#ifdef UE_INLINE_GENERATED_CPP_BY_NAME
#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_ActorTest)
#endif

// 始末
void UGameFeatureAction_ActorTest::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext &Context)
{
	Super::OnGameFeatureDeactivating(Context);
	
	if(SpawnedActor.IsValid()){
		SpawnedActor->Destroy();
	}
	
	SpawnedActor = nullptr;
}

// ワールドに出す
void UGameFeatureAction_ActorTest::AddToWorld(const FWorldContext &WorldContext, const FGameFeatureStateChangeContext& ChangeContext)
{
	UWorld*				_World = WorldContext.World();
	UGameInstance*		_GameInstance = WorldContext.OwningGameInstance;

	if( (_GameInstance == nullptr)||(_World == nullptr) || (!_World->IsGameWorld()) ){
		return;
	}
	
	const auto& [_ActorClass, _Transform] = ActorSetting;
	if( _ActorClass.IsNull()){
		return;
	}
	
	// ロード
	TSubclassOf<AActor> _SpawnActorClass = _ActorClass.LoadSynchronous();
	// スポーン
	SpawnedActor = _World->SpawnActor<AActor>(_SpawnActorClass, _Transform);
}

```

## テストアクターとデータ設定
新たにプラグイン `FeatureTest2`を作成し、テスト用アクター `TestActor`を作成します。
![Feature2_TestActor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/8504c95d-1018-a51f-a5d7-069063a16648.png)

データアセットは以下のように Action に `Add Actor (Test)`が増えているのでこれを指定、ActorClass には 上記で作成したテストアクターを指定します。
![Feature2_TestFeature2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/9b3e8513-00c7-f151-e100-325cc0a8424e.png)

## 実行結果
結果として以下のようにキューブのアクターが追加されます。プラグインを`有効`以外にするとアクターは始末されます。
![Result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/5a9f0334-a9a5-bf1f-45b5-4600cf11974a.png)

:::note warn
アクター生成時に同期ロードをしているので実用には改造が必要だと思われます。
:::



# まとめ
機能実装のアクションは `GameFeatureAction` クラスで拡張可能なようで、UI Widget や GameplayAbility なども追加可能なようにできるようです。