UE5 GameplayEffectの適用に関するメモ

# 概要
UnrealEngine5 のGameplayEffectについて適用時の細かい挙動のメモ書きです。
GameplayEffectの詳しい内容は参考サイトへ。


## 更新履歴
| 日付 | 内容 |
|:-----|:----|
|2024/06/20| 初版 |
|2024/09/04| AbilitySystemGlobals について修正 |
|2024/10/11| スタックについて追記 |
|2024/10/16| 削除時コールバックについて追記 |
|2024/10/22| RemoveActiveEffectsWithTagsメソッドについて追記 |
|2025/01/23| AbilityGlobalsの注意事項を追記/モディファイア演算について追記 |
|2025/04/23| GameplayEffectAggregator について追記 |


## 参考
以下の記事を参考にいたしました、ありがとうございます。
[UE公式：GameplayEffects](https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/GameplayEffects?application_version=5.3)
[GASDodument](https://github.com/sentyaanko/GASDocumentation/blob/lang-ja/README.jp.md)
[[UE4]GameplayAbilitySystemで「スタミナ」を作る](https://historia.co.jp/archives/17941/)
[GameplayEffect で継続ダメージ処理をやってみる。](https://massa8080.hatenablog.com/entry/2019/07/07/143840)

## 関連記事
[GameplayEffectComponentについてのメモ](https://qiita.com/unknown_ds/items/9504833e3ee8d418f8d5)

## 環境
Windows10
Visual Studio 2022
UnrealEngine 5.3.2, 5.5.1


## 関連ソース
"Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\GameplayEffect.h"
"Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\GameplayEffectTypes.h"


# GameplayEffectについて
プラグイン `GameplayAbilitySystem` の機能で `Attributes` として設定される値を変化させるシステムです。バフ/デバフなどの適用などに使用することができます。
仕組みが複雑ですので全部を理解するのは大変です。


## Period時の適用時の初回実行の設定
即時適用以外の `Infinite`、`Has Duration`時に`Execute Peridic Effect on Application` のチェックボックスでGE適用時の実行の有無を設定できます。
初回時(タイマーが0秒時）に適用するかどうかで周回数が変わってくることに注意が必要です。

![ExecutePeridicEffect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/01dcf521-4cea-0bd1-5138-de7bace0ab4b.png)
上記設定例だとチェックを入れると6回実行されます。（チェックを外すと5回）


## Periodタイマーのリセット
`Periodic Inhibiton Policy`の設定が `Never Reset` だとタイマーは一切リセットされないので、 `Reset Period` に設定し何らかでGE適用が阻害された場合にリセットされる。
![PeriodSetting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/9fc513d5-3998-07c3-2cb6-fbc7c780802c.png)

GEの阻害方法としては `Target Tag Requirements Gameplay Effect Component` を設定し`Ongoing Tag Requirements` の `Must Not Have Tags`に停止条件となるタグを設定するなどがあります。
![Example01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/566db484-f710-0d27-3183-dabf553ee277.png)


## クールダウンを単独で動かす
クールダウンが設定されている`GameplayAbility`は通常、 `CommitAbility` でクールダウン処理が開始されますが、単独で動かすこともできます。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/7ef034bb-eb0d-b01f-c649-2d925e7c9637.png">

外部から呼ぶこともできます。特定条件下でクールダウンを再計算したいときなどにつかえるかもしれません。以下、C++コード例。

```cpp:.cpp
// アクターで以下のようにアビリティを持っている
// UPROPERTY(BlueprintReadOnly, EditAnywhere)
// TArray<TSubclassOf<class UGameplayAbility>> Abilities;

// 0番目のアビリティに対し、クールダウンを強制実行
auto _ASC = GetAbilitySystemComponent();
auto _Spec = _ASC->FindAbilitySpecFromClass(Abilities[0]);
if (_Spec) {
	auto _Ability = Cast<UPlayerGameplayAbility>(_Spec->GetPrimaryInstance());
	_Ability->K2_CommitAbilityCooldown(false, true);
}
```


## C++で動的にゲームプレイエフェクトを適用
`Instant` な C++コードで動的にGEを作成して適用することができるようです。
`Instant` 以外の`HasDuration` 等でも作成はできますがレプリケーション時に問題がでるようです（シングルプレイで試したところ特に問題はでませんでした）。
以下サンプルコード。

```cpp:.cpp
auto _ASC = GetAbilitySystemComponent();

// アトリビュート MoveSpeed を変更する GEを作成して適用する
UGameplayEffect* _GETest = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("DynamicGETest")));
_GETest->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 _Idx = _GETest->Modifiers.Num();
_GETest->Modifiers.SetNum(_Idx + 1);

FGameplayModifierInfo& _Info = _GETest->Modifiers[_Idx];
_Info.ModifierMagnitude = FScalableFloat(1000.0f);
_Info.ModifierOp = EGameplayModOp::Additive;
_Info.Attribute = UMyAttributeSet::GetMoveSpeedAttribute();	// 変更するアトリビュート
_ASC->ApplyGameplayEffectToSelf(_GETest, 1.0f, _ASC->MakeEffectContext());
```


## GE適用時にアビリティを起動させる
GEコンポーネントの `Abilities Gameplay Effect Component`を設定し、`Ability`に発動させたいアビリティクラスを指定する。
起動時にアビリティクラス側の `OnAvaterSet`クラスが呼ばれるのでそこでアクティベート用にコードを書く必要があります。以下コード例。

```cpp:.h
UCLASS()
class SAMPLE_API UPGA_Sample : public UPlayerGameplayAbility
{
	GENERATED_BODY()

//..省略..

// アバター設定or切り替え時の処理
virtual void OnAvatarSet(const FGameplayAbilityActorInfo* _ActorInfo, const FGameplayAbilitySpec& _Spec) override;

};
```

```cpp:.cpp
void UPGA_Sample::OnAvatarSet(const FGameplayAbilityActorInfo* _ActorInfo, const FGameplayAbilitySpec& _Spec)
{
	Super::OnAvatarSet(_ActorInfo, _Spec);

	// GEからのパッシブ発動
	if (_Spec.GameplayEffectHandle.WasSuccessfullyApplied()) {
		_ActorInfo->AbilitySystemComponent->TryActivateAbility(_Spec.Handle, false);
	}
}

```
:::note warn
`OnAvatarSet`はアバター設定or切り替え時に呼ばれます、なのでゲーム開始時にも呼ばれることに注意。
:::


## スタック
ゲームプレイエフェクトは適用された数をそのままスタックすることができます。

![GE_Stacking.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/f04b661a-cab9-6b73-c4e8-2fbfadd85215.png)

スタック後の動作をどのようにするかは以下の項目で設定を行うことができます。

| 項目 | 説明 |
|:-----|:----|
|StackingType| ソース毎にスタックするか否か|
|Stack Duration Refresh Policy| スタック中のGEの持続時間の処理方法|
|Stack Period Reset Policy| スタック中のGEの期間の処理方法 |
|Stack Expiration Policy| Durationの場合、期限切れになるGEスタックの処理方法 |

適用は通常と同様にできます。削除時は減らしたいスタック数を第2引数に入れてスタック数を減らすことができます。
以下コード例。

```C++:
// 適用する（スタックを１つ増やす）
TSubclassOf<UGameplayEffect> _GE = /* 適用するStack設定されたGameplayEffect */;
FGameplayEffectSpecHandle _SpecHandle = _ASC->MakeOutgoingSpec(_GE, 1.0f, _ASC->MakeEffectContext());

if( _SpecHandle.IsValid() ){
	Handle = _ASC->ApplyGameplayEffectSpecToSelf(*_SpecHandle.Data.Get());
}

// スタックを1つ減らす（スタックがなくなったら削除）
if(Handle.IsValid()){
	_ASC->RemoveActiveGameplayEffect(Handle, 1);
}

// スタック数を取得する
int32 _StackCount = _ASC->GetCurrentStackCount(Handle);
```


## モディファイアの演算について（Modifier Op）
UE5.5あたりからモディファイアの演算命令が増えました。

![ModifierOp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/52bd1c56-5e23-03aa-2586-e591608fcae8.png)

```cpp:モディファイア計算式
((BaseValue + AddBase) * MultiplyAdditive / DivideAdditive * MultiplyCompound) + AddFinal
```
上記の計算に従うようです。
`Add (Base)` と `Multiply (Compound)`  と `Add (Final)` を使うことが多いかと思います。
`Multiply (Additive)` と `Divide (Additive)` は乗数/除数を加算してから計算するという特殊な処理なので複数のモディファイア計算が入ると意図しない結果になりやすいです。



## GameplayEffectContextの拡張
FGameplayEffectContext を継承して独自データを追加する
AbilitySystemGlobals を継承して定義したゲームプレイエフェクトコンテキストを返すメソッドを用意する

### 独自のGameplayEffectContextを定義する
`FGameplayEffectContext` を継承して追加データを定義します。
以下サンプルコード。

```cpp:MyGameplayEffectTypes.h
USTRUCT()
struct SAMPLE_API FMyGameplayEffectContext : public FGameplayEffectContext
{
	GENERATED_USTRUCT_BODY()

public:
	// ターゲットデータを取得する
	virtual FGameplayAbilityTargetDataHandle GetTargetData()
	{
		return(TargetData);
	}
	
	// ターゲットデータを追加する
	virtual void AddTargetData(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
	{
		TargetData.Append(TargetDataHandle);
	}
		
	//------------------------------------------------------------------------
	// FGameplayEffectContext のサブクラスがオーバーライドする必要がある関数
	//------------------------------------------------------------------------
	virtual UScriptStruct* GetScriptStruct() const override
	{
		return( FAppGameplayEffectContext::StaticStruct() );
	}

	virtual FAppGameplayEffectContext* Duplicate() const override
	{
		FAppGameplayEffectContext* NewContext = new FAppGameplayEffectContext();
		*NewContext = *this;
		NewContext->AddActors(Actors);
		if (GetHitResult())
		{
			NewContext->AddHitResult(*GetHitResult(), true);
		}
		
		NewContext->TargetData.Append(TargetData);
		
		return(NewContext);
	}

	virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;

// 追加データ
protected:
	// ターゲットデータ
	FGameplayAbilityTargetDataHandle TargetData;
};


template<>
struct TStructOpsTypeTraits<FAppGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FAppGameplayEffectContext>
{
	enum
	{
		WithNetSerializer = true,
		WithCopy = true
	};
};
```

## 個別のゲームプレイエフェクト削除時のコールバック登録
`AbilitySystemComponent` の `OnGameplayEffectRemoved_InfoDelegate` に登録することでできます。以下コード例。

```C++
void MyEffect::ApplayEffect()
{
	auto _ASC = GetAppAbilitySystemComponent();

..省略..
 
	// ゲームプレイエフェクトの適用
	FActiveGameplayEffectHandle Handle = _ASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());

	// エフェクト削除時のデリゲート登録
	_ASC->OnGameplayEffectRemoved_InfoDelegate(Handle)->AddUObject(this, &ThisClass::OnEffectRemoved);
}

void MyEffect::OnEffectRemoved(const FGameplayEffectRemovalInfo& _RemovalInfo)
{
	if (_RemovalInfo.bPrematureRemoval){
		UE_LOG(LogTemp, Log, TEXT("エフェクトは途中で削除されました"));
	}
	else{
		UE_LOG(LogTemp, Log, TEXT("エフェクトが正常に終了しました"));
	}
}

```

:::note 
個別のゲームプレイエフェクトではなく全てのゲームプレイエフェクト削除を対象にする場合は、`OnAnyGameplayEffectRemovedDelegate()` で実現できます。
:::


## 特定タグがついたゲームプレイエフェクトをまとめて削除する
`AbilitySystemComponent`クラスの `RemoveActiveEffectsWithTags` を使うと指定タグがついたゲームプレイエフェクトをまとめて削除できます。
その際にゲームプレイエフェクト側の[Asset Tags Gameplay Effect Component]の[AssetTags]に指定タグをつける必要があります。
以下サンプルコード

```cpp:.cpp
// 指定タグがついたゲームプレイエフェクトを削除する
FGameplayTagContainer	_EffectTagsToRemove;
_EffectTagsToRemove.AddTag( FGameplayTag::RequestGameplayTag(FName("Effect.RemoveOnDeath")) );
int32 _NumEffectsRemoved = AbilitySystemComponent->RemoveActiveEffectsWithTags(_EffectTagsToRemove);

UE_LOG(LogTemp, Log, TEXT("%d 個削除された。"), _NumEffectsRemoved);
```

対象のゲームプレイエフェクトのAssetTagsにタグが設定されている必要があります。
![AssetTags.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/9984d83d-cb44-9081-819b-6a14058af1c0.png)

似たようなメソッドが他にもあります。`RemoveActiveEffectsWithSourceTags`, `RemoveActiveEffectsWithAppliedTags`, `RemoveActiveEffectsWithGrantedTags` これらは細かく適用条件が違うようです


# AbilitySystemGlobals

## 独自のAbilitySystemGlobalsを定義する
`UAbilitySystemGlobals` を継承することと、`DefaultGame.ini`の設定が必要です。
以下サンプルコード。

```cpp:MyAbilitySystemGlobals.h
UCLASS()
class SAMPLE_API UMyAbilitySystemGlobals : public UAbilitySystemGlobals
{
	GENERATED_BODY()
	
public:
	UMyAbilitySystemGlobals(){}

	// 独自定義のアビリティシステムグローバルクラスを取得
	static UMyAbilitySystemGlobals& AppGet()
	{
		return dynamic_cast<UMyAbilitySystemGlobals&>(Get());
	}

	// 独自定義のゲームプレイエフェクトコンテキストを返す
	virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};
```

```cpp:MyAbilitySystemGlobals.cpp
// 独自定義のゲームプレイエフェクトコンテキストを返す
FGameplayEffectContext* UMyAbilitySystemGlobals::AllocGameplayEffectContext() const
{
	// プロジェクト固有の GameplayEffectContext 構造体を割り当てる必要があります。
	// 呼び出し元には割り当て解除の責任があります
	return( new FMyGameplayEffectContext() );
}

```

```config:DefaultGame.ini
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/MyGame.MyAbilitySystemGlobals"
```

:::note alert
MyAbilitySystemGlobals に UObjectのコンストラクタでアクセスは絶対しないでください。GameplayCueなどを使用していた場合パッケージ版でガーベージコレクトが呼ばれたタイミングでクラッシュします。
:::


## コスト無視/クールダウン無視
`UAbilitySystemGlobals`クラスに用意されている `bIgnoreAbilitySystemCosts` `bIgnoreAbilitySystemCooldowns` を使うとコスト無視/クールダウン無視ができます、主にデバッグ用機能と思われます。
このフラグの変更はコンソールコマンドを使って行うことができます。

```text:コンソールコマンド
AbilitySystem.IgnoreCosts 1   // コスト無視
AbilitySystem.IgnoreCosts 0   // コスト適用

AbilitySystem.IgnoreCooldowns 1    // クールダウン無視
AbilitySystem.IgnoreCooldowns 0    // クールダウン適用
```

C++から行う場合は `ConsoleCommand` でコマンドを実行することができます。
以下コード例。

```cpp:.cpp
GetController()->ConsoleCommand("AbilitySystem.IgnoreCosts 1");
```

コンソールマネージャを使って変更することもできます。
以下コード例。

```cpp:.cpp
	IConsoleVariable* _CVarIgnoreCosts = IConsoleManager::Get().FindConsoleVariable(TEXT("AbilitySystem.IgnoreCosts"));
	if (_CVarIgnoreCosts)
	{
		_CVarIgnoreCosts->Set(1);  // コストを無視する
	}

```


# AttributeSet
## GameplayEffectAggregator について
Aggregator を使うと同一のアトリビュートを変更する GameplayEffectの効果を一定の条件で制限することができます。
`UAttributeSet`を継承したクラスで `OnAttributeAggregatorCreated` を継承して設定します。以下サンプルコード。

```C++:MyAttributeSet.cpp
// Aggregatorが作られた際に呼ばれる処理
void UMyAttributeSet::OnAttributeAggregatorCreated(const FGameplayAttribute& _Attribute, FAggregator* _NewAggregator) const
{
	Super::OnAttributeAggregatorCreated(_Attribute, _NewAggregator);
	
	if (!_NewAggregator){ return; }

    // MoveSpeed アトリビュートに対して設定をする
	if(_Attribute == GetMoveSpeedAttribute()){
	_NewAggregator->EvaluationMetaData = &FAppAggregatorEvaluateMetaDataLibrary::TestMod;
	}
}
```

Aggregatorの評価関数を用意します、以下はマイナス値のうち一番低い値を適用する例です。

```C++:MyGameplayEffectAggregatorLibrary.h
#pragma once

#include "GameplayEffectAggregator.h"
// Modidier評価関数用
struct SAMPLE_API FMyAggregatorEvaluateMetaDataLibrary
{
	static FAggregatorEvaluateMetaData TestMod;
};
```

```C++:MyGameplayEffectAggregatorLibrary.cpp
void QualifierFunc_MostNegativeMod_AllPositiveMods0(const FAggregatorEvaluateParameters& _EvalParameters, const FAggregator* _Aggregator)
{
	// マイナス値のうち一番低い値だけ適用、プラス値は全て適用する
	const FAggregatorMod* _MostNegativeMod = nullptr;
	float _CurrentMostNegativeDelta = 0.f;
	float _BaseValue = _Aggregator->GetBaseValue();

	_Aggregator->ForEachMod( [&](const FAggregatorModInfo& _ModInfo)
	{
		if( !_ModInfo.Mod->Qualifies() ){
			return;
		}

		float _ExpectedDelta = 0.f;
		switch( _ModInfo.Op )
		{
		case EGameplayModOp::AddBase:
		case EGameplayModOp::AddFinal:
			_ExpectedDelta = _ModInfo.Mod->EvaluatedMagnitude;
			break;
		case EGameplayModOp::MultiplyAdditive:
		case EGameplayModOp::MultiplyCompound:
			_ExpectedDelta = (_BaseValue * _ModInfo.Mod->EvaluatedMagnitude) - _BaseValue;
			break;
		case EGameplayModOp::DivideAdditive:
			_ExpectedDelta = _ModInfo.Mod->EvaluatedMagnitude > 0.f ? ((_BaseValue / _ModInfo.Mod->EvaluatedMagnitude) - _BaseValue) : 0.f;
			break;
		case EGameplayModOp::Override:
			_ExpectedDelta = _ModInfo.Mod->EvaluatedMagnitude - _BaseValue;
			break;
		};

		if(_ExpectedDelta < 0.f){
			// マイナス値は一旦全て無効にする
			_ModInfo.Mod->SetExplicitQualifies(false);
			
			// マイナス値のうち一番低いものを調べる
			if (_ExpectedDelta < _CurrentMostNegativeDelta){
				_CurrentMostNegativeDelta = _ExpectedDelta;
				_MostNegativeMod = _ModInfo.Mod;
			}
		}
	});
	// 一番低いマイナス値を有効にする
	if (_MostNegativeMod)
	{
		_MostNegativeMod->SetExplicitQualifies(true);
	}
}

// 上記の関数を使うための設定
FAggregatorEvaluateMetaData FAppAggregatorEvaluateMetaDataLibrary::TestMod(QualifierFunc_MostNegativeMod_AllPositiveMods0);
```


# まとめ
GameplayEffectアセットのわかりやすい管理方法を模索しています。