본문 바로가기
둥지/Unreal

Unreal GAS(GameplayAbilitySystem) Documentation 번역글 2부

by 까닭 2023. 5. 27.

4.5 Gameplay Effect

4.5.1 Gameplay Effect 정의

GameplayEffect(GameplayEffect, GE)는 Ability가 자신을 포함한 다른 객체의 Attribute 및 GameplayTag를 변경하기 위한 수단입니다. 즉각적인 Attribute 변화를 일으킬 수 있거나(예: 피해나 치유) 이동 속도 증가나 기절과 같은 장기적인 상태 버프/디버프를 적용할 수도 있습니다. UGameplayEffect 클래스는 단일 GameplayEffect를 정의하는 데이터 전용 클래스입니다. 추가적인 로직은 GameplayEffect에 포함되지 않아야 합니다. 보통 디자이너는 UGameplayEffect의 여러 블루프린트 자식 클래스를 생성하여 사용합니다.

 

GameplayEffect는 수정자(Modifier)와 실행(Execution, GameplayEffectExecutionCalculation)을 통해 Attribute를 변경합니다.

 

GameplayEffect에는 세 가지 지속 시간 타입을 가집니다: Instant, Duration, Infinite

 

또한, GameplayEffect는 GameplayCue를 추가하거나 실행할 수 있습니다. Instant GameplayEffect는 GameplayCue GameplayTag에서 Execute를 호출하는 반면, Duration 또는 Infinite GameplayEffect는 GameplayCue GameplayTag에서 Add와 Remove를 호출합니다.

Duration 타 GameplayCue
이벤트
사용 시기
Instant  Execute Attribute의 BaseValue에 즉각적이고 영구적인 변화를 줄 때 사용합니다. 이 경우 GameplayTag는 적용되지 않으며, 한 프레임 동안조차도 존재하지 않습니다.
Duration  Add & Remove Attribute의 CurrentValue를 일시적으로 변경하거나 특정 기간 동안 GameplayTag를 적용할 때 사용합니다. 해당 기간은 UGameplayEffect 클래스 또는 블루프린트에서 정의됩니다.
Infinite Add & Remove Attribute의 CurrentValue를 일시적으로 변경하거나 무한으로 GameplayTag를 적용할 때 사용합니다. 해당 Effect는 스스로 만료되지 않으며, 반드시 Ability나 ASC(Ability System Component)를 통해 수동으로 제거해야 합니다.

Duration 및 Infinite 타입의 GameplayEffect는 Periodic Effect(주기적 효과)를 사용할 수 있습니다. Periodic Effect는 설정된 주기(Period)마다 해당 Modifier와 Execution을 실행합니다. Periodic Effect는 Attribute의 BaseValue를 변경하거나 GameplayCue를 실행할 때 Instant 타입의 GameplayEffect로 처리됩니다. 이는 DOT(Damage Over Time) 같은 효과에 유용합니다.

💡 NOTE: Periodic Effect는 예측될 수 없습니다.

또한 Duration 및 Infinite 타입의 GameplayEffect는 Ongoing Tag Requirement(진행 중 태그 요구 조건)를 통해 적용 상태를 일시적으로 비활성화하거나 활성화할 수 있습니다. 비활성화되면 Modifier와 적용된 GameplayTag는 제거되지만, GameplayEffect 자체는 제거되지 않습니다. 이후 요구 조건이 충족되면 Modifier와 GameplayTag가 다시 적용됩니다.

 

Duration 또는 Infinite 타입의 GameplayEffect의 Modifier를 수동으로 재계산해야 할 경우(예: MMC가 속성이 아닌 데이터를 사용하는 경우), 다음 함수를 호출할 수 있습니다:

UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel);
 
 

이때 동일한 레벨을 유지하려면 다음과 같이 할 수 있습니다:

UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel();

 

Attribute를 기반으로 한 Modifier는 해당 Attribute가 업데이트될 때 자동으로 업데이트됩니다.

SetActiveGameplayEffectLevel() 함수가 Modifier를 업데이트하는 핵심 작업은 다음과 같습니다:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
//Private 함수이기 때문에, 만약 호출할 수 있었다면 레벨을 굳이 다시 설정하지 않고도 이 세 함수를 호출했을 것입니다.
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffect는 일반적으로 인스턴스화되지 않습니다. Ability나 ASC가 GameplayEffect를 적용할 때, GameplayEffect의 ClassDefaultObject를 기반으로 GameplayEffectSpec이 생성됩니다. 성공적으로 적용된 GameplayEffectSpec은 FActiveGameplayEffect라는 구조체에 추가되며, ASC는 이를 특별한 컨테이너 구조체인 ActiveGameplayEffect에서 관리합니다.

 

4.5.2 Gameplay Effect 적용

GameplayEffect는 다양한 방법으로 적용할 수 있으며, 주로 GameplayAbility의 함수나 ASC의 함수를 통해 이루어집니다. 이러한 함수들은 보통 ApplyGameplayEffectTo 형태를 가지며, 이러한 다양한 함수들은 결국 Target의 UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()를 호출하여 Effect를 적용합니다.

 

GameplayAbility 외부(예: 투사체)에서 GameplayEffect를 적용하려면, Target의 ASC를 가져와서 해당 ASC의 함수 중 하나를 사용해 ApplyGameplayEffectToSelf를 호출해야 합니다. Duration 또는 Infinite 타입의 GameplayEffect가 ASC에 적용되었을 때 이를 감지하려면, 다음과 같이 델리게이트에 바인딩하면 됩니다:

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

 

해당 콜백 함수는 다음과 같습니다:

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

서버는 항상 이 함수를 호출하며, 리플리케이션 모드와 관계없이 호출됩니다. Autonomous Proxy는 Full 및 Mixed 모드에서만 리플리케이트된 GameplayEffect에 대해 이 함수를 호출합니다. Simulated Proxy는 Full 복제 모드에서만 이 함수를 호출합니다.

 

4.5.3 Gameplay Effect 삭제

GameplayEffect는 GameplayAbility의 함수나 ASC의 함수를 통해 다양한 방식으로 제거할 수 있습니다. 일반적으로 RemoveActiveGameplayEffect 형태를 가지며, 이러한 함수들은 결국 Target의 FActiveGameplayEffectsContainer::RemoveActiveEffects()를 호출합니다.

 

GameplayAbility 외부(예: 투사체)에서 GameplayEffect를 제거하려면, 타겟의 ASC를 가져와서 해당 ASC의 함수 중 하나를 사용해 RemoveActiveGameplayEffect를 호출해야 합니다. Duration 또는 Infinite 타입의 GameplayEffect가 ASC에서 제거되었을 때 이를 감지하려면, 다음과 같이 델리게이트에 바인딩하면 됩니다:

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

해당 콜백 함수는 다음과 같습니다:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

서버는 항상 이 함수를 호출하며, 리플리케이션 모드와 관계없이 호출됩니다. Autonomous ProxyFullMixed 모드에서만 리플리케이트된 GameplayEffect에 대해 이 함수를 호출합니다. Simulated ProxyFull 모드에서만 이 함수를 호출합니다.

 

 

4.5.4 Gameplay Effect Modifier

Modifier는 Attribute를 변경하며, 예측적으로 Attribute 변경할 수 있는 유일한 방법입니다. GameplayEffect는 Modifier를 0개 혹은 여러 개 가질 수 있습니다. 각 Modifier는 지정된 연산을 통해 하나의 Attribute만 변경할 수 있습니다.

연산 내용
Add Modifier에서 지정한 Attribute에 결과 값을 더합니다. 빼기를 원할 경우 음수를 사용합니다.
Multiply Modifier에서 지정한 Attribute에 결과 값을 곱합니다.
Divide Modifier에서 지정한 Attribute를 결과 값으로 나눕니다.
Override Modifier에서 지정한 Attribute를 결과 값으로 덮어씁니다.

Attribute의 CurrentValue는 모든 Modifier가 BaseValue에 추가된 집합적인 결과입니다. 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. 이들은 모두 float 값을 생성하며, 그 값을 기반으로 연산을 수행해 지정된 Attribute를 변경합니다.

Modifier 타입 내용
Scalable Float FScalableFloat는 Data Table의 행(변수)과 열(레벨)을 참조하는 구조체입니다. Scalable Float는 자동으로 지정된 테이블 행의 값을 Ability의 현재 레벨(또는 GameplayEffectSpec에서 오버라이드된 레벨)에서 읽습니다. 이 값은 추가적으로 계수(coefficient)를 통해 조정될 수 있습니다. Data Table/Row가 지정되지 않으면 값을 1로 간주하고, 계수를 사용하여 모든 레벨에서 단일 값을 하드 코딩할 수 있습니다.

Attribute Based Attribute Based Modifier는 Source( GameplayEffectSpec을 생성한 주체)나 Target( GameplayEffectSpec을 받은 대상)의 CurrentValue 또는 GameplayEffectSpec를 사용합니다. 이 값은 계수, 전/후 추가 값을 사용해 추가적으로 수정됩니다. Snapshotting은 GameplayEffectSpec이 생성될 때 백업된 Attribute 값을 캡처하며, Non-Snapshotting은 GameplayEffectSpec이 적용될 때 값을 캡처합니다.
Custom Calculation Class Custom Calculation Class는 복잡한 Modifier에 가장 큰 유연성을 제공합니다. 이 Modifier는 ModifierMagnitudeCalculation 클래스를 사용하며, 추가적으로 계수와 전/후 추가 값을 사용해 결과 float 값을 수정할 수 있습니다.
Set By Caller SetByCaller Modifier는 런타임에 Ability 또는 GameplayEffectSpec을 생성한 주체가 외부에서 값을 설정하는 Modifier입니다. 예를 들어, 플레이어가 버튼을 누른 시간에 따라 Ability의 피해량을 설정하려면 SetByCaller를 사용할 수 있습니다. SetByCaller는 TMap<FGameplayTag, float>으로 GameplayEffectSpec에 저장됩니다. Modifier는 Aggregator에 지정된 GameplayTag와 연결된 SetByCaller 값을 확인하도록 지시합니다. GameplayTag 버전만 사용할 수 있으며 FName 버전은 비활성화됩니다. Modifier가 SetByCaller로 설정되었지만 GameplayEffectSpec에 올바른 GameplayTag와 연결된 SetByCaller가 존재하지 않는 경우, 런타임 오류가 발생하고 값이 0으로 반환됩니다. Divide 연산의 경우 문제가 발생할 수 있습니다. SetByCaller의 사용 방법에 대한 더 많은 정보는 SetByCaller 관련 문서를 참조하세요.

 

4.5.4.1 Multiply 및 Divide Modifier

기본적으로, 모든 Multiply 및 Divide Modifier는 Attribute의 BaseValue에 곱하거나 나누기 전에 서로 더해집니다.

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;
}

from GameplayEffectAggregator.cpp

 

이 공식에서 Multiply와 Divide Modifier는 Bias 값이 1로 설정됩니다. (참고로 Addition은 Bias가 0입니다.) 위 코드는 다음과 같이 해석됩니다:

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

 

위 공식은 몇 가지 예상치 못한 결과를 초래합니다. 첫째, 이 공식은 모든 Modifier를 더한 후 BaseValue에 곱하거나 나누어 적용합니다. 대부분의 사람들은 서로 곱하거나 나누어 계산될 것이라고 예상합니다. 예를 들어 Multiply Modifier가 1.5인 경우 두 개를 적용하면 BaseValue가 1.5 x 1.5 = 2.25배가 되어야 할 것으로 예상하지만, 실제로는 1.5 + 1.5 = 2가 되어 BaseValue에 2를 곱하게 됩니다 (50% 증가 + 또 다른 50% 증가 = 100% 증가). 이 예시는 GameplayPrediction.h의 예시와 같습니다. 기본 속도 500에 10% 속도 버프를 적용하면 550이 됩니다. 여기에 또 다른 10% 버프를 추가하면 600이 됩니다.

그리고 둘째, 이 공식은 Paragon에 맞게 설계되었기 때문에 값에 대해 문서화되지 않은 규칙이 있습니다.

 

Multiply와 Divide의 덧셈 공식에 대한 규칙 

  • 규칙 1: (값이 1 미만인 항이 1개 이하) AND (여러 개의 값이 [1, 2) 범위에 존재 가능)
  • 규칙 2: (값이 2 이상인 항은 하나만 존재 가능)

이 공식에서 Bias는 범위 [1, 2) 내의 숫자의 정수 자릿수를 빼줍니다. 첫 번째 Modifier의 Bias는 합산 시작 값에서 빼지기 때문에 (합산 시작 값은 루프 전에 Bias로 설정됨), 개별 값 하나만 있을 때는 작동하며, 1 미만의 값이 하나만 존재하는 경우에도 제대로 작동합니다.

 

 

Multiply의 몇 가지 예시:

1. 승수: 0.5

1 + (0.5 - 1) = 0.5   //정상적인 결과

 

2. 승 : 0.5, 0.5

1 + (0.5 - 1) + (0.5 - 1) = 0   //잘못된 결과

혹시 1을 예상하셨나요? Multiply Modifier가 여러 개 있는 경우 1 미만의 값은 의미가 없습니다. Paragon은 Multiply Modifier에 대해 가장 큰 음수 값만 사용하도록 설계되었기 때문에 1 미만의 값이 하나만 존재하게 됩니다.

 

3. 승수: 1.1, 0.5

1 + (0.5 - 1) + (1.1 - 1) = 0.6   //정상적인 결과

 

4 . 승수: 5, 5

1 + (5 - 1) + (5 - 1) = 9    //잘못된 결과

혹시 10을 예상하셨나요? Modifier의 합계는 항상 Modifier - Modifier 개수 + 1이 됩니다.

 

많은 게임들은 Multiply와 Divide Modifier가 BaseValue에 곱하거나 나누어지기 전에 서로 곱하고 나누어지기를 원합니다. 이를 구현하려면 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 Modifier의 GameplayTag

SourceTag와 TargetTag는 각 Modifier에 대해 설정할 수 있습니다. 이들은 GameplayEffect의 Application Tag 요구 사항처럼 작동합니다. 즉, 태그는 효과가 적용될 때만 고려됩니다. 예를 들어, 주기적인 Infinite GameplayEffect에서는 첫 번째 적용 시에만 태그가 고려되고, Periodic Execution에서는 고려되지 않습니다.

 

Attribute Based Modifier는 또한 SourceTagFilter와 TargetTagFilter를 설정할 수 있습니다. 이 필터들은 Attribute Based Modifier의 소스가 되는 Attribute의 Magnitude를 결정할 때 사용되어 특정 Modifier를 그 Attribute에서 제외시킵니다. 소스 또는 타겟에 필터의 모든 태그가 없을 경우 해당 Modifier는 제외됩니다.

 

위 내용은 다음을 의미합니다:
소스 ASC와 타겟 ASC의 태그는 GameplayEffect에 의해 캡처됩니다. 소스 ASC 태그는 GameplayEffectSpec이 생성될 때 캡처되고, 타겟 ASC 태그는 효과가 실행될 때 캡처됩니다. Infinite GameplayEffect나 Duration GameplayEffect의 Modifier가 적용될 자격이 있는지(즉, Aggregator가 자격이 있는지) 결정할 때 필터가 설정된 경우, 캡처된 태그는 필터와 비교됩니다.

 

4.5.5 Gameplay Effect Stacking(중첩)

기본적으로 GameplayEffect는 새로운 인스턴스를 적용할 때 이전에 존재한 GameplayEffectSpec에 대해 알지 못하고 신경 쓰지 않습니다. GameplayEffect는 스택되도록 설정할 수 있으며, 이 경우 새로운 GameplayEffectSpec 인스턴스가 추가되는 대신 현재 존재하는 GameplayEffectSpec의 스택 수가 변경됩니다. 스택은 Duration과 Infinite에서만 동작합니다.

 

스택에는 두 가지 유형이 있습니다: Source별 집합(Aggregate by Source)과 Target별 집합(Aggregate by Target)

스택 유형 설명
Aggregate by Source 각 Source ASC마다 타겟에 대한 별도의 스택 인스턴스가 있습니다.
각 Source는 X 만큼의 스택을 적용할 수 있습니다.
Aggregate by Target 타겟에 대해 하나의 스택 인스턴스만 존재합니다.
각 Source는 공유된 스택 한도까지 스택을 적용할 수 있습니다.

스택에는 만료, 지속 시간 새로 고침, 주기 초기화에 대한 Policy도 있습니다. 이들에 대한 도움말 툴팁은 GameplayEffect Blueprint에서 확인할 수 있습니다.

 

샘플 프로젝트에는 GameplayEffectSpec 변경 사항을 수신하는 커스텀 Blueprint 노드가 포함되어 있습니다. HUD UMG 위젯은 이를 사용하여 플레이어의 패시브 방어구 스택 수를 업데이트합니다. 이 AsyncTask는 수동으로 EndTask()가 호출될 때까지 계속 살아 있습니다. 호출은 UMG 위젯의 Destruct 이벤트에서 수행됩니다. AsyncTaskEffectStackChanged.h/cpp를 참조하십시오.

 

4.5.6 Ability 부여

GameplayEffect는 ASC에 새로운 GameplayAbility를 부여할 수 있습니다. Duration 혹은 Infinite GameplayEffect만이 Ability를 부여할 수 있습니다.

 

일반적인 사용 사례 중 하나는 다른 플레이어가 특정 동작(예: 넉백이나 끌어당김)에 반응하도록 강제하는 것입니다. 예를 들어, 특정 행동을 적용하기 위해 GameplayEffect를 그들에게 부여하고, 자동으로 활성화되는 GameplayAbility(부여 시 Ability를 자동으로 활성화하는 방법에 대해서는 패시브 Ability 참조)를 부여하면 원하는 동작이 실행됩니다.

 

디자이너는 GameplayEffect가 어떤 Ability를 부여할지, 어떤 레벨로 부여할지, 어떤 입력에 바인딩할지, 그리고 부여된 Ability의 Removal Policy(제거 방침)을 선택할 수 있습니다.

Removal Policy 설명
Cancel Ability Immediately GameplayEffect가 제거될 때 부여된 Ability는 즉시 취소되고 제거됩니다.
Remove Ability on End 부여된 Ability는 완료될 때까지 유지되며 이후 타겟에서 제거됩니다.
Do Nothing GameplayEffect가 제거되더라도 부여된 Ability는 영향을 받지 않습니다.
Ability는 수동으로 제거될 때까지 영구적으로 유지됩니다

 

4.5.7 Gameplay Effect Tag

GameplayEffect는 여러 개의 GameplayTagContainer를 가질 수 있습니다. 디자이너는 각 카테고리에 대해 추가된 태그(Added Tag)와 제거된 태그(Removed Tag)를 설정할 수 있으며, 그 결과는 컴파일 시 Combined GameplayTagContainer에 표시됩니다.

  • 추가된 태그: 상위 클래스에 없던 태그를 해당 GameplayEffect가 추가하는 경우입니다.
  • 제거된 태그: 상위 클래스에는 있지만 해당 서브 클래스에는 없는 태그를 의미합니다.
카테고리 설명
Gameplay Effect Asset Tags GameplayEffect가 가진 태그입니다. 해당 태그는 별도의 기능을 수행하지 않으며 GameplayEffect를 설명하는 용도로만 사용됩니다.
Granted Tags GameplayEffect에 존재하며, 해당 GameplayEffect가 적용된 ASC에도 전달되는 태그입니다. GameplayEffect가 제거되면 ASC에서도 태그가 제거됩니다. 이는 Duration 및 Infinite GameplayEffect에만 작동합니다.
Ongoing Tag Requirements GameplayEffect가 적용된 후, 해당 태그들은 GameplayEffect가 활성(on) 또는 비활성(off) 상태인지 결정합니다. GameplayEffect는 비활성 상태에서도 적용될 수 있습니다. 태그 요구 사항을 충족하지 않아 비활성 상태였던 GameplayEffect가 요구 사항을 다시 충족하면, GameplayEffect는 활성화되며 그 Modifier를 다시 적용합니다. 이 기능은 Duration 및 Infinite GameplayEffect에서만 작동합니다.
Application Tag Requirements 타겟에 적용될 수 있는지 여부를 결정하는 태그입니다. 요구 사항이 충족되지 않으면 GameplayEffect는 적용되지 않습니다.
Remove Gameplay Effects with Tags GameplayEffect가 성공적으로 적용되면 타겟의 Asset Tags나 Granted Tags에 해당 태그를 가진 다른 GameplayEffect가 제거됩니다.

 

4.5.8 면역(Immunity)

GameplayEffect는 GameplayTag를 기반으로 다른 GameplayEffect의 적용을 차단하는 면역(Immunity)을 부여할 수 있습니다. 면역은 Application Tag Requirement와 같은 다른 수단을 통해서도 효과적으로 달성할 수 있지만, 해당 시스템을 사용하면 UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate 델리게이트를 통해 면역으로 인해 GameplayEffect가 차단되었을 때 알림을 받을 수 있습니다.

 

GrantedApplicationImmunityTag는 Source ASC(Source에 Ability가 있었던 경우 해당 Abilty의 AbilityTag도 포함)에 지정된 태그가 있는지를 검사합니다. 이를 통해 특정 캐릭터나 Source에 기반한 태그로부터 오는 GameplayEffect를 모두 차단할 수 있습니다.

 

Granted Application Immunity Query는 들어오는 GameplayEffectSpec이 지정된 쿼리 중 하나와 일치하는지를 확인하여 적용을 차단하거나 허용합니다.

 

해당 쿼리들은 GameplayEffect 블루프린트에서 마우스를 올리면 유용한 툴팁으로 설명을 제공해 줍니다.

 

4.5.9 Gameplay Effect Spec

GameplayEffectSpec(GESpec)은 GameplayEffect의 인스턴스화된 버전으로 생각할 수 있습니다. GESpec은 이를 대표하는 GameplayEffect 클래스에 대한 참조, 생성 시점의 레벨, 그리고 이를 생성한 주체를 포함합니다. GameplayEffect는 디자이너가 런타임 이전에 만들어야 하는 반면, GameplayEffectSpec은 런타임에 자유롭게 생성 및 수정될 수 있습니다. GameplayEffect를 적용할 때, GameplayEffectSpec은 GameplayEffect로부터 생성되며 실제로 Target에 적용되는 것이 바로 이 GESpec입니다.

 

GameplayEffectSpec은 UAbilitySystemComponent::MakeOutgoingSpec()을 사용해 생성되며, 해당 함수는 BlueprintCallable입니다. GESpec은 즉시 적용될 필요는 없습니다. 일반적으로 GESpec을 Ability에서 생성된 프로젝트타일에 전달하고, 해당 프로젝트타일이 나중에 맞은 대상에게 이를 적용하는 방식으로 사용됩니다. GESpec가 성공적으로 적용되면 FActiveGameplayEffect라는 새로운 구조체가 반환됩니다.

 

참고해두면 좋은 GameplayEffectSpec의 주요 내용입니다:

  • 해당 GESpec가 생성된 GameplayEffect 클래스
  • GameplayEffectSpec의 레벨. 보통 GESpec를 생성한 Ability의 레벨과 같지만 다를 수도 있음.
  • GameplayEffectSpec의 지속 시간. 기본적으로 원본 GameplayEffect의 지속 시간이지만 다르게 설정될 수 있음.
  • GameplayEffectSpec의 Period(Period Effect의 경우). 기본적으로 원본 GameplayEffect의 Period지만 변경될 수 있음.
  • GESpec의 현재 스택 수. 스택 한계는 원본 GameplayEffect에 설정되어 있음.
  • GameplayEffectContextHandle은 해당 GameplayEffectSpec를 생성한 주체를 나타냄.
  • 스냅샷팅(Snapshotting)에 의해 GameplayEffectSpec 생성 시점에 캡처된 Attribute.
  • GameplayEffect가 부여하는 GameplayTag 외에 Target에게 추가로 부여되는 DynamicGrantedTag.
  • GameplayEffect가 가지는 AssetTag 외에 GESpec가 추가로 가지는 DynamicAssetTag.
  • SetByCaller TMap

 

4.5.9.1 SetByCaller

SetByCaller는 GameplayEffectSpec이 GameplayTag 또는 FName에 연결된 float 값을 운반하도록 허용합니다. 이 값들은 각각의 TMap에 저장됩니다:

  • TMap<FGameplayTag, float>
  • TMap<FName, float>

SetByCaller는 GameplayEffect의 Modifier로 사용되거나, 일반적으로 float 값을 다른 시스템으로 전달하는 수단으로 사용될 수 있습니다. 보통 Ability 내부에서 생성된 수치 데이터를 GameplayEffectExecutionCalculation나 ModifierMagnitudeCalculation에 전달할 때 SetByCaller가 사용됩니다.

SetByCaller 사용처 사용 방법
Modifier GameplayEffect 클래스에서 미리 정의되어야 하며, GameplayTag 버전만 사용할 수 있습니다. 만약 GameplayEffectSpec이 일치하는 태그와 float 값 쌍을 가지지 않는다면, 게임은 런타임 오류를 발생시키고 해당 GameplayEffectSpec의 값은 0이 반환됩니다. 이는 나눗셈(Divide) 연산 시 잠재적 문제를 일으킬 수 있습니다.
기타(Elsewhere) 미리 정의될 필요가 없으며, 어디서든 사용할 수 있습니다. GameplayEffectSpec에 존재하지 않는 SetByCaller 값을 읽으면 개발자가 정의한 기본 값(Default Value)을 반환할 수 있으며, 경고 메시지를 선택적으로 출력할 수도 있습니다.

블루프린트에서 SetByCaller 값을 할당하려면, 필요한 버전에 대한 블루프린트 노드(GameplayTag 또는 FName)를 사용합니다.

블루프린트에서 SetByCaller 값을 읽으려면, Blueprint Library에 커스텀 노드를 만들어야 합니다.

C++에서 SetByCaller 값을 할당하려면 필요한 함수 버전(GameplayTag 또는 FName)을 사용하세요:

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

C++에서 SetByCaller 값을 읽으려면 필요한 함수 버전(GameplayTag 또는 FName)을 사용하면 됩니다 :

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

GameplayTag 버전을 FName 버전보다 사용하는 것이 좋습니다. 이렇게 하면 블루프린트에서 철자 오류를 방지할 수 있습니다.

 

4.5.10 Gameplay Effect Context

GameplayEffectContext 구조체는 GameplayEffectSpec의 시작자(Instigator)와 TargetData에 대한 정보를 보유합니다. 해당 구조체는 또한 ModifierMagnitudeCalculation, GameplayEffectExecutionCalculation, AttributeSet, GameplayCue 등과 같은 다양한 장소에서 임의의 데이터를 전달할 때 서브클래싱하여 사용하는 데 유용합니다.

 

GameplayEffectContext를 서브클래싱하려면 다음 단계를 따르세요:

  1. FGameplayEffectContext 서브클래스 생성
  2. FGameplayEffectContext::GetScriptStruct() 재정의
  3. FGameplayEffectContext::Duplicate() 재정의
  4. 새로운 데이터가 리플리케이트되어야 하는 경우 FGameplayEffectContext::NetSerialize() 재정의
  5. 부모 구조체인 FGameplayEffectContext가 사용하는 것처럼 서브클래스를 위해 TStructOpsTypeTrait 구현
  6. AbilitySystemGlobal 클래스에서 AllocGameplayEffectContext()를 재정의하여 서브클래스의 새 객체를 반환하도록 설정

GASShooter는 서브클래싱된 GameplayEffectContext를 사용하여 TargetData를 추가하고, 이를 GameplayCue에서 접근할 수 있도록 합니다. 특히 산탄총과 같이 여러 적을 한 번에 맞힐 수 있는 경우에 유용합니다.

 

4.5.11 Modifier Magnitude Calculation

ModifierMagnitudeCalculation (ModMagCalc 또는 MMC)는 GameplayEffect에서 Modifier로 사용되는 강력한 클래스입니다. 이들은 GameplayEffectExecutionCalculation와 유사하게 작동하지만, 기능이 덜 강력하고 가장 중요한 점은 예측이 가능하다는 것입니다. 이들의 주요 목적은 CalculateBaseMagnitude_Implementation()에서 float 값을 반환하는 것입니다. 이 함수를 Blueprint와 C++에서 서브클래싱하고 재정의할 수 있습니다.

 

MMC는 Instant, Duration, Infinite, Periodic 등 어떤 종류의 GameplayEffect에도 사용할 수 있습니다.

 

MMC의 강점은 GameplayEffectSpec에 대한 전체 액세스를 통해 Source 또는 Target의 여러 Attribute 값을 캡처할 수 있다는 점입니다. 이로 인해 GameplayTag와 SetByCaller를 읽을 수 있습니다. Attribute는 스냅샷 방식으로 캡처할 수 있으며, 그렇지 않은 경우에도 캡처할 수 있습니다. 스냅샷된 Attribute는 GameplayEffectSpec이 생성될 때 캡처되고, 비스냅샷 Attribute는 GameplayEffectSpec이 적용될 때 캡처되며, Infinite와 Duration 효과에 대해 Attribute가 변경될 때 자동으로 업데이트됩니다. Attribute 캡처는 해당 Attribute의 CurrentValue를 ASC의 기존 모드에서 다시 계산합니다. 이 재계산은 AbilitySet의 PreAttributeChange()를 실행하지 않으므로, 모든 클램핑은 여기에서 다시 수행해야 합니다.

스냅샷 Source GameplayEffectSpec 캡쳐 시점  Infinite 또는 Duration GE의 Attribute가 변경될 경우 자동 업데이트 여부.
Yes Source Creation No
Yes Target Application No
No Source Application Yes
No Target Application Yes

MMC에서 나온 float 값은 GameplayEffect의 Modifier에서 계수(coefficient)와 전후 계수 추가에 의해 더 수정될 수 있습니다.

 

예시로, 타겟의 mana 속성을 캡처하여 독 효과로부터 이를 감소시키는 MMC가 있을 수 있습니다. 해당 감소량은 타겟이 가진 mana 양과 타겟이 가지고 있을 수 있는 태그에 따라 달라집니다.

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
{
	// Source와 Target의 태그를 모은다. 이는 어떤 버프를 사용할지에 영향을 미칠 수 있다.
	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); // 0으로 나누는 것을 피한다.

	float Reduction = -20.0f;
	if (Mana / MaxMana > 0.5f)
	{
    	// Target의 마나가 절반 이상일 경우 효과를 두 배로 증가시킨다.
		Reduction *= 2;
	}
	
	if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
	{
    	// 타겟이 PoisonMana에 취약하면 효과를 두 배로 증가시킨다.
		Reduction *= 2;
	}
	
	return Reduction;
}

 

4.5.12 Gameplay Effect Execution Calculation

GameplayEffectExecutionCalculation(ExecutionCalculation, Execution(해당 용어는 플러그인의 소스 코드에서 자주 보임), 또는 ExecCalc)은 GameplayEffect가 ASC에 변화를 주는 가장 강력한 방법입니다. ModifierMagnitudeCalculation와 유사하게, ExecutionCalculation도 Attribute를 캡처할 수 있으며, 이를 선택적으로 스냅샷 방식으로 캡처할 수 있습니다. MMC와는 달리, ExecutionCalculation은 하나 이상의 Attribute를 변경할 수 있으며, 본질적으로 프로그래머가 원하는 모든 작업을 수행할 수 있습니다. 그러나 이 강력한 기능과 유연성의 단점은 예측이 불가능하고 C++로 구현해야 한다는 것입니다.

 

ExecutionCalculation은 Instant와 Periodic GameplayEffect에서만 사용 가능합니다. Execute라는 단어가 포함된 것은 일반적으로 이 두 종류의 GameplayEffect를 의미합니다.

 

스냅샷은 GameplayEffectSpec이 생성될 때 Attribute를 캡처하며, 비스냅샷은 GameplayEffectSpec이 적용될 때 Attribute를 캡처합니다. Attribute 캡처는 해당 Attribute의 CurrentValue를 ASC의 기존 모드에서 다시 계산합니다. 이 재계산은 AbilitySet의 PreAttributeChange()를 실행하지 않으므로, 모든 클램핑은 여기에서 다시 수행해야 합니다.

스냅샷 Source 혹은 Target GameplayEffectSpec 캡처 시점
Yes Source  Creation
Yes Target  Application
No  Source  Application
No  Target  Application

Attribute 캡처를 설정하려면, 에픽 게임즈의 ActionRPG Sample Project에서 설정한 패턴을 따르며, Attributes를 캡처하는 방법을 정의하는 구조체를 정의하고, 구조체의 생성자에서 해당 구조체의 복사본을 생성해야 합니다. ExecCalc마다 이런 구조체가 필요합니다. 구조체 이름은 고유해야 합니다. 동일한 이름을 사용하면 Attribute 캡처에서 잘못된 동작이 발생할 수 있습니다(주로 잘못된 Attribute의 값이 캡처됨).

 

로컬 예측, 서버 전용, 서버 시작 GameplayAbiliy의 경우, ExecCalc는 서버에서만 호출됩니다.

Source와 Target에서 여러 Attribute를 읽어 복잡한 공식에 따라 피해를 계산하는 것이 ExecCalc의 가장 일반적인 예입니다. 포함된 Sample Project에는 GameplayEffectSpec의 SetByCaller에서 피해 값을 읽고, Target에서 캡처된 방어구  Attribute를 기준으로 그 값을 완화하는 간단한 ExecCalc가 포함되어 있습니다. 이 예시는 GDDamageExecCalculation.cpp/.h에서 확인할 수 있습니다.

 

4.5.12.1  Execution Calculation 실행 계산에 데이터 보내기

ExecutionCalculation에 Attribute를 캡처하는 것 외에도 데이터를 전달하는 몇 가지 방법이 있습니다.

 

4.5.12.1.1 SetByCaller

GameplayEffectSpec에 설정된 모든 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
값을 GameplayEffect에 하드코딩하려면, 캡처된 Attribute 중 하나를 백업 데이터로 사용하는 CalculationModifier를 사용하여 값을 전달할 수 있습니다.

아래의 스크린샷 예제에서는 캡처된 Damage 속성에 50을 추가하고 있습니다. 또한, 값을 오버라이드로 설정하여 하드코딩된 값만 사용할 수도 있습니다.

ExecutionCalculationAttribute를 캡처할 때 이 값을 읽습니다.

float Damage = 0.0f;
// ExecutionCalculation에서 CalculationModifier로 설정된 옵션성 피해 값을 GameplayEffect의 Damage GE에 캡처합니다.
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

 

4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier

값을 GameplayEffect에 하드코딩하려면, CalculationModifier를 사용하여 Temporary Variable 또는 C++에서 Transient Aggregator라고 불리는 값을 전달할 수 있습니다. Temporary Variable은 GameplayTag와 연결됩니다.

 

아래의 스크린샷 예제에서는 Data.Damage GameplayTag를 사용하여 Temporary Variable에 50을 추가하고 있습니다.

ExecutionCalculation의 생성자에 Backing Temporary Variable를 추가하세요:

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

ExecutionCalculation은 Attribute 캡처 함수와 유사한 특수 캡처 함수를 사용하여 이 값을 읽습니다.

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

 

4.5.12.1.4 Gameplay Effect Context

ExecutionCalculation으로 데이터를 보내려면, GameplayEffectSpec에 GameplayEffectContext를 커스텀하여 전달할 수 있습니다.

 

ExecutionCalculation에서 FGameplayEffectCustomExecutionParameter를 통해 EffectContext에 접근할 수 있습니다.

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


GameplayEffectSpec 또는 EffectContext의 내용을 변경해야 하는 경우:

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

 

 

ExecutionCalculation에서 GameplayEffectSpec을 수정할 때는 주의해야 합니다.

GetOwningSpecForPreExecuteMod()에 대한 주석을 참고하십시오.

/** Const 접근이 아닙니다. 특히 Attribute 캡처 후 Spec을 수정할 때 주의하세요. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

 

4.5.13 Custom Application Requirement

CustomApplicationRequirement(CAR) 클래스는 GameplayEffect가 적용될 수 있는지에 대한 고급 제어를 디자이너에게 제공합니다. 이는 GameplayEffect의 단순한 GameplayTag 검사보다 더 복잡한 조건을 설정할 수 있게 해줍니다. CAR은 CanApplyGameplayEffect()를 오버라이드하여 블루프린트에서 구현할 수 있으며, C++에서는 CanApplyGameplayEffect_Implementation()을 오버라이드하여 구현할 수 있습니다.

 

CAR의 사용 예시:

  • 대상이 특정 Attribute의 값을 일정 수준 이상 가지고 있어야 하는 경우
  • 대상이 특정 GameplayEffect의 스택을 일정 수 이상 가지고 있어야 하는 경우

CAR은 더 많은 고급 기능을 수행할 수 있습니다. 예를 들어, 해당 대상에게 이미 GameplayEffect의 인스턴스가 적용되어 있는지 확인하고, 새 인스턴스를 적용하는 대신 기존 인스턴스의 Duration을 변경할 수 있습니다(이 경우 CanApplyGameplayEffect()에서 false를 반환).

 

4.5.14 Cost Gameplay Effect

GameplayAbility에는 선택적으로 Ability의 Cost(비용)로 사용할 수 있도록 설계된 GameplayEffect가 존재합니다. Cost란, AbilitySystemComponent (ASC)가 GameplayAbility를 활성화하기 위해 필요한 Attribute의 양을 의미합니다. 만약 GameplayAbility가 Cost에 해당하는 GameplayEffect를 감당할 수 없다면 활성화되지 않습니다.

해당 Cost GameplayEffect는 Instant 타입이어야 하며, 하나 이상의 Modifier를 통해 Attribute에서 값을 차감합니다. 기본적으로 Cost GameplayEffect는 예측(Prediction)을 지원해야 하므로 ExecutionCalculation을 사용하지 않는 것이 좋습니다. 복잡한 Cost 계산이 필요하다면 MMC(GameplayModMagnitudeCalculation)를 사용하는 것이 허용되며 권장됩니다.

 

처음 시작할 때는 대부분 GameplayAbility마다 고유한 Cost GameplayEffect를 설정하게 될 것입니다. 하지만 더 고급 기법으로는 여러 개의 GameplayAbility에서 하나의 Cost GameplayEffect를 재사용할 수 있습니다. 이때는 Cost 값이 각 GameplayAbility에 정의되어야 하며, 생성된 GameplayEffectSpec에 GameplayAbility별 데이터를 추가로 설정합니다. 이 방법은 인스턴스화된(Instanced) GameplayAbility에서만 작동합니다.

 

Cost GameplayEffect를 재사용하는 두 가지 방법:

 

1. MMC 사용하기 (가장 쉬운 방법)
MMC를 만들고, GameplayEffectSpec에서 GameplayAbility 인스턴스로부터 Cost 값을 가져옵니다.

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 자식 클래스에 추가된 FScalableFloat 타입의 변수입니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;

2. UGameplayAbility::GetCostGameplayEffect() 오버라이드하기
이 함수를 오버라이드하면 GameplayAbility의 Cost 값을 기반으로 GameplayEffect를 런타임에 생성할 수 있습니다.

 

4.5.15 GameplayEffect Cooldown

GameplayAbility에는 Cooldown(쿨타임) 용도로 사용할 수 있도록 설계된 GameplayEffect가 있습니다. Cooldown은 GameplayAbility를 활성화한 후 다시 사용할 수 있을 때까지의 시간을 결정합니다. 만약 GameplayAbility가 아직 Cooldown 상태라면 활성화할 수 없습니다. 이 Cooldown GameplayEffect는 Duration 타입이어야 하며 Modifier가 없어야 합니다. 또한 GameplayEffect의 GrantedTag에 GameplayAbility별로 고유한 GameplayTag(“Cooldown Tag”)를 할당해야 합니다. 만약 게임에 슬롯 개념이 존재하고, 슬롯 간에 Cooldown을 공유한다면 슬롯당 고유한 GameplayTag를 사용할 수도 있습니다.

GameplayAbility는 실제로 Cooldown 태그의 존재 여부를 확인하지, Cooldown GameplayEffect 자체를 확인하지는 않습니다. 기본적으로 Cooldown GameplayEffect는 예측을 지원해야 하므로 ExecutionCalculation를 사용하지 않는 것이 좋습니다. 대신, 복잡한 Cooldown 계산에는 MMC를 사용하는 것이 허용되며 권장됩니다.

 

처음에는 GameplayAbility마다 고유한 Cooldown GameplayEffect를 설정하게 됩니다. 하지만 더 고급 기법으로는 여러 개의 GameplayAbility에서 하나의 Cooldown GameplayEffect를 재사용할 수 있습니다. 이 경우 Cooldown 시간과 Cooldown 태그는 각 GameplayAbility에서 정의해야 하며, 생성된 GameplayEffectSpec에 해당 데이터를 동적으로 설정합니다. 이 방법은 인스턴스화된 GameplayAbility에서만 작동합니다.

 

Cooldown GameplayEffect를 재사용하는 두 가지 방법:

 

1. SetByCaller를 활용한 방법(가장 쉬운 방법)

공유 Cooldown GameplayEffect(GE)의 Duration을 GameplayTag와 함께 SetByCaller로 설정합니다. GameplayAbility 서브클래스에서 다음을 정의합니다. GameplayAbility 서브클래스에 Duration에 대한 float / FScalableFloat, 고유 Cooldown 태그에 대한 FGameplayTagContainer, Cooldown 태그와 Cooldown GE의 태그를 합친 반환 포인터로 사용할 임시 FGameplayTagContainer를 정의합니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// GetCooldownTags()에서 반환할 포인터를 사용할 임시 컨테이너입니다.
// 이것은CooldownTag와 Cooldown GE의 CoolDown 태그를 합친 값입니다.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

그런 다음, UGameplayAbility::GetCooldownTags()를 오버라이드하여 Cooldown 태그와 기존 Cooldown GameplayEffect의 태그를 합친 값을 반환하도록 합니다.

const FGameplayTagContainer* UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    MutableTags->Reset(); // MutableTags는 CDO의 TempCooldownTags에 기록되므로, GameplayAbility의 Cooldown 태그가 변경될 경우(다른 슬롯으로 이동) 이를 지우기 위해 초기화.
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown()을 오버라이드하여 Cooldown 태그를 주입하고, Cooldown GameplayEffectSpec에 SetByCaller를 추가합니다.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
    UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
    if (CooldownGE)
    {
        FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
        SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
        SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(OurSetByCallerTag)), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
    }
}

이 그림에서 Cooldown의 Duration Modifier는 SetByCaller로 설정되며, Data.Cooldown이라는 데이터 태그를 사용합니다. Data.Cooldown은 위 코드에서 OurSetByCallerTag에 해당합니다.

 

2. MMC를 활용한 방법

해당 방법은 위와 동일한 설정을 사용하지만, Cooldown GE의 지속 시간을 SetByCaller로 설정하는 대신, Duration을 Custom Calculation Class로 설정하고, 새로 만들 MMC를 가리키도록 합니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// GetCooldownTags()에서 반환할 포인터로 사용할 임시 컨테이너입니다.
// CooldownTags와 Cooldown GE의 Cooldown 태그를 합친 값입니다.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

 

그런 다음, UGameplayAbility::GetCooldownTags()를 오버라이드하여 Cooldown 태그와 기존 Cooldown GE의 태그를 합친 값을 반환하도록 합니다.

const FGameplayTagContainer* UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    MutableTags->Reset(); // MutableTags는 CDO의 TempCooldownTags에 기록되므로, Ability의 Cooldown 태그가 변경될 경우(다른 슬롯으로 이동) 이를 지우기 위해 초기화.
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown()을 오버라이드하여 Cooldown 태그를 Cooldown GameplayEffectSpec에 주입합니다.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
    UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
    if (CooldownGE)
    {
        FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
        SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
    }
}
float UPGMMC_HeroAbilityCooldown::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
    const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

    if (!Ability)
    {
        return 0.0f;
    }

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

 

4.5.15.1 Cooldown GameplayEffect의 남은 시간 얻어내기

bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration)
{
    if (AbilitySystemComponent && CooldownTags.Num() > 0)
    {
        TimeRemaining = 0.f;
        CooldownDuration = 0.f;

        FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
        TArray< TPair<float, float> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
        if (DurationAndTimeRemaining.Num() > 0)
        {
            int32 BestIdx = 0;
            float LongestTime = DurationAndTimeRemaining[0].Key;
            for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
            {
                if (DurationAndTimeRemaining[Idx].Key > LongestTime)
                {
                    LongestTime = DurationAndTimeRemaining[Idx].Key;
                    BestIdx = Idx;
                }
            }

            TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
            CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;

            return true;
        }
    }

    return false;
}
💡 NOTE: 클라이언트에서 Cooldown의 남은 시간을 Query(조회)하려면 리플리케이트된 GameplayEffect를 수신할 수 있어야 합니다. 이는 ASC의 리플리케이션 모드에 따라 달라집니다.

 

4.5.15.2 Cooldown 시작 및 종료 청취(Listening)

Cooldown이 시작되는 시점을 수신하려면, AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf에 바인딩하여 Cooldown GE가 적용될 때 응답하거나, AbilitySystemComponent->RegisterGameplayTagEvent(Cooldown 태그, EGameplayTagEventType::NewOrRemoved)에 바인딩하여 Cooldown 태그가 추가될 때 응답할 수 있습니다. Cooldown GE가 언제 추가되었는지 확인하는 것이 좋은데, Cooldown GE를 적용한 GameplayEffectSpec에도 접근할 수 있기 때문입니다. 이를 통해 Cooldown GE가 로컬에서 예측한 것인지 서버에서 수정한 것인지를 확인할 수 있습니다.

Cooldown이 언제 끝나는지 수신하려면, AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate()에 바인딩하여 Cooldown GE가 제거되는 시점에 응답하거나, AbilitySystemComponent->RegisterGameplayTagEvent(Cooldown  태그, EGameplayTagEventType::NewOrRemoved)에 바인딩하여 Cooldown 태그가 제거되는 시점에 응답하면 됩니다. 서버의 수정된 Cooldown GE가 들어오면 로컬에서 예측한 Cooldown제거되어 Cooldown진행 중임에도 불구하고 OnAnyGameplayEffectRemovedDelegate()가 발동되므로 Cooldown 태그가 제거되는 시점을 잘 살펴볼 것을 권장합니다. 예측된 Cooldown GE를 제거하고 서버의 수정된 Cooldown GE를 적용하는 동안 Cooldown 태그는 변경되지 않습니다.

💡NOTE: 클라이언트에서 GameplayEffect가 추가되거나 제거되는 것을 듣기 위해서는 클라이언트가 리플리케이트된 GameplayEffect를 받을 수 있어야 합니다. 이는 해당 클라이언트의 ASC(Ability System Component)의 리플리케이션 모드에 따라 달라집니다.

샘플 프로젝트에는 Cooldown이 시작되고 끝나는 것을 듣는 Costom Blueprint 노드가 포함되어 있습니다. HUD UMG Widget은 이를 사용하여 메테오의 Cooldown의 남은 시간을 업데이트합니다. 해당 AsyncTask는 EndTask()가 수동으로 호출될 때까지 계속 살아 있습니다. UMG Widget의 Destruct 이벤트에서 이를 처리합니다. AsyncTaskCooldownChanged.h/cpp를 참고하세요.

 

4.5.15.3 Cooldown 예측

현재 Cooldown을 실제로 예측할 수 없습니다. 로컬에서 예측된 Cooldown GE가 적용될 때 UI Cooldown 타이머를 시작할 수 있지만, GameplayAbility의 실제 Cooldown은 서버의 Cooldown의 남은 시간에 연결되어 있습니다. 플레이어의 지연 시간에 따라, 로컬에서 예측된 Cooldown이 만료되었을 수 있지만, GameplayAbility는 여전히 서버에서 Cooldown 중일 수 있으며, 이로 인해 서버의 Cooldown이 만료될 때까지 GameplayAbility를 즉시 재활성화할 수 없습니다.

 

샘플 프로젝트에서는 로컬에서 예측된 Cooldown이 시작될 때 메테오 Ability의 UI 아이콘을 회색으로 처리하고, 서버에서 수정된 Cooldown GE가 들어오면 Cooldown 타이머를 시작하는 방식으로 이를 처리합니다.

 

게임 플레이 결과로, 지연 시간이 높은 플레이어는 짧은 Cooldown 능력에 대해 낮은 발사 속도를 가지게 되어 지연 시간이 낮은 플레이어에 비해 불리한 상황에 처하게 됩니다. Fortnite는 이를 피하기 위해 무기들이 Cooldown GameplayEffect를 사용하지 않고 맞춤형 기록 방식을 사용합니다.

 

진정한 예측된 Cooldown을 허용하는(플레이어가 로컬 Cooldown이 만료되었을 때 GameplayAbility를 활성화할 수 있지만 서버에서는 여전히 Cooldown 중인 상태) 에픽 게임즈은 Epic이 향후 GAS의 다음 버전에서 구현하고자 하는 기능입니다.

 

4.5.16 활성화된 GameplayEffect 지속 시간 변경

쿨타임 GE 또는 지속시간 GameplayEffect의 남은 시간을 변경하려면, GameplayEffectSpec의 Duration을 변경하고, StartServerWorldTime을 업데이트하고, CachedStartServerWorldTime을 업데이트하고, StartWorldTime을 업데이트한 다음 CheckDuration()으로 지속시간 검사를 다시 실행해야 합니다. 서버에서 이 작업을 수행하고 FActiveGameplayEffect를 더티로 표시하면 클라이언트에 변경사항이 리플리케이트됩니다.

 

Cooldown GE나 다른 Duration GameplayEffect의 남은 시간을 변경하려면, GameplayEffectSpec의 Duration을 변경하고, StartServerWorldTime, CachedStartServerWorldTime, StartWorldTime을 업데이트한 후, CheckDuration()으로 Duration 검사를 다시 실행해야 합니다. 서버에서 이를 수행하고 FActiveGameplayEffect를 더티 마킹하면, 변경 사항이 클라이언트에 리플리케이트됩니다.

💡NOTE: 이것은 const_cast가 필요하며 에픽 게임즈가 의도한 Duration 변경 방식이 아닐 수도 있지만, 지금까지는 잘 작동하는 것 같습니다.
bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
    if (!Handle.IsValid())
    {
        return false;
    }

    const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
    if (!ActiveGameplayEffect)
    {
        return false;
    }

    FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
    if (NewDuration > 0)
    {
        AGE->Spec.Duration = NewDuration;
    }
    else
    {
        AGE->Spec.Duration = 0.01f;
    }

    AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
    AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
    AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
    ActiveGameplayEffects.MarkItemDirty(*AGE);
    ActiveGameplayEffects.CheckDuration(Handle);

    AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
    OnGameplayEffectDurationChange(*AGE);

    return true;
}

 

4.5.17 런타임에서 GameplayEffect 동적 생성하기

런타임에서 동적 GameplayEffect를 생성하는 것은 고급 주제입니다. 이 작업은 자주 할 필요는 없습니다.

Instant GameplayEffect만 C++에서 런타임에 처음부터 생성할 수 있습니다. Duration과 Infinite GameplayEffect는 런타임에서 동적으로 생성할 수 없습니다. 왜냐하면, 이를 리플리케이트할 때 해당 GameplayEffect 클래스 정의를 찾게 되는데, 이는 존재하지 않기 때문입니다. 이 기능을 구현하려면, 대신 에디터에서 하던 것처럼 Archetype GameplayEffect 클래스를 만들고, 런타임에서 GameplayEffectSpec 인스턴스를 필요한 대로 커스터마이즈하는 방식으로 접근해야 합니다.

런타임에서 생성된 Instant GameplayEffect는 로컬 예측 GameplayAbility 내에서 호출될 수 있습니다. 그러나 동적 생성이 부작용을 일으킬 수 있는지는 아직 불확실합니다.

샘플 프로젝트에서는 캐릭터가 치명적인 타격을 입혔을 때, 그 캐릭터를 처치한 플레이어에게 골드와 경험치를 보내기 위해 GameplayEffect를 생성합니다.

// 보상을 주기 위해 동적 Instant GameplayEffect를 생성
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;

int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);

FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();

FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();

Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());

 

두 번째 예시는 로컬 예측 GameplayAbility 내에서 생성된 런타임 GameplayEffect를 보여줍니다. 코드 내 주석을 참조하여 사용할 때 주의하세요!

void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
    {
        if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
        {
            EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
        }

        // 런타임 중 GE 생성.
        UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
        GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.

        // 간단한 스케일러블 float Modifier를 추가하여 MyAttribute를 42로 덮어씁니다.
        // 실제 애플리케이션에서는 TriggerEventData를 통해 전달된 정보를 소비합니다.
        const int32 Idx = GameplayEffect->Modifiers.Num();
        GameplayEffect->Modifiers.SetNum(Idx + 1);
        FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
        ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
        ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
        ModifierInfo.ModifierOp = EGameplayModOp::Override;

        // GE 적용.

        // 여기서 GESpec을 생성하여 ASC가 GE 클래스 기본 객체에서 GESpec을 생성하는 동작을 피합니다.
        // 동적 GE가 있을 때 기본 GameplayEffect 클래스로 GESpec을 생성하면 Modifier가 손실되기 때문에 이렇게 처리합니다. 
        // 주의: 이 해킹이 문제가 될 수 있는지 불확실합니다!
        // GESpec에서 GE가 UPROPERTY로 참조되므로 GE 객체가 GarbageCollector에 의해 수거되는 것을 방지합니다.
        FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // new, handle 내에서 shared ptr로 수명이 관리되기 때문입니다.
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
    }
    EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}

 

4.5.18 Gameplay Effect Container
에픽 게임즈의 Action RPG 샘플 프로젝트는 FGameplayEffectContainer라는 구조체를 구현합니다. 이는 기본 GAS에는 없지만 GameplayEffect와 TargetData를 담는 데 매우 유용합니다. 이 구조체는 GameplayEffectSpec를 생성하고 기본 값을 설정하는 등의 작업을 자동화합니다. GameplayAbility에서 GameplayEffectContainer를 생성하고 이를 발사된 투사체에 전달하는 것은 매우 쉽고 직관적입니다. 저는 포함된 샘플 프로젝트에서 GameplayEffectContainer를 구현하지 않았는데, 이는 기본 GAS에서 이를 사용하지 않고 작업하는 방법을 보여주기 위함입니다. 하지만 이 구조체를 프로젝트에 추가하는 것을 고려해보는 것이 좋습니다.

GameplayEffectContainer 안의 GESpec에 접근하여 SetByCaller를 추가하는 등의 작업을 하려면, FGameplayEffectContainer를 분해하고 GESpec의 인덱스를 사용하여 GESpec 참조에 접근해야 합니다. 이를 위해서는 액세스하려는 GESpec의 인덱스를 미리 알아야 합니다.

GameplayEffectContainer는 효율적인 타겟팅을 위한 선택적인 수단도 포함하고 있습니다.