본문 바로가기
둥지/Unreal

Unreal GAS(GameplayAbilitySystem) Documentation 번역글 2부

by 까닭 2023. 5. 27.

4.5 Gameplay Effect

4.5.1 Gameplay Effect 정의

GameplayEffect(GE)는 Ability가 자신과 다른 플레이어의 AttributeGameplayTag를 변경하는 통로입니다. 피해나 치유와 같은 즉각적인 Attribute 변화를 일으키거나 이동 속도 부스트나 기절과 같은 장기적인 상태 버프/디버프를 적용할 수 있습니다. UGameplayEffect 클래스는 하나의 GameplayEffect를 정의하는 데이터 전용 클래스입니다. GameplayEffect에 추가 로직을 추가해서는 안 됩니다. 보통 디자이너는 UGameplayEffect의 블루프린트 자식 클래스를 많이 만들게 됩니다.

 

GameplayEffect는 모디파이어와 실행을 통해 Attribute를 변경합니다. (GameplayEffectExecutionCalculation)

 

GameplayEffect에는 세 가지 지속 시간 유형이 있습니다: Instant, Duration, Infinite.

 

또한 GameplayEffectGameplayCue를 추가/실행할 수 있습니다. 인스턴트 GameplayEffectGameplayCue GameplayTag에서 실행을 호출하는 반면, 지속 시간 또는 무한 GameplayEffectGameplayCue GameplayTag에서 추가 및 제거를 호출합니다.

 

기간 유형 GameplayCue 이벤트 사용 시기
Instant  실행 Attribute의 기본값을 즉시 영구적으로 변경합니다. GameplayTag는 한 프레임에도 적용되지 않습니다.
Duration  추가 & 삭제 Attribute의 현재값을 임시로 변경하고 GameplayEffect가 만료되거나 수동으로 제거될 때 제거될 GameplayTag를 적용하는 데 사용됩니다. 지속 시간은 UGameplayEffect 클래스/Blueprint에서 지정됩니다.
Infinite 추가 & 삭제 Attribute의 현재값을 임시로 변경하고 GameplayEffect가 제거될 때 제거될 GameplayTag를 적용하는 데 사용됩니다. 이 태그는 저절로 만료되지 않으며 어빌리티 또는 ASC에서 수동으로 제거해야 합니다.

Duration과 Infinite GameplayEffect에는 해당 기간에 정의된 대로 특정 초마다 해당 모디파이어와 실행을 적용하는 주기적 이펙트를 적용할 수 있는 옵션이 있습니다. 주기적 이펙트는 Attribute의 기본값을 변경하고 GameplayCue를 실행할 때 인스턴트 GameplayEffect로 취급됩니다. 이는 시간에 따른 피해(DOT) 유형 이펙트에 유용합니다.

💡 NOTE: 주기적 Effect는 예측할 수 없습니다.

Duration과 Infinite GameplayEffect는 적용 후 진행 중인 태그 요건(Gameplay Effect Tag)이 충족되지 않으면 일시적으로 끄고 켤 수 있습니다. GameplayEffect를 끄면 해당 모디파이어와 적용된 GameplayTag의 이펙트는 제거되지만 GameplayEffect는 제거되지 않습니다. GameplayEffect를 다시 켜면 해당 모디파이어와 GameplayTag가 다시 적용됩니다.

 

지속시간 또는 무한 GameplayEffect의 모디파이어를 수동으로 다시 계산해야 하는 경우 (Attribute에서 가져오지 않는 데이터를 사용하는 MMC가 있다고 가정할 때), UAbilitySystemComponent::ActiveGameplayEffect를 호출하면 됩니다. UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()을 사용하여 이미 가지고 있는 레벨과 동일한 레벨로 SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)을 호출할 수 있습니다. 백킹 Attribute를 기반으로 하는 모디파이어는 해당 백킹 Attribute가 업데이트되면 자동으로 업데이트됩니다. 모디파이어를 업데이트하는 SetActiveGameplayEffectLevel()의 주요 함수는 다음과 같습니다:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// private 함수를 사용하지 않으면 레벨을 기존 수준으로 설정할 필요 없이 이 세 함수를 호출할 수 있습니다.
UpdateAllAggregatorModMagnitudes(Effect);

 

GameplayEffect는 일반적으로 인스턴스화되지 않습니다. Ability 또는 ASC가 GameplayEffect를 적용하고자 할 때 GameplayEffect의 ClassDefaultObject에서 GameworkEffectSpec을 생성합니다. 성공적으로 적용된 GameworkEffectSpec은 FAactiveGameplayEffect라는 새로운 구조체에 추가되며, 해당 구조체는 ASC가 ActiveGameplayEffect라는 특수한 컨테이너 구조체에서 추적합니다.

 

4.5.2 Gameplay Effect 적용

GameplayEffectGameplayAbility의 함수와 ASC의 함수에서 다양한 방식으로 적용할 수 있으며, 보통 ApplyGameplayEffectTo의 형태를 취합니다. 다른 함수는 본질적으로 타겟에서 UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()를 호출하는 편의 함수입니다.

 

예를 들어, 발사체에서 GameplayEffect를 GameplayAbility 외부에 적용하려면 타겟의 ASC를 가져와서 그 함수 중 하나를 사용하여 ApplyGameplayEffectToSelf에 적용해야 합니다.

 

델리게이트에 바인딩하여 ASC에 Duration 혹은 Infinite GameplayEffect가 적용될 때를 수신할 수 있습니다:

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 삭제

GameplayEffectGameplayAbility의 함수와 ASC의 함수에서 다양한 방식으로 제거할 수 있으며, 보통 RemoveActiveGameplayEffect의 형태를 취합니다. 다른 함수는 본질적으로 타겟에서 FActiveGameplayEffectsContainer::RemoveActiveEffects()를 호출하는 편의 함수입니다.

 

GameplayAbility 외부에서 GameplayEffect를 제거하려면 타겟의 ASC를 가져와서 그 함수 중 하나를 사용하여 RemoveActiveGameplayEffect를 호출해야 합니다.

 

델리게이트에 바인딩하여 ASC에서 Duration 혹은 Infinite GameplayEffect가 제거되는 시점을 수신할 수 있습니다:

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

콜백 함수입니다:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

서버는 리플리케이션 모드에 관계없이 항상 이 함수를 호출합니다. 자율 프록시는 전체 및 혼합 리플리케이션 모드에서 리플리케이트된 GameplayEffect에 대해서만 이 함수를 호출합니다. 시뮬레이션 프록시는 전체 리플리케이션 모드에서만 이 함수를 호출합니다.

 

 

4.5.4 Gameplay Effect 모디파이어

모디파이어는 Attribute를 변경하며, Attribute를 예측적으로 변경할 수 있는 유일한 방법입니다. GameplayEffect는 모디파이어를 0개 또는 여러 개 가질 수 있습니다. 각 모디파이어는 지정된 작업을 통해 하나의 Attribute만 변경할 수 있습니다.

동작 내용
Add 모디파이어의 지정된 특성에 결과를 추가합니다. 빼기에는 음수 값을 사용합니다.
Multiply 결과를 모디파이어의 지정된 Attribute으로 곱합니다.
Divide 모디파이어의 지정된 Attribute에 대해 결과를 나눕니다.
Override 모디파이어의 지정된 Attribute을 결과로 재정의합니다.

Attribute의 현재값은 기본값에 추가된 모든 모디파이어의 집계 결과입니다. 모디파이어가 집계되는 방식에 대한 공식은 GameplayEffectAggregator.cpp의 FAggregatorModChannel::EvaluateWithBase에 다음과 같이 정의되어 있습니다:

 

((InlineBaseValue + Additive) * Multiplicitive) / Division

 

모든 재정의 모디파이어는 최종값을 재정의하며 마지막으로 적용된 모디파이어가 우선합니다.

💡 NOTE: 백분율 기반 변경 사항의 경우 곱셈 작업을 사용하여 더하기 후에 변경이 이루어지도록 해야 합니다
💡 NOTE: 예측은 백분율 변경에 문제가 있습니다.

모디파이어에는 네 가지 유형이 있습니다: Scalable Float, Attribute Based, Custom Calculation Class, Set By Caller입니다. 이들 모두는 연산에 따라 모디파이어의 지정된 Attribute를 변경하는 데 사용되는 일부 실수 값을 생성합니다.

모디파이어 유형 내용
Scalable Float FScalableFloat는 변수를 행으로, 레벨을 열로 가진 데이터 테이블을 가리킬 수 있는 구조체입니다. Scalable Float는 지정된 테이블 행의 값을 어빌리티의 현재 레벨(또는 GameplayEffectSpec에서 재정의된 경우 다른 레벨)에서 자동으로 읽습니다. 이 값은 계수를 통해 추가로 조작할 수 있습니다. 데이터 테이블/행이 지정되지 않은 경우, 값을 1로 취급하므로 계수를 사용하여 모든 레벨에서 단일 값으로 하드코딩할 수 있습니다.

Attribute Based Attribute 기반 모디파이어는 소스(GameplayEffectSpec을 생성한) 또는 타겟(GameplayEffectSpec을 수신한)에 있는 지원 Attribute의 CurrentValue 또는 BaseValue를 받아 계수와 사전 및 사후 계수 추가를 통해 추가로 수정합니다. 스냅샷은 GameplayEffectSpec이 생성될 때 지원 Attribute가 캡처되는 반면, 스냅샷이 없으면 GameplayEffectSpec이 적용될 때 Attribute가 캡처됩니다.
Custom Calculation Class Custom Calculation 클래스는 복잡한 모디파이어에 가장 큰 유연성을 제공합니다. 이 모디파이어는 ModifierMagnitudeCalculation 클래스를 취하며 계수와 사전 및 사후 계수 추가를 통해 결과 부동 소수점 값을 추가로 조작할 수 있습니다.
Set By Caller SetByCaller 모디파이어는 실행 시간에 어빌리티 또는 GameplayEffectSpec에서 GameplayEffectSpec을 만든 사람이 GameplayEffect 외부에 설정하는 값입니다. 예를 들어, 플레이어가 어빌리티를 충전하기 위해 버튼을 누른 시간에 따라 데미지를 설정하려는 경우 SetByCaller를 사용할 수 있습니다. SetByCaller는 본질적으로 GameplayEffectSpec에 있는 TMap<FGameplayTag, float> 입니다. 모디파이어는 Aggregator에 제공된 GameplayTag와 연관된 SetByCaller 값을 찾으라고 지시하는 것뿐입니다. 모디파이어가 사용하는 SetByCaller는 GameplayTag 버전 개념만 사용할 수 있습니다. 여기서 FName 버전은 비활성화됩니다. 모디파이어가 SetByCaller로 설정되어 있지만 GameplayEffectSpec에 올바른 GameplayTag를 가진 SetByCaller가 존재하지 않는 경우, 게임은 런타임 오류를 발생시키고 0 값을 반환합니다. 이는 분할 작업의 경우 문제를 일으킬 수 있습니다. SetByCaller 사용 방법에 대한 자세한 내용은 SetByCaller를 참조하십시오.

 

4.5.4.1 곱셈 및 나눗셈 모디파이어

기본적으로 모든 곱셈 및 나눗셈 모디파이어는 Attribute 기본값으로 곱하거나 나누기 전에 함께 추가됩니다.

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

 

곱셈 및 나눗셈 모디파이어 모두 이 공식에서 치우침 값이 1입니다.(추가는 치우침이 0입니다)

따라서 다음과 같이 표시됩니다:

 

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

 

이 공식은 예상치 못한 결과를 초래합니다. 첫째, 이 수식은 모든 모디파이어를 더한 다음 기본값에 곱하거나 나누기 전에 모든 모디파이어를 합칩니다. 대부분의 사람들은 이 수식이 수식들을 곱하거나 나눌 것이라고 예상할 것입니다. 예를 들어 1.5의 Multiply 모디파이가 두 개 있는 경우 대부분의 사람들은 기본값에 1.5 x 1.5 = 2.25를 곱할 것으로 예상합니다. 대신 1.5초를 합산하여 기본값에 2를 곱합니다. (50% 증가 + 또 다른 50% 증가 = 100% 증가) 기본 속도 500에 10% 속도 버프를 적용하면 550이 되는 GameplayPrediction.h의 예시입니다. 10% 속도 버프를 하나 더 추가하면 600이 됩니다.

 

둘째, 이 공식은 파라곤을 염두에 두고 설계되었기 때문에 사용할 수 있는 값에 대한 문서화되지 않은 규칙이 몇 가지 있습니다.

 

곱하기와 나누기 곱셈 덧셈 공식에 대한 규칙:

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

수식의 바이어스는 기본적으로 [1, 2] 범위에 있는 숫자의 정수 자릿수를 뺍니다. 첫 번째 모디파이어의 Bias는 시작 Sum 값(루프 전에 Bias로 설정됨)에서 빼기 때문에 어떤 값이든 그 자체로 작동하며, [1, 2] 범위의 숫자에 대해 1 미만의 값이 작동하는 이유이기도 합니다.

 

곱하기의 몇 가지 예시:

승수: 0.5

1 + (0.5 - 1) = 0.5, correct

 

승수: 0.5, 0.5

1 + (0.5 - 1) + (0.5 - 1) = 0

혹시 1을 예상하셨나요? 1보다 작은 여러 값은 승수를 더하는 데 적합하지 않습니다. 파라곤은 곱하기 모디파이어에 가장 큰 음수값만 사용하도록 설계되었기 때문에 1보다 작은 값은 최대 하나만 기본값에 곱할 수 있습니다.

 

승수: 1.1, 0.5

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

 

승수: 5, 5

1 + (5 - 1) + (5 - 1) = 9

혹시 10을 예상하셨나요? 항상 모디파이어의 합계 - 모디파이어 수 + 1이 됩니다.

 

많은 게임에서 곱하기 및 나누기 모디파이어가 기본값에 적용하기 전에 함께 곱하고 나누기를 원할 것입니다. 이를 위해서는 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 모디파이어의 GameplayTag

소스 태그와 타겟 태그는 각 모디파이어에 대해 설정할 수 있습니다. GameplayEffect의 애플리케이션 태그 요건과 동일하게 작동합니다. 따라서 태그는 이펙트가 적용될 때만 고려됩니다. 즉, 주기적인 무한 이펙트가 있는 경우 이펙트가 처음 적용될 때만 고려되고 주기적으로 실행될 때마다 고려되지 않습니다.

 

Attribute 기반 모디파이어는 SourceTagFilter 및 TargetTagFilter도 설정할 수 있습니다. Attribute 기반 모디파이어의 소스인 속성의 크기를 결정할 때 이러한 필터를 사용하여 해당 속성에 대한 특정 모디파이어를 제외할 수 있습니다. 소스 또는 대상에 필터의 모든 태그가 없는 모디파이어는 제외됩니다.

 

자세히 설명합니다: 소스 ASC와 타깃 ASC의 태그는 GameplayEffect에 의해 캡처됩니다. 소스 ASC 태그는 GameplayEffectSpec이 생성될 때 캡처되고, 이펙트가 실행될 때 타겟 ASC 태그가 캡처됩니다. infinite 또는 duration Effect의 모디파이어가 적용될 "자격"이 있고(즉, 해당 Aggregator가 자격이 있음) 해당 필터가 설정되어 있는지 확인할 때 캡처된 태그가 필터와 비교됩니다.

 

4.5.5 Gameplay Effect Stacking(중첩)

기본적으로 GameplayEffect는 애플리케이션의 기존 GameplayEffectSpec 인스턴스를 모르거나 신경쓰지 않는 새 GameplayEffectSpec 인스턴스를 적용합니다. GameplayEffect는 GameplayEffectSpec의 새 인스턴스가 추가되는 대신 현재 존재하는 GameplayEffectSpec의 스택 수가 변경되도록 스택을 설정할 수 있습니다. 스택은 지속시간 및 무한 GameplayEffect에 대해서만 작동합니다.

 

Stacking에는 두 가지 유형이 있습니다: 소스별 집계와 대상별 집계입니다.

Stacking Type 내용
Aggregate by Source 타겟의 소스 ASC 당 별도의 스택 인스턴스가 있습니다. 각 소스는 X만큼의 스택을 적용할 수 있습니다.
Aggregate by Target 소스에 관계없이 타겟에는 스택 인스턴스가 하나만 있습니다. 각 소스는 공유 스택 한도까지 스택을 적용할 수 있습니다.

스택에는 만료, 기간 새로고침, 기간 재설정에 대한 정책(Policy)도 있습니다. GameplayEffect 블루프린트에는 유용한 호버 툴팁이 있습니다.

 

샘플 프로젝트에는 GameplayEffectSpec 변경을 수신하는 커스텀 블루프린트 노드가 포함되어 있습니다. HUD UMG 위젯은 이를 사용하여 플레이어가 보유한 패시브 방어구 스택의 양을 업데이트합니다. 이 AsyncTask는 UMG 위젯의 Destruct 이벤트에서 수동으로 EndTask()를 호출할 때까지 영원히 살아있을 것입니다. AsyncTaskEffectStackChanged.h/cpp 를 참고하세요.

 

4.5.6 어빌리티 부여

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

 

일반적인 사용 사례는 다른 플레이어를 밀치거나 당겨서 이동시키는 것과 같은 동작을 강제로 수행하려는 경우입니다. 플레이어에게 원하는 동작을 하는 자동 활성화 어빌리티를 부여하는 GameplayEffect를 적용하면 됩니다. (어빌리티가 부여되면 동으로 활성화하는 방법은 패시브 어빌리티를 참조하세요)

디자이너는 GameplayEffect가 부여하는 어빌리티, 부여할 레벨, 바인딩할 입력, 부여된 어빌리티의 제거 정책(Removal Policy)을 선택할 수 있습니다.

Removal Policy 내용
Cancel Ability Immediately 부여된 어빌리티는 해당 어빌리티를 부여한 GameplayEffect가 대상에서 제거되면 즉시 취소되고 제거됩니다.
Remove Ability on End 부여된 어빌리티는 완료될 때까지 허용된 다음 대상에서 제거됩니다.
Do Nothing 부여된 어빌리티는 대상에서 부여된 GameplayEffect를 제거해도 영향을 받지 않습니다. 대상은 나중에 수동으로 제거할 때까지 영구적으로 능력을 보유합니다.

 

4.5.7 Gameplay Effect Tag

GameplayEffect는 여러 GameplayTagContainer를 포함합니다. 디자이너는 각 카테고리에 대해 추가된 GameplayTagContainer와 제거된 GameplayTagContainer를 편집하고, 그 결과는 컴파일 시 결합된 GameplayTagContainer에 표시됩니다. 추가된 태그는 이 GameplayEffect가 부모 클래스에 없던 새 태그를 추가하는 것입니다. 제거된 태그는 부모 클래스에는 있지만 이 서브클래스에는 없는 태그입니다.

카테고리 내용
Gameplay Effect Asset Tags GameplayEffect에 있는 태그입니다. 자체적으로 어떤 기능도 수행하지 않으며 GameplayEffect를 설명하는 용도로만 사용됩니다.
Granted Tags GameplayEffect에 존재하지만 GameplayEffect가 적용되는 ASC에도 부여되는 태그입니다. GameplayEffect가 제거되면 ASC에서 제거됩니다. 이는 지속 시간 및 무한 GameplayEffect에 대해서만 작동합니다.
Ongoing Tag Requirements 이 태그가 적용되면 GameplayEffect가 켜져 있는지 꺼져 있는지를 결정합니다. GameplayEffect는 꺼져 있어도 여전히 적용될 수 있습니다. 진행 중 태그 요건을 충족하지 못해 GameplayEffect가 꺼져 있지만 요건이 충족되면 GameplayEffect가 다시 켜지고 해당 모디파이가 다시 적용됩니다. 이는 지속시간 및 무한 GameplayEffect에 대해서만 작동합니다.
Application Tag Requirements GameplayEffect를 대상에 적용할 수 있는지 여부를 결정하는 대상의 태그입니다. 이러한 요구 사항이 충족되지 않으면 GameplayEffect가 적용되지 않습니다.
Remove Gameplay Effects with Tags 에셋 태그 또는 부여된 태그에 이러한 태그가 있는 대상의 GameplayEffect는 이 GameplayEffect가 성공적으로 적용되면 대상에서 제거됩니다.

 

4.5.8 면역

GameplayEffect는 GameplayTag를 기반으로 다른 GameplayEffect의 적용을 효과적으로 차단하는 면역을 부여할 수 있습니다. 면역은 애플리케이션 태그 요구사항과 같은 다른 수단을 통해서도 효과적으로 달성할 수 있지만, 이 시스템을 사용하면 면역으로 인해 GameplayEffect가 차단될 때를 위한 델리게이트 UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate를 제공합니다.

GrantedApplicationImmunityTags는 소스 ASC(소스 어빌리티의 AbilityTags에 태그가 있는 경우 그 태그 포함)에 지정된 태그가 있는지 확인합니다. 태그를 기반으로 특정 캐릭터 또는 소스의 모든 GameplayEffect에 대한 면역을 부여하는 방법입니다.

부여된 애플리케이션 면역 쿼리는 들어오는 GameplayEffectSpec이 쿼리 중 하나라도 일치하는지 확인하여 해당 애플리케이션을 차단하거나 허용합니다.

쿼리에는 GameplayEffect 블루프린트에 유용한 호버 툴팁이 있습니다.

 

4.5.9 Gameplay Effect Spec

GameplayEffectSpec(GESpec)은 GameplayEffect의 인스턴스라고 생각하면 됩니다. 이 인스턴스에는 해당 인스턴스가 나타내는 GameplayEffect 클래스, 생성된 레벨, 생성자가 누구인지에 대한 레퍼런스가 들어 있습니다. 디자이너가 런타임 전에 생성해야 하는 GameplayEffect와는 달리 런타임 전에 자유롭게 생성하고 수정할 수 있습니다. GameplayEffect를 적용할 때, GameplayEffect로부터 GameplayEffectSpec이 생성되고 이것이 실제로 타겟에 적용됩니다.

GameplayEffectSpec은 블루프린트 콜러블인 UAbilitySystemComponent::MakeOutgoingSpec()을 사용하여 GameplayEffect로부터 생성됩니다. GameplayEffectSpec을 즉시 적용할 필요는 없습니다. 어빌리티에서 생성된 프로젝타일에 GameplayEffectSpec을 전달하여 나중에 그 프로젝타일이 타격하는 타깃에 적용할 수 있도록 하는 것이 일반적입니다. GameplayEffectSpec가 성공적으로 적용되면 FActiveGameplayEffect라는 새 구조체를 반환합니다.

 

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

  • GameplayEffect가 생성된 GameplayEffect 클래스입니다.
  • GameplayEffectSpec의 레벨입니다. 보통 GameplayEffectSpec을 생성한 어빌리티의 레벨과 같지만 다를 수 있습니다.
  • GameplayEffectSpec의 지속시간입니다. 기본값은 GameplayEffect 의 지속시간이지만 다를 수 있습니다.
  • 주기적 이펙트의 GameplayEffectSpec 기간입니다. 기본값은 GameplayEffect의 기간이지만 다를 수 있습니다.
  • GameplayEffectSpec의 현재 스택 수입니다. 스택 제한은 GameplayEffect에 있습니다.
    GameplayEffectContextHandle](<https://github.com/tranek/GASDocumentation/tree/master#concepts-ge-context>)은 누가 이 GameplayEffectSpec을 생성했는지 알려줍니다.
  • GameplayEffectSpec 생성 시 스냅샷으로 인해 캡처된 어트리뷰트입니다.
  • GameplayEffect가 부여하는 GameplayTag 외에 GameplayEffect가 타겟에 부여하는 DynamicGrantedTags 입니다.
  • GameplayEffect가 가진 애셋 태그 외에 GameplayEffectSpec이 가진 동적 애셋 태그입니다.
  • SetByCaller TMap.

 

4.5.9.1 SetByCaller

SetByCaller를 사용하면 GameplayEffectSpec에 GameplayTag 또는 FName 과 연관된 float 값을 전달할 수 있습니다. 그 값은 각각의 TMap에 저장됩니다: TMap<FGameplayTag, float> 및 TMap<FName, float>에 저장됩니다. 이들은 GameplayEffect의 모디파이어로 사용하거나 float를 이리저리 옮기는 일반적인 수단으로 사용할 수 있습니다. 어빌리티 내부에서 생성된 수치 데이터는 SetByCaller를 통해 GameplayEffectExecutionCalculation 또는 ModifierMagnitudeCalculation에 전달하는 것이 일반적입니다.

SetByCaller  사용 방
Modifiers GameplayEffect 클래스에서 미리 정의해야 합니다. GameplayTag 버전만 사용할 수 있습니다. GameplayEffect 클래스에 정의되어 있지만 GameplayEffectSpec에 해당 태그와 실수 값 쌍이 없는 경우, 게임에서 GameplayEffectSpec을 적용할 때 런타임 오류가 발생하고 0을 반환합니다. 이는 나누기 연산에서 발생할 수 있는 문제입니다. https://github.com/tranek/GASDocumentation#concepts-ge-mods을 참조하십시오.
Elsewhere 어디에도 미리 정의할 필요가 없습니다. GameplayEffectSpec에 존재하지 않는 SetByCaller를 읽으면 선택적 경고와 함께 개발자가 정의한 기본값을 반환할 수 있습니다.

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

블루프린트에서 SetByCaller 값을 읽으려면, 블루프린트 라이브러리에서 커스텀 노드를 만들어야 합니다.
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;

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

 

 

4.5.10 Gameplay Effect Context

GameplayEffectContext 구조체에는 GameplayEffectSpec의 instigator와 타깃 데이터에 대한 정보가 들어있습니다. 이 구조체는 ModifierMagnitudeCalculation / GameplayEffectExecutionCalculation, AttributeSet, GameplayCue 같은 곳에 임의의 데이터를 전달하기 위한 서브클래스로도 좋은 구조체입니다.

 

GameplayEffectContext를 서브클래싱합니다:

  1. 서브클래스 FGameplayEffectContext
  2. Override FGameplayEffectContext::GetScriptStruct()
  3. FGameplayEffectContext::GetScriptStruct() 재정의
  4. 새 데이터를 리플리케이트해야 하는 경우 FGameplayEffectContext::NetSerialize() 를 오버라이드합니다.
  5. 부모 구조체 FGameplayEffectContext 에 있는 것처럼 서브클래스에 TStructOpsTypeTraits 를 구현합니다.
  6. AbilitySystemGlobals 클래스에서 AllocGameplayEffectContext()를 오버라이드하여 서브클래스의 새 오브젝트를 반환합니다.

GASShooter는 서브클래싱된 GameplayEffectContext를 사용하여 GameplayCue에서 액세스할 수 있는 타겟 데이터를 추가하는데, 특히 산탄총은 적을 두 명 이상 맞출 수 있기 때문입니다.

 

4.5.11 Modifier Magnitude Calculation

ModifierMagnitudeCalculation(ModMagCalc 또는 MMC)는 ameplayEffect에서 모디파이어로 사용되는 강력한 클래스입니다. GameplayEffectExecutionCalculation와 비슷하게 작동하지만 덜 강력하며 가장 중요한 것은 예측할 수 있다는 점입니다. 이 함수의 유일한 목적은 CalculateBaseMagnitude_Implementation()에서 실수 값을 반환하는 것입니다. 블루프린트와 C++에서 이 함수를 서브클래싱하고 오버라이드할 수 있습니다.

 

MMC는 인스턴트, 지속시간, 무한, 주기적 등 모든 GameplayEffect의 지속시간에 사용할 수 있습니다.

MMC의 강점은 GameplayTag와 SetByCaller를 읽기 위해 GameplayEffectSpec에 대한 전체 액세스 권한으로 GameplayEffect의 소스 또는 타깃에서 원하는 수의 어트리뷰트 값을 캡처할 수 있다는 점입니다. 어트리뷰트는 스냅샷할 수도 있고 아닐 수도 있습니다. 스냅샷 어트리뷰트는 GameplayEffectSpec이 생성될 때 캡처되는 반면, 스냅샷이 아닌 어트리뷰트는 GameplayEffectSpec이 적용될 때 캡처되어 무한 및 지속시간 GameplayEffect에 대해 어트리뷰트가 변경되면 자동으로 업데이트됩니다. 어트리뷰트 캡처는 ASC 의 기존 모드로부터 CurrentValue 를 재계산합니다. 이 재계산은 어빌리티 세트에서 PreAttributeChange()를 실행하지 않으므로 여기서 클램핑을 다시 해야 합니다.

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

MMC의 결과 float는 GameplayEffect의 모디파이어에서 계수와 사전 및 사후 계수 추가를 통해 추가로 수정할 수 있습니다.

예를 들어 대상의 마나 속성을 캡처하는 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;
}

 

4.5.12 Gameplay Effect Execution Calculation

GameplayEffect Execution Calculation(실행 계산. 플러그인 소스 코드에서 이 용어를 자주 볼 수 있음.)은 GameplayEffect가 ASC를 변경할 수 있는 가장 강력한 방식입니다. ModifierMagnitudeCalculations와 마찬가지로 어트리뷰트를 캡처하고 선택적으로 스냅샷을 찍을 수 있습니다. MMC와 달리 둘 이상의 어트리뷰트를 변경할 수 있으며 기본적으로 프로그래머가 원하는 모든 것을 할 수 있습니다. 이러한 강력한 성능과 유연성의 단점은 예측할 수 없으며 C++로 구현해야 한다는 것입니다.

ExecutionCalculation는 인스턴트 및 주기적 GameplayEffect와만 사용할 수 있습니다. '실행'이라는 단어가 들어간 것은 일반적으로 이 두 가지 유형의 GameplayEffect를 나타냅니다.

스냅샷을 하면 GameplayEffectSpec이 생성될 때 어트리뷰트를 캡처하는 반면, 스냅샷을 하지 않으면 GameplayEffectSpec이 적용될 때 어트리뷰트를 캡처합니다. 어트리뷰트를 캡처하면 ASC 의 기존 모드에서 CurrentValue 를 재계산합니다. 이 재계산은 어빌리티 세트에서 PreAttributeChange()를 실행하지 않으므로 여기서 클램핑을 다시 수행해야 합니다.

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

어트리뷰트 캡처를 설정하려면, 에픽의 ActionRPG 샘플 프로젝트에서 제시하는 패턴을 따라 어트리뷰트를 보유하는 구조체를 정의하고 캡처 방법을 정의한 다음 구조체의 생성자에서 그 사본 하나를 생성합니다. 모든 ExecCalc에 대해 이와 같은 구조체를 갖게 됩니다. 참고: 각 구조체는 동일한 네임스페이스를 공유하므로 고유한 이름이 필요합니다. 구조체에 동일한 이름을 사용하면 속성 캡처 시 잘못된 동작이 발생할 수 있습니다. (대부분 잘못된 속성 값을 캡처)

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

소스 및 타깃의 여러 어트리뷰트에서 읽은 복잡한 수식을 기반으로 받는 대미지를 계산하는 것이 ExecCalc의 가장 일반적인 예입니다. 포함된 샘플 프로젝트에는 GameplayEffectSpec의 SetByCaller에서 대미지 값을 읽은 다음 타깃에서 캡처한 방어구 어트리뷰트를 기반으로 그 값을 완화하는 간단한 대미지 계산용 ExecCalc 가 있습니다. GDDamageExecCalculation.cpp/.h를 참조하세요.

 

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

속성을 캡처하는 것 외에도 몇 가지 방법으로 데이터를 실행 계산으로 전송할 수 있습니다.

 

4.5.12.1.1 SetByCaller

GameplayEffectSpec에 설정된 모든 SetByCaller는 실행 계산에서 직접 읽을 수 있습니다.

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에 값을 하드코딩하려면, 캡처한 어트리뷰트 중 하나를 백업 데이터로 사용하는 CalculationModifier를 사용하여 값을 전달하면 됩니다.

이 스크린샷 예제에서는 캡처한 대미지 어트리뷰트에 50을 추가하고 있습니다. 오버라이드로 설정하여 하드코딩된 값만 가져오도록 할 수도 있습니다.

실행 계산은 어트리뷰트를 캡처할 때 이 값을 읽습니다.

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

GameplayEffect에 값을 하드코딩하려면, C++에서 호출되는 임시 변수 또는 트랜지언트 Aggregator를 사용하는 계산 모디파이어를 사용하여 값을 전달하면 됩니다. 임시 변수는 GameplayTag에 연결됩니다.

 

이 스크린샷 예제에서는 Data.Damage GameplayTag를 사용하여 임시 변수에 50을 추가하고 있습니다.

백업 임시 변수를 ExecutionCalculation의 생성자에 추가합니다:

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

실행 계산은 어트리뷰트 캡처 함수와 유사한 특수 캡처 함수를 사용하여 이 값을 읽습니다.

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

 

4.5.12.1.4 Gameplay Effect Context

GameplayEffectSpec의 커스텀 GameplayEffectContext를 통해 ExecutionCalculation 에 데이터를 전송할 수 있습니다. 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());

실행 계산에서 GameplayEffectSpec을 수정할 때는 주의하세요. 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

Custom Application Requirement 리퀘이션(CAR) 클래스는 디자이너가 GameplayEffect에 대한 단순한 GameplayTag 검사에 비해 GameplayEffect의 적용 여부를 고급 제어할 수 있도록 해줍니다. 블루프린트에서는 CanApplyGameplayEffect()를 오버라이드하여 구현할 수 있고, C++ 에서는 CanApplyGameplayEffect_Implementation() 를 오버라이드하여 구현할 수 있습니다.

CAR 사용 시기의 예시입니다:

  • Target에 특정 양의 속성이 있어야 합니다.
  • Target에 특정 수의 GameplayEffectSpec이 있어야 합니다.

CAR은 또한 이 GameplayEffect의 인스턴스가 이미 타겟에 있는지 확인하고 새 인스턴스를 적용하는 대신 기존 인스턴스의 지속 시간을 변경하는 등의 고급 작업을 수행할 수 있습니다(CanApplyGameplayEffect()의 경우 false 반환).

 

4.5.14 Cost Gameplay Effect

GameplayAbility에는 어빌리티의 비용으로 사용하도록 특별히 설계된 선택적 GameplayEffect가 있습니다. 비용은 GameplayAbility를 활성화하기 위해 ASC가 보유해야 하는 어트리뷰트의 양입니다. GA가 비용 GE를 감당할 수 없으면 활성화할 수 없습니다. 이 비용 GE는 어트리뷰트에서 차감하는 하나 이상의 모디파이어가 있는 인스턴트 GameplayEffect여야 합니다. 기본적으로 비용 GE는 예측하도록 되어 있으며, 이 기능을 유지하려면 ExecutionCalculations를 사용하지 않는 것이 좋습니다. 복잡한 비용 계산에는 MMC를 사용할 수 있으며 권장됩니다.

처음 시작할 때는 비용이 있는 고유한 비용 GE가 GA당 하나씩 있을 가능성이 높습니다. 좀 더 고급 기법은 여러 GA에 하나의 Cost GE를 재사용하고 Cost GE에서 생성된 GameplayEffectSpec을 GA별 데이터로 수정하는 것입니다(비용 값은 GA에 정의됨). 이 방법은 인스턴스화된 어빌리티에서만 작동합니다.

Cost GE를 재사용하는 두 가지 기법입니다:

MMC를 사용합니다. 가장 쉬운 방법입니다. GameplayEffectSpec에서 얻을 수 있는 GameplayAbility 인스턴스에서 비용 값을 읽는 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());
}

이 예제에서 비용 값은 제가 추가한 GameplayAbility 자식 클래스의 FScalableFloat입니다.

 

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

UGameplayAbility::GetCostGameplayEffect()를 재정의합니다. 이 함수를 재정의하고 런타임에 GameplayAbility의 비용 값을 읽는 GameplayEffect를 생성합니다.

 

4.5.15 Gameplay Effect 쿨타임 

GameplayAbility에는 어빌리티의 재사용 대기시간으로 사용하도록 특별히 설계된 선택적 GameplayEffect가 있습니다. 재사용 대기시간은 어빌리티가 활성화된 후 다시 활성화될 수 있는 시간을 결정합니다. GA가 아직 재사용 대기시간 중이면 활성화할 수 없습니다. 이 쿨타임 GE는 모디파이어가 없는 지속시간  GameplayEffect여야 하며, GameplayEffect의 부여된 태그("쿨타임 태그")에 GameplayAbility 또는 어빌리티 슬롯별(게임에 쿨타임을 공유하는 슬롯에 교환 가능한 어빌리티가 할당된 경우) 고유한 GameplayTag가 있어야 합니다. GA는 실제로 쿨타임 GE가 아닌 쿨타임 태그의 존재 여부를 확인합니다. 기본적으로 쿨타임 GE는 예측하도록 되어 있으며, 그 기능을 유지하려면 ExecutionCalculations 를 사용하지 않는 것이 좋습니다. 복잡한 재사용 대기시간 계산에는 MMC를 사용해도 무방하며 권장합니다.

처음 시작할 때는 재사용 대기시간이 있는 고유한 재사용 대기시간 GE가 GA당 하나씩 있을 가능성이 높습니다. 좀 더 고급 기법은 여러 GA 에 하나의 쿨타임 GE 를 재사용하고, 쿨타임 GE 에서 생성된 GameplayEffectSpec 을 GA 전용 데이터(쿨타임 지속시간과 쿨타임 태그는 GA 에 정의되어 있음)로 수정하는 것입니다. 이 방법은 인스턴스 어빌리티에만 적용됩니다.

 

재사용 대기시간 GE를 재사용하는 두 가지 기술:

 

1. SetByCaller를 사용합니다. 가장 쉬운 방법입니다. 공유 재사용 대기시간 GE의 지속시간을 GameplayTag 로 SetByCaller 로 설정합니다. GameplayAbility 서브클래스에 지속시간에 대한 float / FScalableFloat, 고유 쿨타임 태그에 대한 FGameplayTagContainer, 쿨타임 태그와 쿨타임 GE의 태그를 합친 반환 포인터로 사용할 임시 FGameplayTagContainer를 정의합니다.

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

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

// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

그런 다음 UGameplayAbility::GetCooldownTags()를 재정의하여 쿨타임 태그와 기존 쿨타임 GE 태그의 합을 반환합니다.

const FGameplayTagContainer* UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    MutableTags->Reset(); // MutableTags writes to the TempCooldownTags on the CDO so clear it in case the ability cooldown tags change (moved to a different slot)
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown()을 오버라이드하여 쿨타임 태그를 주입하고 쿨타임 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);
    }
}

이 그림에서 쿨타임의 지속 시간 모디파이어는 데이터 태그가 Data,CoolDown인 SetByCaller로 설정되어 있습니다. 위의 코드에서 Data.Cooldown은 OurSetByCallerTag가 됩니다.

 

 

2. MMC를 사용합니다. 이 설정은 쿨타임 GE와 ApplyCooldown에서 SetByCaller를 지속 시간으로 설정하는 것을 제외하고는 위와 동일합니다. 대신 지속 시간을 커스텀 계산 클래스로 설정하고 새로 만들 MMC를 가리킵니다.

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

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

// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

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

const FGameplayTagContainer* UPGGameplayAbility::GetCooldownTags() const
{
    FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
    MutableTags->Reset(); // MutableTags writes to the TempCooldownTags on the CDO so clear it in case the ability cooldown tags change (moved to a different slot)
    const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
    if (ParentTags)
    {
        MutableTags->AppendTags(*ParentTags);
    }
    MutableTags->AppendTags(CooldownTags);
    return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown() 을 오버라이드하여 쿨타임 태그를 쿨타임 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 재사용 대기시간 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: 클라이언트에서 재사용 대기시간 잔여 시간을 쿼리하려면 리플리케이트된 GameplayEffect를 수신할 수 있어야 합니다. 이는 ASC의 리플리케이션 모드에 따라 달라집니다.

 

4.5.15.2 재사용 대기시간 시작 및 종료 청취

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

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

💡NOTE: 클라이언트에서 GameplayEffect가 추가 또는 제거되기를 기다리려면 리플리케이트된 GameplayEffect를 수신할 수 있어야 합니다. 이는 ASC 의 리플리케이션 모드에 따라 달라집니다.


샘플 프로젝트에는 쿨타임 시작과 끝을 수신하는 커스텀 블루프린트 노드가 포함되어 있습니다. HUD UMG 위젯은 이를 사용하여 메테오의 재사용 대기시간에 남은 시간을 업데이트합니다. 이 AsyncTask 는 UMG 위젯의 Destruct 이벤트에서 수동으로 EndTask()를 호출할 때까지 영원히 살아있습니다. AsyncTaskCooldownChanged.h/cpp를 참고하세요.

 

4.5.15.3 쿨타임 예측

현재 재사용 대기시간은 실제로 예측할 수 없습니다. 로컬에서 예측한 쿨타임 GE가 적용될 때 UI 쿨타임 타이머를 시작할 수 있지만, GameplayAbility의 실제 쿨타임은 서버의 쿨타임 잔여 시간에 연동됩니다. 플레이어의 지연 시간에 따라 로컬에서 예측한 쿨타임이 만료되더라도 서버에서는 여전히 쿨타임 중일 수 있으며, 이로 인해 서버의 쿨타임이 만료될 때까지 GameplayAbility가 즉시 다시 활성화되지 않을 수 있습니다.

샘플 프로젝트는 로컬에서 예측한 재사용 대기시간이 시작되면 메테오 어빌리티의 UI 아이콘을 회색으로 표시한 다음 서버의 수정된 재사용 대기시간 GE가 들어오면 재사용 대기시간 타이머를 시작하는 방식으로 이 문제를 처리합니다.

이로 인해 지연 시간이 긴 플레이어는 지연 시간이 짧은 플레이어보다 재사용 대기시간이 짧은 능력의 발사 확률이 낮아져 불리한 게임 플레이를 할 수 있습니다. 포트나이트는 무기에 재사용 대기시간 GameplayEffect를 사용하지 않는 커스텀 북키핑을 적용하여 이러한 문제를 방지합니다.

진정한 예측 재사용 대기시간(플레이어는 로컬 재사용 대기시간이 만료되었지만 서버는 아직 재사용 대기시간 중일 때 GameplayAbility를 활성화할 수 있음)을 허용하는 것은 에픽 게임즈가 향후 GAS의 반복작업에서 언젠가 구현하고자 하는 기능입니다.

 

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

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

💡NOTE: 여기에는 const_cast 가 필요하며 에픽 게임즈가 의도한 지속시간 변경 방식이 아닐 수도 있지만, 지금까지는 잘 작동하는 것 같습니다.
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 동적 생성은 고급 주제입니다. 이 작업을 너무 자주 수행할 필요는 없습니다.

 

인스턴트 GameplayEffect만 런타임에 C++ 에서 처음부터 생성할 수 있습니다. Duration 및 Infinite GameplayEffect는 런타임에 동적으로 생성할 수 없는데, 그 이유는 리플리케이트할 때 존재하지 않는 GameplayEffect 클래스 정의를 찾기 때문입니다. 이 기능을 구현하려면 대신 에디터에서 일반적으로 하는 것처럼 아키타입 GameplayEffect 클래스를 만들어야 합니다. 그런 다음 런타임에 필요한 내용으로 GameplayEffectSpec 인스턴스를 커스터마이징합니다.

 

런타임에 생성된 인스턴트 GameplayEffect는 로컬 예측 GameplayAbility 내에서 호출할 수도 있습니다. 하지만 동적 생성에 부작용이 있을 수 있는지는 아직 알려지지 않았습니다.

 

예제
샘플 프로젝트는 캐릭터가 Attribute에 있는 킬링 타격을 받으면 골드와 경험치를 킬러에게 돌려주는 프로젝트를 생성합니다.

// 현상금 지급을 위한 동적 인스턴트 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를 보여줍니다. 사용은 여러분의 책임하에 하세요(코드 내 주석 참조)!

UGameplayAbilityRuntimeGE::UGameplayAbilityRuntimeGE()
{
    NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

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.

        // 42로 MyAttribute를 재정의하는 단순 확장 가능한 float 모디파이어를 추가합니다.
        // 실제 애플리케이션에서는 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을 생성하므로
        // 모디파이어가 손실됩니다. 주의: 여기서 수행한 이 "해킹"이 단점을 가질 수 있는지 여부는 알 수 없습니다!
        // Spec에서 GE는 UPROPERTY이기 때문에 GarbageCollector가 GE 오브젝트를 수집하는 것을 방지합니다.
        FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", 수명은 핸들 내의 공유 ptr에 의해 관리되므로
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
    }
    EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}

 

4.5.18 Gameplay Effect Container

에픽 게임즈의 액션 RPG 샘플 프로젝트는 FGameplayEffectContainer라는 구조체를 구현합니다. 바닐라 GAS에는 없지만 GameplayEffect와 타겟 데이터를 담을 때 매우 편리합니다.GameplayEffect로부터 GameplayEffectSpec을 생성하고 그 GameplayEffectContext에서 기본값을 설정하는 등의 작업을 자동화합니다. GameplayAbility에서 GameplayEffectContainer를 생성하고 스폰된 투사체에 전달하는 것은 매우 쉽고 간단합니다. 바닐라 GAS에서 GameplayEffectContainer없이 어떻게 작동하는지 보여드리기 위해 포함된 샘플 프로젝트에서 GameplayEffectContainer를 구현하지 않았지만, 이 기능을 살펴보고 프로젝트에 추가하는 것을 고려해 보시길 적극 권장합니다

GameplayEffectContainer 내부의 GESpec에 액세스하여 SetByCaller 추가와 같은 작업을 수행하려면 FGameplayEffectContainer를 분해하고 GESpec 배열의 인덱스로 GESpec 레퍼런스에 액세스합니다. 이를 위해서는 액세스하려는 GESpec의 인덱스를 미리 알고 있어야 합니다.

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