Unreal Engine/Gameplay Ability System

[UE] Gameplay Ability System -4- Gameplay Effects

KANTAM 2023. 5. 28. 22:03

×https://github.com/tranek/GASDocumentation#concepts-a-changes

 

GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's Gamepl...

github.com

단순히 위의 문서를 보고 제멋대로 해석해서 적은 것입니다. 차라리 위의 문서를 봐주세요. 저 영어 못 합니다.

4.5 Gameplay Effects

4.5.1 Gameplay Effect Definition

GameplayEffectsAbility가 자신 혹은 다른 액터의 AttributesGameplayTags를 변경하는 혈관 역할을 한다. GameplayEffectsAttribute에 데미지나 치유같은 즉각적인 변화나 버프와 디버프와 같은 지속 시간이 있는 변화를 일으킬 수 있다. UGameplayEffect 클래스는 하나의 게임플레이 이펙트를 정의하는 데이터 전용 클래스를 의미한다. GameplayEffects에 어떠한 추가적인 로직을 더해서는 안된다. 일반적으로 디자이너가 UGameplayEffect의 자식 클래스로 블루프린트를 많이 만든다.

GameplayEffectsModifiersExecutions을 통해 Attributes를 변경한다. (GameplayEffectExecutionCalculation)

GameplayEffects는 3가지의 duration 타입이 있다: Instant, Duration, Infinite

추가적으로, GameplayEffectsGameplayCues를 추가하고 실행할 수 있다. 하나의 Instant GameplayEffectGameplayCue, GameplayTags에 있는 Execute를 실행하며, 하나의 Duration 혹은 Infinite GameplayEffectGameplayCue, GameplayTags에 있는 Remove를 실행한다.

Duration Type GameplayCue Event When to use
Instant Execute Attribute의 BaseValue에 즉각적, 영구적인 변화를 줄 수 있다. GameplayTags는 한 프레임이라도 적용되지 않는다.
Duration Add & Remove Attribute의 CurrentValue에 일시적인 변화를 줄 수 있다. 또한 GameplayTags를 적용할 수 있는데 GameplayEffect가 제거되면 같이 제거된다. 적용 기간은 UGameplayEffect 클래스나 블루프린트에서 지정된다.
Infinite Add & Remove Attribute의 CurrentValue에 일시적인 변화를 줄 수 있다. 또한 GameplayTags를 적용할 수 있는데 GameplayEffect가 제거되면 같이 제거된다. 스스로 제거되지 않고 어빌리티나 ASC를 통해 수동적으로 제거된다.

DurationInfinite GameplayEffectPeriodic Effects라는 옵션을 적용할 수 있다. Periodic Effects는 해당 기간에 매 X초 마다 ModifiersExecutions를 적용할 수 있다. Periodic EffectAttributeBaseValue를 변경하거나 GameplayCues를 실행한다면 Instant GameplayEffects로 간주된다. 시간이 지날 때마다 데미지를 받는 효과를 구현할 때 유용하다.(독뎀) Note: Periodic Effetcts cannot be predicted.

Infinite GameplayEffects는 기간동안 Ongoing Tag Requirements와 매칭이 되느냐(유무)에 따라 잠시 꺼질 수도 켜질 수도 있다. GamepalyEffect를 끄는 것은 그 이펙트의 Modifiers와 적용된 GameplayTags를 제거하는 것이지만 GameplayEffect자체는 제거되는게 아니다. 다시 키면 ModifiersGameplay Tags가 적용된다.

만약 Duration 혹은 Infinite GameplayEffect에서 Modifiers를 수동적으로 다시 계산해야 한다면 (Attribute에서 제공되지 않는 MMC를 사용하는 경우, 파생되어 나와야 하는 경우), 이미 같은 레벨 사용하고 있는 UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.Level()을 통해 UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandleActiveHandle, Int32 NewLevel)을 사용할 수 있다. backing Attributes를 통해 얻은 Modifiersbacking Attributes가 업데이트 될경우 자동적으로 업데이트된다.

Modifiers를 업데이트 하기위한 SetActiveGameplayEffectLevel()의 키는 다음과 같다:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// Private function otherwise we'd call these three functions without needing to set the level to what it already is
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffects는 일반적으로 인스턴스화 되지 않는다. 어빌리티나 ASCGameplayEffect를 적용하고 싶을 때, GameplayEffectClassDefaultObjet를 통해 GameplayEffectSpec을 만든다. 성공적으로 적용된 GameplayEffectSpecsFActiveGameplayEffect라는 새로운 struct에 추가된다. 이 것은 ASC가 이펙트를 적용하는 이펙트들인 ActiveGameplayEffects라는 컨테이너이다.

 

4.5.2 Applying Gameplay Effects

GameplayEffectsGameplayAbilitiesASC의 함수로 부터 여러 방법으로 호출될 수 있으며 보통 ApplyGameplayEffectTo의 형태를 취한다. 다른 함수는 필수적으로 편리한 함수이다. 결과적으로 Target에 UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()를 호출한다. 

GameplayAbility밖에서 GameplayEffect를 적용하기 위해선, 예를 들어 발사체, Target의 ASC를 얻어야 하며 그 ASC의 ApplyGameplayEffectToSelf를 호출해야 한다. 

델리게이트를 통해 Duration이나 Infinite GameplayEffects가 언제 ASC에 적용되었는지 알 수 있다. 

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);

콜백 함수는 다음과 같다.

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);

replication mode에 관계없이 서버는 언제나 이 함수를 호출한다. Full과 Mixed replication 모드에서 autonomous proxy는 replicate된 GameplayEffect에 대해서만 이 기능을 호출한다. Simulated proxies는 Full replication 모드에서만 호출한다. 

 

4.5.3 Removing Gameplay Effects

GameplayEffect는 다양한 방법으로 GameplayAbilities의 함수와 ASC의 함수에서 제거될 수 있다. 보통은 RemoveActiveGameplayEffect의형태를 취한다. 타겟의 FActiveGaemplayEffectsContainer::RemoveActiveEffects()를 사용하는게 편리하다. 

GameplayAbility밖에서 GameplayEffect를 제거하기 위해선 타겟의 ASC를 얻어야 하며, 그 ASC의 RemoveActiveGameplayEffect 함수를 사용한다. 

델리게이트를 통해 Duration이나 Infinite GameplayEffects가 언제 ASC에서 제거되었는지 알 수 있다. 

AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

콜백 함수는 다음과 같다.

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

replication mode에 관계없이 서버는 언제나 이 함수를 호출한다. Full과 Mixed replication 모드에서 autonomous proxy는 replicate된 GameplayEffect에 대해서만 이 기능을 호출한다. Simulated proxies는 Full replication 모드에서만 호출한다. 

 

4.5.4 Gameplay Effect Modifiers

Modifiers는 Attribute를 변경하며 Attribute를 predictively하게 변경할 수 있는 유일한 방법이다. GameplayEffect는 0개 혹은 여러개의 modifiers를 가질 수 있다. 각각의 Modifier는 연산자를 통해 오직 하나의Attribute의 변경만 담당한다. 

Operation Description
Add Modifier에 지정된 Attribute에 더한다. 빼기는 음수를 사용
Multiply Modifier에 지정된 Attribute에 곱한다.
Divide Modifier에 지정된 Attribute에 나눈다.
Override Modifier에 지정된 Attribute에 덮어쓰기한다.

한 Attribute의 CurrentValue는 BaseValue에 추가된 모든 Modifier의 집계 결과이다. Modifier가 어떻게 계산되는지는 GameplayEffectAggregator.cpp의 FAggregatorModChannel::EvaluateWithBase에 있다. 

((InlineBaseValue + Additive) * Multiplicitive) / Division

Override Modifier는 마지막으로 적용된 modifier가 우선하여 최종값을 덮어쓴다.

Note: 백분율 기반 변경이라면 Multiply를 사용하여 더하기 후에 발생하도록 해야한다. 

Note: prediction은 백분율 변경에 문제가 있다. 

Modifier에는 가지 타입이 있다. Scalable Float, Attribute Based, Custom Calculation Class, 그리고 Set By Caller.연산자에 따라 Modifier의 지정된 Attribute를 변경하기위해 사용하는 float값이다. 

Modifier Type Description
Scalable Float Scalable Float는 변수를 행으로 하고 레벨을 열로하는 DataTable를 가리킬 수 있는 구조이다. Scalable Float는 자동적으로 어빌리티의 현재 레벨에 따라 지정한 테이블 행을 읽는다. (혹은 GameplayEffectSpec에서 지정한 다른 레벨) 이 값은 계수에 의해 추가적으로 조정될 수 있다. 만약 테이블/로우 데이터가 지정되어 있지 않다면 계수가 모든 레벨에서 단일 값의 하드코드로 사용될 수 있도록 값이 1로 지정된다. 
Attribute Based Attribute Based Modifier는 backing Attribute나 Source(GameplayEffectSpect을 만든 사람), Target(GameplayEffectSpec을 받는)으로 부터 받는 CurrentValue나 BaseValue이다. 혹은 선 후 계수에 따라 조정된다. SnapShotting은 GmaeplayEffectSpec이 생성될 때 backing Attribute가 캡쳐된다는 것을 의미하고 Snapshotting이 없다는 것은 GameplayEffectSpec이 적용될 때, backing Attribute가 캡쳐된다는 것을 의미한다.
Custom Calculatio Class Custom Calculation Class는 복잡한 Modifier를 위해 가장 유연한 방법을 제시한다. 이 Modifier는 modifierMagnitudeCalculation 클래스를 취하고 선 후 계수를 이용하여 최종 float 값에 조정을 할 수 있다.
Set By Caller Set By Caller는 어빌리티나 GameplayEffectSpec을 만든 녀석에 의해 런타임에 GameplayEffect밖에서 set될 수 있는 값이다. 예를 들어, 플레이어가 차지 공격을 할 경우 얼마나 오래 버튼을 누르고 있었느냐에 따라 데미지가 달라진다. SetByCaller는 본질적으로 GameplayEffectSpec에 있는 TMap<FGameplayTag, float>이다. Modifier는 계수가 제공된 GameplayTag와 관련된 SetByCaller 값을 찾으라고 한다. SetByCallers는 GameplayTag만을 사용하는 버전의 Modifier에서 사용될 수 있다. FName을 사용해서는 안된다. Modifier가 SetByCaller로 지정되어 있으면서 SetByCaller에 올바른 GameplayTag가 존재하지 않는다면 런타임 에러를 발생하며 0을 반환한다. 이러한 이슈는 Divide 연산자에서 일어날 수 있다. 

 

Scalable Float

 

4.5.4.1 Multiply and Divide Modifiers

기본적으로, 모든 곱하기와 나누기 Modifier는 Attribute의 BaseValue에 곱하지고 나눠지기 전에 함께 이루어진다. 

from GameplayEffectAggregator.cpp:

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
	float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
	float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
	...
	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
	...
}
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
	float Sum = Bias;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Sum += (Mod.EvaluatedMagnitude - Bias);
		}
	}

	return Sum;
}

곱하기와 나누기 모두 위의 Bias값이 1이다(더하기는 0). 그래서 다음과 같이 보인다. 

1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...

이 공식은 예상하지 못한 결과값을 준다. 먼저 이 공식은 BaseValue를 곱하거나 나누기 전에 모든 Modifier를 더한다. 대부분의 사람들이 그것들을 모두 곱하거나 나눈다고 생각할 것이다. 예를 들어 곱하기 modifer로 값은 1.5를 2개 가지고 있다면 대부분의 사람들은 BaseValue가 1.5 × 1.5 = 2.25로 곱해진다고 생각할 것이다. 하지만 이것은 BaseValue에 2를 곱하기 위해 1.5를 더한다(50% increase + another 50% increase = 100% increase). 예를 들어, 500의 베이스 스피드가 있다. 여기에 10%의 이동속도 버프를 추가한다면 값이 550이 되고 여기에 10% 버프를 추가한다면 600이 된다. 

둘째로, 이 공식은 파라곤을 염두에 두고 설계되었기 때문에 어떤 값을 사용할 수 있는지에 대한 문서화되지 않은 규칙을 가지고 있다. 
곱하기와 나누기의 곱셈 더하기 규칙:

  • (No more than one value <1) AND (Any number of value [1, 2))
  • OR (One value >= 2)

공식 안의 Bias는 기본적으로  [1, 2)의 범위의 digit of numbers를 뺀다. The first Modifier's Bias subtracts out from the starting Sum value (set to the Bias before the loop) which is why any value by itself works and why one value < 1 will work with the numbers in the range [1, 2).

Some examples with Multiply:
Multipliers: 0.5
1 + (0.5 - 1) = 0.5, correct

Multipliers: 0.5, 0.5
1 + (0.5 - 1) + (0.5 - 1) = 0, incorrect expected 1? Multiple values less than 1 don't make sense for adding multipliers. Paragon was designed to only use the greatest negative value for Multiply Modifiers so there would only ever be at most one value less than 1 multiplying into the BaseValue.

Multipliers: 1.1, 0.5
1 + (0.5 - 1) + (1.1 - 1) = 0.6, correct

Multipliers: 5, 5
1 + (5 - 1) + (5 - 1) = 9, incorrect expected 10. Will always be the sum of the Modifiers - number of Modifiers + 1.

Many games will want their Multiply and Divide Modifiers to multiply and divide together before applying to the BaseValue. To achieve this, you will need to change the engine code for FAggregatorModChannel::EvaluateWithBase().

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
	float Division = MultiplyMods(Mods[EGameplayModOp::Division], Parameters);
	...

	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
float FAggregatorModChannel::MultiplyMods(const TArray<FAggregatorMod>& InMods, const FAggregatorEvaluateParameters& Parameters)
{
	float Multiplier = 1.0f;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Multiplier *= Mod.EvaluatedMagnitude;
		}
	}

	return Multiplier;
}

 

4.5.4.2 Gameplay Tags On Modifiers

각 Modifier마다 SoucreTags와 TargetTags를 설정할 수 있다. 이 2개는 GameplayEffect의 Application Tag requirements와 똑같이 실행된다. 그러므로 effect가 적용되었을 때만 이 태그들도 처리된다. Periodic, infinite effect라고해도 계속해서 처리되는게 아닌 적용되었을 때 한번만 처리된다. 

Attribute Based Modifier는 또한 SourceTagFilter와 TargetTagFilter도 설정할 수 있다. Attribute Based Modfier의 소스인 Attribute의 크기를 결정할 때, 이러한 필터는 해당 Attribute에 대한 특정 modifier를 제외하는데 사용된다. 필터의 태그 중 일부가 포함되지 않은 소스 또는 대상인 Modifier는 제외된다. 다른 Attribute로 부터 파생되는 값인데 그 값을 필터링 한다는 뜻인듯 하다.

This means indetal: 소스 ASC 및 대상 ASC의 태그는 GameEffects에 의해 캡쳐된다. 소스 ASC의 태그는 GameplayEffectSpec이 만들어질 때 캡쳐되고, 타겟 ASC는 effect를 실행할 때 캡쳐된다. 결정할 때, infinite 혹은 duration effect의 Modifier가 적용될 자격이 있는지 그리고 그러한 필터가 있는지 캡쳐된 태그가 그러한 필터와 비교된다. 

 

4.5.5 Stacking Gameplay Effect

GameplayEffects는 기본적으로 GameplayEffectSpec의 인스턴스를 적용하는데 이전에 GameplayEffectSpec의 인스턴스가 실행중이고 존재하는지는 생각하지 않는다. GameplayEffects는 새로운 GameplayEffectSpec의 인스턴스로 추가되는 대신에 현재 존재하는 GameplayEffect의 스택 카운트를 변경하는 것으로 쌓일 수 있다. Stacking은 오직 duration과 infinite GameplayEffects에서만 가능하다. 

2가지 방법: Aggregate by Source and Aggregate by Target

Stacking Type Description
Aggreagete by Source 타겟의 소스 ASC마다 스택 인스턴스가 별도로 있다. 각 소스는 X개의 스택을 적용할 수 있다.
Aggregate by Target 소스와 관계없이 타겟에 단 하나의 스택 인스턴스가 있다. 각 소스는 공유 스택 제한까지 스택을 적용할 수 있다.

스택에는 만료, 기간 새로 고침 및 기간 재설정에 대한 정책도 있다. 

샘플 프로젝트에는 GameplayEffect의 스택이 바뀌는걸 탐지하기 위한 커스텀 블루프린트가 있다. HUD UMG 위젯에서 플레이어가 가진 아머의 스택의 양을 업데이트할 때 사용한다. 이 AsyncTask는 EndTask()가 호출될 때까지 영원히 살아있다. EndTask는 위젯의 소멸될 때 호출된다. AsyncTaskEffectStackChanged.h/cpp

 

4.5.6 Granted Abilities

GameplayEffects는 ASc에 새로운 GameplayAblility를 부여할 수 있다. Duration과 Infinite에서 어빌리티를 부여하는게 가능하다. 

흔한 사용경우로는 다른 플레이어를 넉백시키거나 당기거나하는 이동시키는 것을 예로 들 수 있다. 다른 플레이어에서 자동적으로 활성화 되는 Ability를 부여하는 Effect를 적용시킨다. 어빌리티가 부여되었을 때, 자동적으로 액티브되는 지는 Passive Abliities를 보면 알 수 있다. 

디자이너는 GameplayEffect가 부여하는 기능, 부여할 수준, 바인딩할 입력 및 부여된 기능에 대한 제거 정책을 선택할 수 있다. (밀치기 몽타주 호출시키기?)

Removal Policy Description
Cancel Ablilty Immediately 부여된 어빌리티는 부여된 게임플레이 이펙트가 대상에서 제거되면 즉시 취소되고 제거된다.
Remove Ability on End 부여된 어빌리티가 완료되고 타겟에서 제거된다.
Do Nothing 부여된 어빌리티는 게임플레이 이펙트가 대상에서 제거되어도 영향을 받지 않는다. 나중에 수동적으로 제거될 때까지 영구히 타겟에 남는다. 

 

4.5.7 Gameplay Effect Tags

게임플레이 이펙트는 여러개의 GameplayTagContainers 가질 수 있다. 디자이너가 각 카테고리마다 게임플레이 태그 컨테이너를 더하거나 제거하여 결과적으로 컴파일 중에 결합된 게임플레이 태그 컨테이너가 생겨난다. 더해진 태그는 부모 게임플레이 이펙트에서 없던 태그를 추가한 태그이다. 제거할 태그는 부모 게임플레이 이펙트에 있는 태그 중에서 제거할 태그이다. 

Category Description
Gameplay Effect Asset Tags 게임플레이 이펙트가 가진 태그이다. 이것은 아무런 기능을 제공하지 않고 단순히 게임플레이 이펙트에 대한 설명을 위한 태그이다.
Granted Tags 게임플레이 이펙트에 살고 있지만 게임플레이 이펙트가 적용되는 ASC에도 주어지는 태그이다. 게임플레이 이펙트가 제거될 때, ASC에서도 제거된다. Duration과 Infinte에서만 적용된다. 
Ongoing Tag Requirements 일단 적용되면 이 태그들은 게임플레이 이펙트가 켜져 있는지 여부를 결정한다. 게임플레이 이펙트는 해제된 상태에서 계속 적용될 수 있다. 진행중인 태그 요구사항이 충족되지 않아 게임플레이 이펙트가 해제되고 다시 충족이 되면 게임플레이 이펙트는 다시 켜지고 그 modifier를 다시 적용한다. Duration과 Infinite에서만 적용된다.
Application Tag Requirements 게임플레이 이펙트를 타겟에 적용할 수 있는지 여부를 결정하는 타겟의 태그이다. 이 요구사항이 충족되지 않으면 게임플레이 이펙트는 적용되지 않는다.
Remove Gameplay Effects with Tags 이 게임플레이 이펙트가 성공적으로 적용되었을 때, 타겟의 Remove Gameplay Effects with Tags에 있는 태그가 Asset Tags나 Granged Tags에 있다면 타겟에서 삭제된다.

 

4.5.8 Immunity

게임플레이 이펙트는 게임플레이 태그를 기반으로 다른 게임플레이 효과의 적용을 차단할 수 있는 면역을 부여할 수 있다. 

Application Tag Requirements와 같은 다른 방법을 통해 면역 기능을 효과적으로 제공할 수 있지만 이 기능은 게임플레이 이펙트가 막혔을 경우의 델리게이트를 제공한다. (UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate)

GrantedApplicationImmunityTags는 소스의 ASC가 명시된 태그를 가지고 있는지 체크한다 (있는 경우 소스 어빌리티의 어빌리티 태그에 있는 태그 포함). 이 방법은 특정 캐릭터 또는 태그를 기반으로 소스로부터의 모든 게임플레이 효과에 대한 면역을 제공하는 방법이다.

Granted Application Immunity Query는 받은 게임플레이 이펙트 스펙에 대해서 차단하거나 허용하는 쿼리와 일치하는지 확인한다. 

 

4.5.9 Gameplay Effect Spec

GESpec은 게임플레이 이펙트의 인스턴스화라고 생각할 수 있다. 게임플레이 이펙트 클래스를 대변하고 어떤 레벨이 생성되었고 누가 생성했는지에 대한 참조를 들고있다. GESpec은 런타임에 자유롭게 만들어지며 조정된다. before application unlike GameplayEffects which should be created by designers prior to runtime. 게임플레이 이펙트가 적용될 때, 게임플레이 이펙트 스펙은 게임플레이 이펙트로부터 생성되며 실질적으로 타겟에 적용된다. 

게임플레이 이펙트 스펙은 게임플레이 이펙트로부터 생성되며 UAbilitySystemComponent::MakeOutgoingSpec()을 이용하며 BlueprintCallable이다. 게임플레이 이펙트 스펙은 곧 바로 적용해서는 안된다. 게임플레이 이펙트 스펙을 나중에 부딪히는 대상에 적용할 수 있는 어빌리티로 생성된 발사체에 게임플레이 이펙트스펙을 전달하는 것은 일반적이다. 게임플레이 스펙이 성공적으로 적용되었다면 FActiveGemaplayEffect라고 불리는 새로운 구조체를 반환한다. 

Notable GameplayEffectSpec Contents:

  • The GameplayEffect class that this GameplayEffect was created from.
  • 게임플레이이펙트 스펙 레벨
  • 게임플레이 이펙트 스펙 기간
  • The period of the GameplayEffectSpec for periodic effects. Defaults to the period of the GameplayEffect but can be different.
  • The current stack count of this GameplayEffectSpec. The stack limit is on the GameplayEffect.
  • The GameplayEffectContextHandle tells us who created this GameplayEffectSpec.
  • Attributes that were captured at the time of the GameplayEffectSpec's creation due to snapshotting.
  • DynamicGrantedTags that the GameplayEffectSpec grants to the Target in addition to the GameplayTags that the GameplayEffect grants.
  • DynamicAssetTags that the GameplayEffectSpec has in addition to the AssetTags that the GameplayEffect has.
  • SetByCaller TMaps.

4.5.9.1 SetByCallers

SetByCallers는 게임플레이 이펙트 스펙이 게임플레이 태그와 FName과 연관된 float값을 전달할 수 있도록 한다. 그 것들은 TMap<FGameplayTag, flat>, TMap<FName, float>형태로 게임플레이 이펙트 스펙에 저장된다. 이 것은 게임플레이 이펙트에서 Modifiers로 이용될 수 있다. 이 방법은 어빌리티 안의 GameplayEffectExecutionCalculations나 ModfierMagnitudeCalculations에 데이터를 전달하는데 흔한 방법이다. 

SetByCaller Use Notes
Modifiers 게임플레이 이펙트 클래스에서 무조건 먼저 정의되어야 한다. 게임플레이 태그 버전만 사용할 수 있다. 게임플레이 이펙트 클래스에 정의되어 있는 것이 게임플레이 이펙트 스펙에 동일한 것이 없다면 런타임 에러가 발생하고 0을 반환한다. 이 것은 나누기 연산에서 잠재적으로 문제가 있다. 
Elsewhere 어딘가에 먼저 정의할 필요가 없다. 게임플레이 이펙트 스펙에 존재하지 않은 SetByCaller를 읽는 것은 개발자가 먼저 정의한 기본 값을 반환하고 문제 일으킬 수 있다.

C++에서 SetByCaller 값을 할당하고 싶다면:

void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);

C++에서 SetByCaller 값을 읽고 싶다면:

float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;

게임플레이 태그 버전을 쓰는게 FName보다 좋다. 스펠링 오류를 예방

 

4.5.10 Gameplay Effect Context

Gameplay Effect Context 구조체에는 GameplayEffectSpec의 Instigator와 타겟의 데이터가 들어가 있다. 이것은 또한 임의의 데이터를 전달하기 위한 서브 클래스에 적합한 구조이다. ModifierMagnitudeCalculations / GameplayEffectExecutionCalculations, AttributeSets와  GameplayCues사이에

GASShooter에서는 타겟 데이터를 더하기 위해 게임플레이 이펙트 콘텍스트를 사용하며 타겟 데이터는 게임플레이 큐에 접근하며 특히 샷건 의 경우 여러명의 적을 공격할 수 있다. 

To subclass the GameplayEffectContext:

  1. Subclass FGameplayEffectContext
  2. Override FGameplayEffectContext::GetScriptStruct()
  3. Override FGameplayEffectContext::Duplicate()
  4. Override FGameplayEffectContext::NetSerialize() if your new data needs to be replicated
  5. Implement TStructOpsTypeTraits for your subclass, like the parent struct FGameplayEffectContext has
  6. Override AllocGameplayEffectContext() in your AbilitySystemGlobals class to return a new object of your subclass

 

4.5.11 Modifier Magnitude Cacluation

ModifierMagnitudeCalculation(ModMagCalc or MMC)는 게임플레이 이펙트에서 Modifiers로 사용되는 굉장히 유용한 클래스이다. GameplayEffectExecutionCalculations와 비슷하게 작동하지만 이 것은 predicted가 된다. 그들의 주된 목적은 CalculateBaseMagnitude_Implementation()으로부터 float값을 반환하는 것이다. 이 클래스로 하위클래스를 블루프린트나 C++에서 만들 수 있다. 

MMC는 Instant, Duration, Infinite, Periodic아무데서나 사용가능하다. 

MMC의 강점은 게임플레이 이펙트의 소스 또는 타겟에 있는 모든 어트리뷰트의 값을 갭쳐할 수 있다는 것이다. 게임플레이 이펙트 스펙과 완전히 액세스하여 게임플레이 태그와 SetByCaller를 읽을 수 있다. 어트리뷰트도 스냅샷이 가능하다. 스냅샷된 어트리뷰트는 게임플레이 이펙트 스펙이 만들어졌을 때, 캡쳐가 되며 스탭샷이 안된 어트리뷰트는 게임플레이 이펙트 스펙이 적용되거나 Infinte, Duration 이펙트에서 자동적으로 업데이트 되었을 때, 캡쳐된다. 어트리뷰를 캡쳐하는 것은 ASC의 존재하는 mods로 부터 CurrentValue를 재계산한다. 이 재계산은 어빌리티 셋의 PreAttributeChange()를 실행하지 않으므로 클램핑은 무조건 여기서 다시 실행해야 한다. 

SnapshotSource or TargetCaptured on GameplayEffectSpecAutomatically updates when Attribute changes for Infinite or Duration GE

Snapshot Source or Target Captured on GameplayEffectSpec Automatically updates when Attribute changes for Infinite or Duration GE
Yes Source Createion No
Yes Target Application No
No Source Application Yes
No Target Application Yes

MMC의 결과인 float 값은 다시 수정될 수 있다. 게임플레이 이펙트의 Modifier안에서 계수와 먼저, 이후의 계수가 더해지면서.

MMC의 예를 들어보면, 타겟의 마나와 태그가 얼마냐 있느냐에 따라 포션의 효과를 줄이는 작업을 한다면 MMC가 타겟의 마나 어트리뷰트를 캡쳐해서 알 수 있다. 

UPAMMC_PoisonMana::UPAMMC_PoisonMana() // 생성자
{

	//ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
	ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
	ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	ManaDef.bSnapshot = false;

	//MaxManaDef defined in header FGameplayEffectAttributeCaptureDefinition MaxManaDef;
	MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
	MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	MaxManaDef.bSnapshot = false;

	RelevantAttributesToCapture.Add(ManaDef);
	RelevantAttributesToCapture.Add(MaxManaDef);
}

float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	// Gather the tags from the source and target as that can affect which buffs should be used
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	float Mana = 0.f;
	GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
	Mana = FMath::Max<float>(Mana, 0.0f);

	float MaxMana = 0.f;
	GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
	MaxMana = FMath::Max<float>(MaxMana, 1.0f); // Avoid divide by zero

	float Reduction = -20.0f;
	if (Mana / MaxMana > 0.5f)
	{
		// Double the effect if the target has more than half their mana
		Reduction *= 2;
	}
	
	if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
	{
		// Double the effect if the target is weak to PoisonMana
		Reduction *= 2;
	}
	
	return Reduction;
}

만약 MMC의 생성자에 RelevantAttributesToCapture에 FGameplayEffectAttributeCaptureDefinition을 더하지 않았다면 캡쳐링 시 스펙을 잃는 에러가 발생할 것이다. 만약 어트리뷰트를 캡쳐할 필요가 없다면 RelevantAttributesToCapture에 아무것도 더할 필요가 없다. 

 

4.5.12 Gameplay Effect Execution Calcualtion (UGDDamageExecCalculation에 있다.)

GameplayEffectExecutionCalculation(ExecutionCalculation, Execution (you will often see this term in the plugin's source code), or ExecCalc)은 ASC를 변경할 수 있는 게임플레이 이펙트의 가장 강력한 방법이다. ModifierMagnitudeCalculations처럼 어트리뷰트를 캡쳐, 스냅샷 할 수 있다. MMC와는 다르게, 이 것은 하나 이상의 어트리뷰트를 변경할 수 있고 본질적으로 프로그래머가 원하는 무엇이든 할수 있다. 이러한 강점과 유연성의 단점으로는 predicted가 불가하며 C++로 구현되어야 한다는 것이다. 

ExecutionCalculation은 Instant와 Periodic 게임플레이 이펙트에서만 사용될 수 있다. Execution이라는 단어가 포함된 모든 항목은 일반적으로 이 두 가지 유형의 게임플레이 효과를 나타낸다. 

스냅샷은 어트리뷰트를 게임플레이 이펙트 스펙이 만들어 졌을 때 캡쳐하고, not 스탭샷은 어트리뷰트를 게임플레이 이펙트 스펙이 적용되었을 때, 캡쳐한다. 어트리뷰트를 캡쳐하는 것은 ASC의 존재하는 모드로부터 CurrentValue를 재계산한다. 이 재계산은 AttributeSet의 PreAttributeChange를 호출하지 않으니 여기서 클램핑이 완료되어야 한다. 

Snapshot Source or Target Captured on GameplayEffectSpec
Yes Source Creation
Yes Target Application
No Source Application
No Target Application

어트리뷰트 캡쳐의 셋업을 위해 에픽의 ActionRPG 샘플프로젝트의 패턴을 보자. 구조체를 하나 정의한다. 구조체는 어트리뷰트를 어떻게 캡쳐하는지 결정하고, 구조체의 생성자에서 그것을 만든다.매 ExecCalc마다 이러한 구조체를 가질 것이다. Note: 각 구조체는 같은 네임스페이스에서 서로 유일한 이름을 가져야한다. 구조체마다 서로 같은 이름을 쓴다면 어트리뷰트를 캡쳐하는데 이상이 있을 것이다. 

// GDDamageExecCalculation.cpp
// Declare the attributes to capture and define how we want to capture them from the Source and Target.
struct GDDamageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);

	GDDamageStatics()
	{
		// Snapshot happens at time of GESpec creation

		// We're not capturing anything from the Source in this example, but there could be like AttackPower attributes that you might want.

		// Capture optional Damage set on the damage GE as a CalculationModifier under the ExecutionCalculation
		DEFINE_ATTRIBUTE_CAPTUREDEF(UGDAttributeSetBase, Damage, Source, true);

		// Capture the Target's Armor. Don't snapshot.
		DEFINE_ATTRIBUTE_CAPTUREDEF(UGDAttributeSetBase, Armor, Target, false);
	}
};

For Local Predicted, Server Only, and Server Initiated GameplayAbilities, the ExecCalc only calls on the Server.

소스와 타겟으로부터 다양한 어트리뷰트를 읽으면서 복잡한 계산을 통해 데미지를 계산하는 것은 ExecCalc의 가장 흔한 예시이다. 샘플 프로젝트에서 데미지를 계산하는 간단한 ExecCalc가 있다. 데미지는 게임플레이 이펙트 스펙의 SetByCaller이며 타겟으로 부터 캡쳐된 어트리뷰트의 아머가 데미지를 완화한다. 

4.5.12.1 Sending Data to Execution Calculations

어트리뷰트를 캡쳐하는 것 외에도 ExecutionCalculation에 데이터를 보내는 방법은 몇 가지가 있다. 

4.5.12.1.1 SetByCaller

게임플레이 이펙트 스펙에 있는 SetByCaller는 직접적으로 ExecutionCalculation에 읽혀진다. 

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
float Damage = FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);

4.5.12.1.2 Backing Data Attribute Calculation Modifier

게임플레이 이펙트에 값을 하드코딩으로 넣길 원한다면 CalculationModifier를 사용하여 넣을 수 있다. 그 것은 backing 데이터로 캡쳐된 어트리뷰트 중 하나를 사용한다.

밑의 예시에선 캡쳐된 데미지 어트리뷰트에 50을 추가했다. Override로 값을 덮어쓰기해도 된다.

 

 

ExecutionCalculation은 어트리뷰트를 갭쳐할 때, 이 값을 읽는다. 

float Damage = 0.0f;
// Capture optional damage value set on the damage GE as a CalculationModifier under the ExecutionCalculation
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier

게임플레이 이펙트에 값을 하드코딩으로 넣길 원한다면, CalculationModifier를 사용하여 넣을 수 있다. 이 것은 C++에서 호출되는 Temporary Variable이나 Transient Aggregator를 사용한다. Temporary Variable은 게임플레이 태그와 연관되어 있다. 

밑의 예시에선 Temporary Variable을 Data.Damage 게임플레이 태그를 사용하여 50의 데미지를 추가한 것이다.

ExecutionCalculation의 생성자에 Temporary Variable을 추가한다. 

ValidTransientAggregatorIdentifiers.AddTag(FGameplayTag::RequestGameplayTag("Data.Damage"));

어트리뷰트 캡쳐 함수와 비슷한 특별한 캡쳐 함수를 사용하여 ExecutionCalculation은 이 값을 읽는다.

float Damage = 0.0f;
ExecutionParams.AttemptCalculateTransientAggregatorMagnitude(FGameplayTag::RequestGameplayTag("Data.Damage"), EvaluationParameters, Damage);

4.5.12.1.4 Gameplay Effect Context

게임플레이 이펙트 스펙의 게임플레이 이펙트 콘텍스트를 커스텀해서 ExecutionCalculation으로 데이터를 보낼 수 있다. 

ExecutionCalculation에서 FGameplayEffectCustomExecutionParameters의 EffectContext에 접근할 수 있다. 

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());

만약 게임플레이 이펙트 스펙이나 EffectContext를 바꿀 필요가 있다면:

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());

ExecutionCalculation의 게임플레이 이펙트 스펙을 수정할 경우 주의하시오. GetOwningSpecForPreExecuteMod()의 코멘트를 보시오.

/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

 

4.5.13 Custom Application Requirement

CustomApplicationRequirement 클래는 디자이너에게 향상된 컨트롤을 제공한다. 게임플레이 이펙트는 게임플레이 이펙트의 게임플레이 태그를 비교하여 적용될 수 있다. 이 것은 블루프린트에선 CanApplyGameplayEffect()를 재정의 하거나 C++에선 CanApplyGameplayEffect_Implementation()을 재정의하는 것으로 구현할 수 있다. 

언제 CARs를 쓰는가:

  • 타겟이 한 Attribute가 어느 정도 필요할 경우
  • 타겟이 한 게임플레이 이펙트의 스택이 얼마정도 필요할 경우

CARs는 다음과 같은 보다 향상된 것들을 할 수 있다. 게임플레이 이펙트의 인스턴스가 이미 타겟에 올라와 있는지. 그리고 이미 존재하는 인스턴스의 기간을 변경하는 것(새로운 인스턴스를 적용하는 것 대신에(return false for CanApplyGameplayEffect())

 

4.5.14 Cost Gameplay Effect

게임플레이 어빌리티는 어빌리티의 코스트로 게임플레이 이펙트를 설정할 수 있다. 코스트는 게임플레이 어빌리티를 활성화하기 위해서 필요한 ASC의 어트리뷰트이다. 만약 게임플레이 어빌리티가 코스트를 감당할 수 없다면 실행이 불가하다. 코스트 게임플레이 이펙트는 어트리뷰트에서 빼는 하나 혹은 여러개의 Modifiers로 Instant 게임플레이 이펙트여야 한다. 기본적으로 Predicted되며 ExecutionCalculations를 사용하지 않는 것이 좋다. MMCs가 복잡한 코스트 연산을 하는데 적합하다. 

아마도 하나의 어빌리티에 하나의 코스트 이펙트를 설정할 것이다. 보다 좋은 테크닉은 여러개의 어빌리티에 하나의 코스트 이펙트를 재사용하는 것이다. 단순히 코스트 이펙트에서 생성된 게임플레이 이펙트 스펙을 조정하는 것으로 어빌리티의 특정 데이터를 사용한다(코스트 값은 어빌리티에서 정의되어 있다.). 이 방법은 오직 instanced 어빌리티에서만 사용가능하다. 

코스트 GE의 2가지 재사용방법:

  • MMC를 사용한다. 이게 가장 쉬운 방법이다. MMC를 만든다. MMC는 게임플레이 어빌리티 인스턴스로부터 코스트 값을 읽는다. 인스턴스는 게임플레이 이펙트 스펙에서 부터 얻을 수 있다.
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

	if (!Ability)
	{
		return 0.0f;
	}

	return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}

이 예시에서 Cost는 GameplayAbility의 자식 클래스에서 FScaleableFloat로 정의한 값이다.

// GameplayAbility의 자식 클래스의 헤더
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;

  • UGameplayAbility::GetCostGameplayEffect()를 Override하는 방법이다. 이 함수를 재정의하고 런타임에 GameplayEffect를 생성하는 방법이다. 이 함수는 게임플레이 어빌리티의 코스트 값을 읽는다. 샘플의 GE_Cost블루프린트들이 이렇게 되어 있다.

4.5.15 Cooldown Gameplay Effect

어빌리티는 어빌리티의 쿨다운 시간을 정할 수 있는 게임플레이 이펙트를 설정할 수 있다. 쿨다운은 다음 재사용 시간까지 얼마나 걸리는 지를 정의한다. 만약  GA가 쿨다운 중이라면 활성화가 불가하다. 쿨다운 이펙트는 Duration으로 정의되어야 하며 하나의 어빌리티나 하나의 어빌리티 슬롯(쿨다운을 공유하는 어빌리티 교체슬롯을 사용한다면)에 modifier와 게임플레이 이펙트의 GrantedTags("Cooldown Tag")안에 unique GameplayTag가 없어야 한다.