6. Gameplay Effect - 1

골두·2024년 7월 16일

Unreal GAS Framework

목록 보기
6/8
post-thumbnail

해당 글은 https://github.com/tranek/GASDocumentation 의 설명을 한글로 번역 후 첨언 및 요약 해 나 보려고 내 입맛대로 작성한 글이다.

GameplayEffect

Gameplay Effect란 Ability을 통해 플레이어 자신이나 다른 액터의 속성과 GameplayTags를 변경시키는 수단이다. 즉각적인 속성 변경 (데미지, 치유) 부터 시작해서 이동속도 증가, 스턴 같은 디버프, 버프 를 적용시키는 것도 가능한 수단이라고 할 수 있다. UGameplayEffect Class는 단순하게 게임 플레이 중 특정 대상에게 효과를 정의하는 "Data-Only" 클래스이기 때문에 추가적인 로직을 포함하면 안된다. 그렇기 때문에 일반적으로 게임 개발자들은 이를 활용할 때 Blueprint로 정의해서 활용하는 경우가 많다. (C++로도 가능은 하지만 사례가 거의 없음)

GE(Gameplay Effect)의 기본

GE는 속성 값을 Modifier(수정자)와 Executions(GameplayEffectExecutionCalculation)을 통해서 변경한다.

GE는 지속시간에 대해서 Instant, Duration, Infinite 까지 총 3가지 타입을 사용한다. 또한 GameplayEffect는 GameplayCue(추후 더 알아볼 내용이지만 짧게는 사운드나 파티클 효과로 인지하면 된다.)의 추가 혹은 실행 또한 가능한데 Instant GE에서는 GC를 추가 및 실행이 가능하고, Duration과 Infinite에서의 GC는 Add or Remove를 통해서 GC를 사용한다.

Duration TypeGameplayCue Event사용 시점
InstantExecute속성의 BaseValue에 대한 즉각적이고 영구적인 변경으로 GameplayTag는 적용되지 않고, 특정 프레임만큼 걸리지 않고 바로 적용된다
DurationAdd, Remove속성의 CurrentValue에 대한 임시 변경 및 GameplayTag의 적용으로 GE가 만료되거나 수동으로 제거될 때 Tag도 같이 제거되는 형식
InfiniteAdd, Remove속성의 CurrentValue에 대한 임시 변경 및 GameplayTag의 적용으로 GE가 제거될 때 역시 Tag도 같이 날아간다. 변경 사항이 자동으로 만료되지 않기에 Ability나 ASC에서 수동으로 제거해줘야 한다.

쉽게 생각해보면 Instant의 경우는 영구적인 변화에 주로 쓰일 것이다. (레벨 업 및 스텟 강화) 그리고 Duration과 Infinite는 특정 상황에서의 버프 및 디버프 효과 부여에 사용할 수 있다. (시간 제한이 있거나 실제 내 영구적인 스텟 수치에 반영되는 값은 아니기에)

Duration과 Infinite의 경우 Period Effect 즉 주기적 효과를 적용할 수 있는데 이 말은 정의된 주기(Period)마다 해당 효과의 Modifier와 Executions를 적용하는 것을 의미한다. 쉽게 설명하자면 10초간 유지되는 Effect에서 1초 마다 10의 체력을 회복시킨다에서 "1초마다 10의 체력 회복"을 설정해주는 것 을 말한다. 참고로 Attribute의 BaseValue 변경과 GameplayCues 실행에 있어서는 Instant GameplayEffect로 취급받는다는 것을 알면 좋다. (즉 Period와는 무관하게 생각해야함)

참고: GE의 Period Effect는 Client-Side-Prediction이 적용되지 않는다.

Duration, Infinite 형태의 GE는 임시로 끄거나 키는 것이 가능한데 Ongoing Tag Requirements(Gameplay Tag의 유무)를 이용해서 껏다 키는 스위칭 전략이 가능하다. GE를 종료하게 되면 해당 효과의 Modifiers와 Executions는 종료되지만 GE 자체가 제거된 것이 아니기 때문에 GE를 다시 키게 될 때 기존의 동작하지 않는 GE 동작들이 동작하게 된다. (즉 Tag를 활용한 if문이라고 할 수 있다)

또한 Duration, Infinite 형태의 GE의 Modifier를 수동으로 재계산해야하는 경우가 생긴다. 대표적인 예시로는 Attribute에서 가져오지 않는 데이터를 사용하는 MMC가 존재하는 경우인데, 이런 경우는 UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)를 활용해 기존 레벨과 동일한 레벨로 설정하는 것이 가능하다. 여기서 레벨(맵의 레벨이 아님을 명시)은 UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()을 통해서 가져오는 것이 가능한데 기반이 되는 속성이 업데이트 될 때 해당 속성을 사용하는 Modifier또한 업데이트 되는 것이 핵심이다.

SetActiveGameplayEffectLevel()의 주요 Modifier 업데이트 함수는 아래와 같다.

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

GE는 기본적으로 인스턴스화가 되지 않는다. Ability나 ASC에서 GE를 적용하려 할 때 GE의 CDO에서 GameplayEffectSpec을 생성하고 성공적으로 적용된 GESpec(GameplayEffectSpec)을 FActiveGameplayEffect라는 새로운 구조체에 추가해 ASC는 ActiveGameplayEffects라는 특수한 컨테이너 구조체에서 관리하게 설계되었다.

GESpec란

GESpec은 밑에서 부터 계속해서 나올 설명이라 원문과는 다르게 위로 끌어올려서 설명한다

GameplayEffectSpec (GESpec)은 GE의 인스턴스라고 생각할 수 있다. GESpec은 자신이 나타내는 GE 클래스, 생성된 레벨, 생성자에 대한 참조를 보유하고 있기 때문이다. GE는 개발자가 런타임 이전에 생성해야하는 반면에 GESpec은 런타임 중에 자유롭게 생성 및 수정이 가능하다라는 특징이 있다. GE를 적용할 때 GE에서 GESpec이 생성되고, 실제로 대상에 적용되는 것은 GE가 아닌 GESpec인 것을 알아야한다.

GESpec은 UAbilitySystemComponent::MakeOutgoingSpec() 함수를 통해서 GE에서 생성되며, Blueprint에서 역시 해당 함수 호출이 가능하다. GESpec은 즉시 적용될 필요는 없다.

또한 GESpec은 즉시 적용될 필요 또한 없음을 기억하면 좋다. GESpec의 경우는 Ability를 통해 생성된 GESpec을 투사체를 통해 날려 맞은 적에게 그 GESpec을 적용하는 것이 일반적이고 GESpec이 성공적으로 적용될 때 FActiveGameplayEffect라는 구조체를 반환시키는 방식이다.

GESpec의 정리 내용 겸 원칙사항

  • GE class는 GESpec 생성의 기반이 되는 class다.
  • 보통은 Ability의 Level과 GESpec의 Level이 같은 경우가 있지만 종종 다를 때가 있다. (웬만해서는 같게하지만 달라도 무방하다)
  • GESpec과 GE의 지속시간을 보통 같게 설정되지만 다르게 설정될 수 있다.
  • GESpec의 주기적 효과(Period Effect)는 기본적으로 GE와 동일하지만 다를 수 있다.
  • GESpec의 스텍카운트 제한은 GE에서 설정하고 보관한다
  • GameplayEffectContextHandle라는 것을 통해서 GESpec을 만든 주체를 파악할 수 있다.
  • 속성(Attribute)은 GESpec 생성 시 스냅샷을 통해서 캡쳐된다. (오역 가능성 있음)
  • DynamicGrantedTag는 GE가 부여하는 GameplayTag 외에 GESpec이 대상에게 부여하는 것을 말한다. (오역 가능성 있음)
  • DynamicAssetTags는 GE가 가지는 AssetTags 외에 가지는 GESpec의 태그를 말한다. (오역 가능성 있음)
  • SetByCallers = TMap 구조다.

SetByCallers

SetByCallers는 GESpec이 GameplayTag or FName과 연관된 float 값을 전달할 수 있도록 해주는 역할을 수행한다. 이 값들은 GESpec 내의 각각의 TMap인 TMap<FGameplayTag or FName, float>에 저장한다. 이 값들이 GE의 Modifier로 사용되거나 float 값을 전달하는 일반적인 수단으로 사용될 수 있는데, Ability 내에서 GE ExecutionCalculations 혹은 ModifierMagnitudeCalculation에 전달하는 데 SetByCallers를 사용하는 것이 일반적이다.

SetByCaller 사용처설명
ModifiersSetByCaller는 GE class에서 미리 정의가 되어있어야 하는데, GameplayTag 버전만 사용 가능하다. GE 클래스에 하나가 정의되어 있지만, GESpec에 해당 태그와 float 값이 없는 경우 GESpec을 적용할 때 런타임 오류가 발생하고 0을 반환하게 된다. 이것은 Divide 연산할 때 잠재적으로 문제가 될 수 있다(분모가 0이 되니까).
ElsewhereSetByCaller를 미리 정의할 필요가 없으며 GESpec에 존재하지 않는 SetByCaller를 읽을 때 개발자가 정의한 기본값을 반환할 수 있다. 필요에 따라 경고문 또한 출력이 가능하다

SetByCaller를 사용하기 위해서는 GameplayTag와 FName을 parameter로 활용하면 된다.

// 다형성을 활용해 동일한 이름의 2개의 함수를 창조
// SetByCaller 정보를 저장하는 함수들
void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);

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

참고: GameplayTag를 사용하는 것을 권장한다. FName보다 오류 가능성이 낮아지기에

GE의 적용 및 제거 방법

GE를 적용하는 방법은 GameplayAbility의 함수나 ASC의 함수를 통해서 다양한 방법으로 적용이 가능하다. 일반적으로는 ApplyGameplayEffectTo()라는 함수를 사용해 추가하고 RemoveActiveGameplayEffect()라는 함수로 제거 한다. 다른 함수들은 본질적으로 Target에서 UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()FActiveGameplayEffectsContainer::RemoveActiveEffects()를 호출해 Effect를 적용 및 제거하는 편리한 방식으로 사용한다.

GE를 GA(GameplayAbility)에서 사용하기 위해서 (대표적으로 Projectile 의 경우)는 대상의 ASC를 가져와 해당 ASC의 ApplyGameplayEffectToSelf, RemoveActiveGameplayEffect 함수를 활용하는 방식으로 한다.

ASC에서는 Duration, Infinite 형태의 GE가 적용 or 제거되는 것을 감지하기 위해서는 ASC의 특정 Delegate에 바인딩을 걸어서 해결할 수 있다.

// 바인딩 함수
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

// 콜백 함수
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

서버는 Replication 모드에 관계 없이 항상 이 함수를 호출하게 된다. 자율 프록시(autonomous proxy)는 Full 혹은 Mixed Replication 모드에서 복제된 GE에 대해서만 해당 함수를 호출하게 된다. 시뮬레이트 된 프록시(Simulated proxies) 환경에서는 Full Replication 모드에서만 해당 함수를 호출하게 된다. 즉 서버의 프록시 상태에 따라 다르게 반응한다.

GE Modifier

Modifier는 속성을 변경하는데 사용되며, 속성을 Predictively하게 변경해주는 유일한 방법(클라이언트 내부에서 예측 계산이 가능하게 해줌)이다. 하나의 GE는 0개부터 여러개의 Modifier를 보유할 수 있는데 각각의 Modifier는 지정된 연산자를 통해 하나의 속성만을 변경하는 책임 원칙을 가지고 있다. (이 말은 즉슨 하나의 Modifier에서는 하나의 속성(ex. 체력)을 관리하는 것만이 가능하기에, 마나와 체력 2개 이상의 회복을 원한다면 원하는 속성 만큼의 Modifier를 만들어 사용해야 한다는 것이다)

연산자설명
Add결과 값을 Modifier에서 지정한 속성에 더한다. Modifier에서 변경 값을 음수로 변경하면 뺄셈 처리가 된다.
Multify곱셈을 의미하며 결과 값을 Modifier에서 지정한 속성에 곱한다
Divide나눗셈을 의미하며 결과 값을 Modifier에서 지정한 속성에 나눈다.
OverrideModifier에서 지정한 속성을 결과 값으로 대체한다.

속성의 CurrentValue의 경우 해당 속성의 BaseValue에 모든 Modifier를 합친 값이 된다. Modifier가 집계되는 방식에 대한 공식의 경우 GameplayEffectAggregator.cpp의 FAggregatorModChannel::EvaluateWithBase에서 다음과 같이 정의되어 있다.

((InlineBaseValue + Additive) * Multiplicitive) / Division

Override의 경우는 마지막에 나온 결과 값에 덮어쓰는 방식으로 가장 마지막에 적용된 Modifier가 우선으로 적용된다. Override가 2번 이상일 경우는 마지막의 Override가 반영된다라는 뜻이다.

참고사항

  • 백분율 기반의 변경 사항이 있을 경우는 덧셈 이후에 발생할 수 있도록 곱하기 연산을 사용해야 한다.
  • Prediction은 백분율 변경 사항을 처리하는 것에 대해 어려움이 있음을 참고해야 한다.

Modifier의 종류

Modifier에는 총 4가지 타입이 존재하는데 Scalable Float, Attribute Based, Custom Calculation Class, Set By Caller 가 이에 속한다. 이들은 어떤 방식으로든 float 값을 생성해내며, Modifier가 지정한 속성을 해당 Modifier의 연산에 따라 변경하는 데 사용한다.

Modifier 종류설명
Scalable FloatFScalableFloats라는 데이터 테이블을 가리킬 수 있는 구조체가 존재하는데, 이 구조체를 이용해 테이블의 행을 변수로, 열을 레벨로 사용해 지정된 테이블 행의 값을 Ability의 현재 레벨(or GESpec에서 재정의된 경우 다른 레벨)에서 읽어오는 작업을 수행한다. 이 값은 계수(cofficient)에 의해 추가 조작이 가능하며, 데이터 테이블이나 행이 지정되지 않은 경우, 값을 1로 취급해 계수를 사용해 모든 레벨에서 단일 값을 하드코딩할 수 있다.
Attribute Based해당 Modifier는 소스(GESpec을 생성한 주체) or Target(GESpec을 받은 대상)의 기반 속성의 BaseValue or CurrentValue를 가져와 계수와 사전/사후 계수 추가를 통해 추가로 수정하는 작업을 수행한다. Snapshotting이라는 GESpec이 생성될 때 기반 속성이 캡쳐되는 것을 의미하는 기능이 있는데 이 기능이 없으면 GESpec이 적용될 때 속성이 캡쳐된다
Custom Calculation Class해당 Modifier는 가장 유연한 Modifier로 복잡한 Modifier생성이 가능하다. ModifierMagnitudeCalculation 클래스를 사용해 결과로 나온 float 값을 계수와 사전/사후 계수 추가를 통해 추가 조작이 가능하다
Set By Caller해당 Modifier는 GE 외부(런타임 환경에서 Ability나 GESpec을 생성한 주체)에 의해 설정되는 값을 말한다. 예를 들어 플레이어가 버튼을 누르고 있는 시간에 따라 데미지를 설정하려면 SetByCaller를 사용할 수 있다. SetByCaller(SBC)는 본질적으로 GESpec에 존재하는 TMap<FGameplayTag, float> 타입이며, Modifier는 단순하게 Aggregator에게 제공된 GTag와 연결된 SBC값을 찾도록 지시한다. 해당 방법은 아래에 더 자세하게 설명한다

곱셈과 나눗셈 Modifier

기본적으로 모든 곱셈과 나눗셈 Modifier는 Attribute의 BaseValue에 곱하거나 나누는 작업을 수행하기 전에 모두 더해진다.

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

위와 같은 코드 공식에서 곱셈과 나눗셈 Modifier는 모두 Bias 값이 1이고, 더하기 Modifier는 Bias가 0이다. 그렇기 때문에 위의 EvaluateWithBase1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...라는 공식이 나오는데 해당 공식은 치명적인 문제가 하나 있다. 대부분 사람들은 곱셈 Modifier가 2개 있을 때 서로 곱해진 후 BaseValue에 반영될 것으로 생각한다. (ex. 특정 속성을 1.5배 해주는 Modifier가 2개 있다면 대부분 사람들은 1.5 * 1.5 = 2.25 가 곱해질 것으로 예상하지만 이 방식은 곱셈의 배수 값이 합으로 연산이 먼저 되기 때문에 1.5 + 1.5 = 3 이 곱해지는 문제가 생긴다.)

GameplayPrediction.h의 예시에서 기본 이동속도 500에 10% 버프를 하면 550이 되고, 거기에 추가로 10% 버프를 하면 600이 되는 것과 같은 이치를 가지고 있다.

또 다른 문제는 이 공식이 Paragon을 염두에 두고 설계한 것이기 때문에, 사용 가능한 값에 대한 문서화되지 않은 규칙이 존재한다.

곱셈, 나눗셈 Modifier의 곱셈 및 더하기 공식에 대한 규칙
1. 값이 1 미만인 경우가 하나 이하 AND(&&) 값이 1 ~ 2 범위 내에 있는 경우가 여러개인 경우
2. OR(||) 하나의 값이 2 이상인 경우

공식에서 Bias는 기본적으로 1~2 범위 내의 숫자에서 정수 자릿수를 빼고, 첫번째 Modifier의 Bias는 시작 Sum 값(Loop 전에 Bias로 설정됨)에서 빼기 때문에 값이 하나만 있거나 하나의 값이 1 미만이고 나머지 값의 범위가 1 ~ 2 범위일 때는 올바르게 작동한다.

예시
1. 곱셈: 0.5, -> 0.5 + 1 + (0.5 - 1) = 0.5 (일치)
2. 곱셈: 0.5, 0.5 -> 1 + (0.5 - 1) + (0.5 - 1) = 0 (불일치 - 예상 값은 아마 1) 여러 개의 값이 1 미만인 경우 Multiplier를 더하는 것에는 의미가 없음을 알아야 한다. Paragon의 케이스에서는 Multifly Modifier에 대해서 가장 큰 음수 값만 사용하도록 설계했고, BaseValue에 곱해지는 값이 1 미만인 경우는 최대 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의 개수 + 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;
}

Modifier의 Gameplay Tags

SourceTag와 TargetTag는 각 Modifier에 설정할 수 있고 GE의 Application Tag 요구사항과 동일하게 작동한다. 즉 태그는 효과가 적용될 때만 고려되고, 주기적인 무한 효과의 경우, 첫 번째 적용 시에만 고려되고 주기적으로 실행되는 것에는 고려되지 않는다.

속성 기반(Attribute Base)의 Modifier는 SourceTagFilter와 TargetTagFilter도 설정이 가능한데 속성 기반 Modifier의 Source가 되는 속성의 크기를 결정할 때 이 필터를 활용해 특정 Modifier를 제거하는 것이 가능하다. Source 혹은 Target이 필터의 모든 태그를 가지고 있지 않은 Modifier는 제외된다.

즉 이말은 Source ASC와 Target ASC의 태그는 GE에 의해 캡쳐되고 Source ASC 태그는 GESpec이 생성될 때 캡쳐되며, Target ASC 태그는 GE 실행 시 캡쳐된다. 무한 혹은 지속 시간 GE의 Modifier가 적용되는지 (즉 Aggregator가 적합한지) 결정할 때 이러한 필터가 있으면 캡쳐된 태그와 필터가 비교된다.

스택형 GE

GE는 기본적으로 이전에 존재하단 GESpec 인스턴스와는 관계없이 새로운 GESpec을 적용하게 된다. GE는 Stack형 여부를 선택이 가능한데 이 Stack 여부를 활성화 한다면 기존의 이전 인스턴스와 관계없이 새로운 GESpec이 추가되는 방식에서 GESpec의 스택 카운트가 변경되는 방식으로 바뀐다. Stack은 오직 Duration과 Infinite에서만 가능하다

이 스택 방식에는 크게 Source 대상 집계(Aggregate)와 Target 대상 집계 2가지로 나뉘어진다

스택 방식설명
Source 대상 집계Source 관점에서 Target별로 별도의 스택 인스턴스가 존재하고, 최대 설정한 X 개 만큼의 스택을 저장하게 만든다 - 예시로 두개의 다른 스킬에서 공격력 증가 효과를 받을 때 각 스킬 별로 인스턴스가 존재하고 스택도 별도로 관리한다
Target 대상 집계Target 관점에서 Source와 관계없이 단일 스택 인스턴스가 존재하고 각 Source는 공유된 스택 제한치만큼 스택을 적용하게 할 수 있다 - 예시로 여러 스킬이나 아이템을 통해 공격력이 증가되는 경우 효과가 모두 하나로 합쳐져서 하나의 스택으로 관리된다

스택의 경우 만료, 지속시간 재갱신, 주기 초기화 기능또한 가지고 있다. GE Blueprint에서 관련되서 유용한 hover 툴팁을 제공해준다.

Ability 부여하는 GE

GE는 또한 ASC에 새로운 Ability 또한 적용시켜주는 것이 가능하다. 이 효과는 Duration과 Infinite에서만 처리가 가능하다.

해당 기능은 일반적으로 다른 플레이어(혹은 액터)들이 넉백이거나 끌어당겨 와지거나 등의 특정 행동을 취하게 하기 위해서 사용한다. (능력에 대한 자동 활성화 기능은 추후 Passive Abilities에서 더 상세히 다룰 예정)

개발자는 GE에서 Ability에 대해 부여할 능력, 부여 레벨, 입력 바인딩(이것 역시 추후 다룰 내용), 능력 부여 등의 설정이 가능하고 필요에 따라 설정해두면 된다.

제거 정책설명
즉시 능력 취소GE가 Target에서 제거될 때 부여된 Ability도 즉시 취소하고 제거함
능력 완료 후 제거부여된 Ability가 전부 완료되고 제거함
영구적 부여GE가 제거되어도 부여된 Ability는 수동으로 제거하기 전까지 영구적으로 남아있음

GE Tags

GE는 여러개의 GameplayTagContaniers(GTC)를 보유하고 있는데, 개발자는 각 카테고리에 대해 추가된 GTC와 제거된 GTC를 편집해 그 결과가 컴파일 시 합쳐진 GTC에 나타나게 된다. 추가된 태그는 해당 GE가 부모 클래스에 없던 새로운 태그(GE에서 새롭게 추가된 태그)고, 제거된 태그(부모 클래스에서 상속받았지만 GE에서 제거된 태그)는 부모 클래스에는 존재하지만 하위에는 태그를 의미한다.

카테고리설명
GE Assets TagGE가 보유한 태그를 의미하며, 기능은 따로 없고 그저 GE에 대해 설명하는 태그 (메타데이터 역할)
Granted TagGE가 적용된 ASC에도 부여되는 태그, GE에서 제거 시 ASC에서도 제거되는 태그로 Duration과 Infinite에서만 사용 가능하다(GE에 영향받는 대상에게 추가 정보 제공 역할)
Ongoing Tag RequirementsGE의 활성화 여부를 결정하는 태그로 GE가 비활성화 상태여도 적용될 수 있는 태그이다. 해당 태그가 특정 조건에 의해 활성화 되면 GE가 다시 활성화되며 Modifier를 재적용 시키게 된다. Duration과 Infinite에서만 사용 가능하다.
Application Tag RequirementsGE를 적용할 수 있는 Target 인지를 인지하는 태그로 요구사항이 충족하지 않으면 GE가 적용되지 않는다. (즉 GE의 조건)
Remove GE with TagTarget에 적용된 GE중 Asset or Granted 태그에 이 태그가 포함되어 있을 때, GE의 제거가 성공되면 제거된다. (다른 GE와의 상호작용 제어)

Immunity (면역)

GESpec에서는 GameplayTag를 기반으로 다른 GE의 적용을 차단하는 것이 가능하다. 면역 상태는 Application Tag Requirements와 비슷한 뜻으로 인지할 수 있지만, 면역 시스템은 UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate라는 Delegate를 통해서 GE를 차단하는 방식이다.

GrantedApplicationImmunityTags라는 태그는 Source의 ASC(Source의 Ability의 AbilityTag 또한 포함한다)에서 지정된 태그가 하나라도 있는지 확인하고, 있다면 특정 캐릭터나 Source에게 면역을 제공한다. 즉 태그를 기반으로 면역을 제공하는 방식이다

Granted Application Immunity Query라는 것도 존재하는데 이거는 GESpec이 차단 혹은 허용을 위한 쿼리와 일치하는지 확인하는 방식이다.

Query 방식은 GE Blueprint에서 유용한 hover 형식의 툴팁이 있으니 참고하면 좋다

GE Context

GE Context 구조체는 GESpec의 instigator와 TargetData를 저장하고 있다. ModifiterMagnitudeCalculator, GE ExecutionCalculations, 속성 Set, GameplayCue 정보들과 같은 위치에서 subclass 형태로 데이터를 전달하기 좋은 구조체라고 할 수 있다.

GameplayEffectContext이 Subclass화 방법

  1. FGameplayEffectContext를 SubClass화해서 선언한다
  2. FGameplayEffectContext::GetScriptStruct()를 재정의한다. (상속받아서 가져온다)
  3. FGameplayEffectContext::Duplicate()를 재정의한다.
  4. 새로운 데이터가 Replicated 되야한다면 FGameplayEffectContext::NetSerialize()를 재정의한다.
  5. 부모 구조체와 같이 TStructOpsTypeTraits 를 정의해서 사용한다.
  6. AbilitySystemGlobals 클래스에서 AllocGameplayEffectContext를 재정의해서 사용해 Subclass 형태의 새로운 객체를 반환해준다.

해당 글의 샘플 프로젝트인 GASShooter는 Subclass화 된 GEContext를 사용해 TargetData를 추가하고, 여러 적에게 피해를 줄 수 있는 샷건에서 GameplayCues를 엑세스 할 수 있다.

profile
나 볼려고 만든 블로그 (블로그 이전: https://goldfrosch.tistory.com/)

0개의 댓글