GAS
GAS는 복잡한 기존 시스템을 몇 가지 구성요소로 분리하면서 자유도와 재활용성을 올리는 프레임 워크이다.
GAS의 주요 구성요소는 다음과 같다.
ASC (AbilitySystemComponent)
액터에 부착해 GAS 시스템을 동작시키는 컴포넌트이다.
컴퓨터로 따지자면 CPU 같은 존재이다.
ASC에는 다양한 요소와 함수들이 내장되어 있다.
그중, 자주 사용되는 몇 가지 함수들은 다음과 같다.
//ASC의 ActorInfo를 초기화하는 함수
virtual void InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor);
//등록되어 있는 AttributeSet을 받아오는 함수
template <class T >
const T* GetSet() const
{
return (T*)GetAttributeSubobject(T::StaticClass());
}
//Attribute를 통해 BaseValue를 받아오는 함수
float GetNumericAttributeBase(const FGameplayAttribute &Attribute) const;
//GE를 실행하는 함수
FActiveGameplayEffectHandle BP_ApplyGameplayEffectSpecToTarget(const FGameplayEffectSpecHandle& SpecHandle, UAbilitySystemComponent* Target);
FActiveGameplayEffectHandle BP_ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpecHandle& SpecHandle);
//GESpec을 만드는 함수
virtual FGameplayEffectSpecHandle MakeOutgoingSpec(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level, FGameplayEffectContextHandle Context) const;
//매칭되는 Tag 여부를 묻는 함수
FORCEINLINE bool HasMatchingGameplayTag(FGameplayTag TagToCheck) const override
{
return GameplayTagCountContainer.HasMatchingGameplayTag(TagToCheck);
}
//Tag를 부여하는 함수
FORCEINLINE void AddLooseGameplayTag(const FGameplayTag& GameplayTag, int32 Count=1)
{
UpdateTagMap(GameplayTag, Count);
}
//GC를 실행하는 함수
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
//GA를 부여하는 함수
FGameplayAbilitySpecHandle GiveAbility(const FGameplayAbilitySpec& AbilitySpec);
//GA를 실행하는 함수
bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
//부여된 GA를 찾는 함수
FGameplayAbilitySpec* FindAbilitySpecFromClass(TSubclassOf<UGameplayAbility> InAbilityClass) const;
FGameplayAbilitySpec* FindAbilitySpecFromInputID(int32 InputID) const;
//Spec을 통해 GA에 입력을 전달해주는 함수
virtual void AbilitySpecInputPressed(FGameplayAbilitySpec& Spec);
virtual void AbilitySpecInputReleased(FGameplayAbilitySpec& Spec);
이외에도 다양한 함수들을 제공한다.
ASC는 AttributeSet과 밀접한 관련이 있다.
ASC의 특징 중 하나는 AttributeSet을 등록하지 않아도 액터에 존재하는 AttributeSet을 찾아 알아서 관리한다는 점이다.
void UAbilitySystemComponent::InitializeComponent()
{
Super::InitializeComponent();
...
TArray<UObject*> ChildObjects;
GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);
for (UObject* Obj : ChildObjects)
{
UAttributeSet* Set = Cast<UAttributeSet>(Obj);
if (Set)
{
SpawnedAttributes.AddUnique(Set);
}
}
...
}
InitializeComponent 부분을 보면 Owner의 모든 ChildObject를 받아와 AttributeSet으로 캐스팅해 본 뒤, 성공하면 등록하는 과정을 거치기 때문이다.
다음은 ASC에서 실행하는 중요한 흐름을 그래프로 만든 것이다.
AttributeSet
AttributeSet은 GAS시스템에서 데이터를 관리하는 클래스이다.
예를 들면, 캐릭터의 체력, 공격력, 사거리 등이 있다.
이 클래스를 ASC와 같은 액터에 부착하여 ASC나 GE를 통해 데이터를 변경하는 것이 일반적이다.
앞서 말했듯이 AttributeSet은 따로 등록할 필요없이 ASC와 같은 액터에 위치시키기만 하면 된다.
ASC가 Initialize될 때, AttributeSet을 찾아 등록하기 때문이다.
AttributeSet은 BaseValue와 CurrentValue로 나눠진다.
BaseValue는 기준이 되는 값으로 일시적으로 변경되지 않는다.
그렇다고 영원히 바뀌지 않는 값은 아니다.
이해가 잘 되지 않는다면 CurrentValue에 대해 이해하면 BaseValue에 대해 쉽게 이해할 수 있다.
CurrentValue는 버프나 디버프 등으로 일시적으로 변경하는 값을 의미한다.
예를 들어, 10초간 공격력이 10% 증가하는 버프를 얻었다고 가정해 보자.
기존 공격력이 10이었다면 BaseValue는 10이지만, 버프를 받아 CurrentValue는 10% 증가하여 11이 되었을 것이다.
이후, 버프가 없어지면 CurrentValue도 BaseValue와 같은 값인 10으로 되돌아 올 것이다.
프로젝트를 진행하다보면 Getter, Setter처럼 데이터에 대한 여러 함수를 만드는 경험이 있을 것이다.
처음에는 괜찮지만, 데이터가 점점 많아지면 이런 함수들이 너무 많아져 복잡한 경험을 했을 것이다.
이러한 부분을 해결하기 위해 AttributeSet에서는 유용한 매크로를 제공한다.
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
해당 매크로는 클래스 이름과 프로퍼티의 이름을 통해 Getter, Setter 등 다양한 함수를 자동으로 만들어 준다.
//AttributeSet.h
...
ATTRIBUTE_ACCESSORS(UCharacterAttributeSet, AttackRange);
ATTRIBUTE_ACCESSORS(UCharacterAttributeSet, MaxAttackRange);
AttributeSet에는 중요한 함수가 4개있다.
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data);
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
- PreAttributeChange: Attribute가 변경되기 전에 호출되는 함수, NewValue가 참조값으로 전달되어 원하는 방식으로 조정이 가능하다.
- PostAttributeChange: Attribute가 변경된 후 호출되는 함수, OldValue에서 NewValue로 변경된 것을 확인할 수 있다. 보통 log를 위한 함수로 사용된다.
- PreGameplayEffectExecute: GE가 실행되기 전에 호출되는 함수, FGameplayEffectModCallbackData를 통해 정보를 확인하여 값을 변경할 수 있다.
- PostGampleEffectExecute: GE가 실행된 후 호출되는 함수, FGameplayEffectModCallbackData를 통해 GE에서 미친 영향을 적용하는 데 사용된다.
AttributeSet의 주요 흐름은 다음과 같다.
또한, AttributeSet에는 실질적인 데이터 외에도 허상의 데이터를 넣어 관리하는 경우가 있다.
이를 MetaAttribute라고 한다.
MetaAttribute가 필요한 이유는 다음과 같다.
예를 들어, 실드 기능을 구현해야 하는 상황에서 공격에 대한 대미지를 입는 GE에 의해 체력을 직접적으로 변경되면 변경되기 이전의 값을 다시 불러와 실드 값을 뺀 뒤 다시 체력을 설정해야 하는 번거로움이 있다.
하지만, Damage라는 허상에 데이터를 넣어 GE에서 입히는 피해를 관리하면 실드 기능은 Damage에서 실드량만큼 뺀 뒤 체력을 깎으면 된다.
이렇게 구현하는 것이 확장성이 좋고 유연한 설계이다.
하지만, 이를 위해 지켜야 하는 원칙은 MetaAttribute를 사용하면 0으로 초기화하는 것을 잊어서는 안 된다.
Gameplay Ability
액터의 행위나 능력을 나타내는 클래스이다.
하나의 작업을 수행하는 독립적인 행동은 모두 GA로 구현할 수 있다.
GA는 GAS 시스템에서 시작이 되는 클래스라고 생각한다.
플레이어 입력이나 게임에서의 상황에 의해 어떠한 동작이 실행되는 경우 GA를 발동시켜야 하기 때문이다.
GA에는 InstancingPolicy라는 특별한 옵션이 있다.
GA를 생성하는 방식을 지정하는 옵션이다.
만약, 게임에 GAS 액터가 너무 많아 GA의 생성이 많아지면 성능적인 문제가 발생할 수 있다.
그렇기 때문에, 이에 대한 옵션을 지정해 적절한 정책을 설정해야 한다.
InstancingPolicy에는 세 가지 옵션이 있다.
- Instanced per Execution: GA를 실행할 때마 GA의 사본을 스폰한다. 변수를 자유롭게 사용할 수 있고 실행 시작 시 초기화가 된다는 장점이 있지만, 오버헤드가 크기 때문에 적게 사용되는 GA에 설정해야 하는 옵션이다.
- Instanced per Actor: GA를 처음 실행할 때 한 번 GA의 인스턴스를 생성하고 그 뒤로 실행될 때는 재사용한다. 실행할 때마다 변수를 초기화해야 하지만, 여러 번 생성되지 않기 때문에 오버헤드를 줄일 수 있고 리플리케이션에 적합하다.
- Non Instanced: 가장 효율적인 정책이다. GA를 실행해도 인스턴스가 생성되지 않고 CDO를 사용한다.
하지만 C++로 작성한 GA만 사용할 수 있다. 또한, 멤버 변수를 변경할 수 없고, 델리게이트, RPC 등 많은 부분에 제약이 걸린다. 즉, 간단하게 Attribute를 읽거나 쓰는 정도의 어빌리티에 적합하다. 예를 들어, 유닛의 기본 공격같이 자주 사용되고 간단한 동작만 하는 행동에 적합하다.
GA는 다양한 태그 컨테이너를 갖고 있다.
이런 태그 컨테이너는 매우 유용하게 사용된다.
이러한 태그를 잘 설정하면 코딩 없이도 다양한 기능을 구현할 수 있다.
예를 들어, 점프 중 공격을 할 수 없게 만들고 싶다면 공격 GA의 Activation Blocked Tags에 점프에 관한 태그를 설정하면 된다.
GA의 주요 함수는 다음과 같다.
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
virtual void CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility) override;
virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;
- ActivateAbility: GA를 실행하는 함수, 해당 GA에 해야 하는 동작을 수행한다.
- CancelAbiltiy: GA의 실행을 취소하는 함수
- EndAbility: GA를 종료하는 함수, 모든 GA는 반드시 해당 함수를 호출하여 종료해야 한다.
GA는 캐릭터나 게임에서의 상황에 의해 실행된다.
하지만, 이외에도 GA를 실행시키는 방법이 있는데, GameplayEvent를 통해서 실행하는 것이다.
GameplayEvent를 통해 GA를 실행시키는 방법은 다음과 같다.
특정 사건이나 어떠한 상황에 의해 로직이 실행되고 해당 로직에서 SendGameplayEventToActor를 실행하는 것이다.
SendGameplayEventToActor에서는 HandleGameplayEvent를 실행하여 부여된 GA 중 전달받은 태그를 TriggerTag로 설정된 GA를 실행시킨다.
예를 들어, 공격 Montage 실행 중, 특정 시점에 공격에 적중한 적을 판정해야 한다고 가정해 보자.
- 공격 Montage에 AnimNotify를 통해 특정 타이밍에 함수를 실행할 수 있게 설정한다.
- 실행되는 함수에서 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor를 실행한다.
- SendGameplayEventToActor에는 실행하고 싶은 GA의 TriggerTag로 설정된 태그를 전달한다.
- Library의 함수에 의해, GA가 실행된다.
AT
AT는 GA를 비동기로 처리하기 위해 사용하는 클래스이다.
어떠한 동작이 한순간에 끝이 날 수도 있지만, 그렇지 않은 경우도 있다.
예를 들어, 애니메이션을 실행해야 하는 GA는 한 프레임에 모든 동작을 끝낼 수 없다.
만약, 공격 애니메이션을 실행하는 중 이동을 막아두려면 공격 애니메이션이 끝났을 때 다시 이동할 수 있게 변경해 주어야 한다.
하지만, 한 순간에 끝나는 GA는 이를 해결할 수 없다.
따라서, 비동기로 어떠한 작업을 하고 해당 작업을 완료했을 때, 완료처리를 하는 방식을 사용하기 위해 AT를 이용하는 것이다.
UE에는 이미 유용한 AT들이 많이 있다.
그중, 애니메이션을 실행하고 대기하는 UAbilityTask_PlayMontageAndWait가 대표적인 것 같다.
물론 AT를 만들어서 사용해도 된다.
다만, AT를 사용하는 공식 같은 점이 있기 때문에 이를 주의하여 제작해야 한다.
- AT를 생성하는 static 함수가 필요하다.
- AT가 작업을 완료했을 때, 이를 알릴 수 있는 델리게이트가 필요하다.
- 즉시 종료되는 것이 아니라면, SetWaitingOnAvartar함수를 호출해 Waiting 상태로 설정한다.
TA
TA는 대상에 대한 판정을 구현할 때 사용하는 특수한 액터이다.
기존 시스템에서 사용하던 다양한 Trace들이 있다.
사실 TA에서도 기존 Trace들을 이용하여 판정 결과를 만들지만, TA에서는 FGameplayAbilityTargetDataHandle라는 구조체를 반환값으로 가지는데, 여기에는 다양한 정보를 담을 수 있다.
FGameplayAbilityTargetDataHandle에 담을 수 있는 대표적인 정보는 다음과 같다.
- HitResult
- 판정된 액터의 포인터들
- 시작 지점
- 끝 지점
이러한 유용한 정보들을 전달할 수 있는 장점도 있지만 다른 기능 또한 제공한다는 점도 장점이라고 할 수 있다.
예를 들어, 스킬을 사용하는 상황에서 스킬의 범위를 표현하기 위한 시각적 효과를 보여주어야 할 때가 있다.
이러한 시각화를 수행하는 액터를 월드레티클(WorldReticle)이라고 한다.
TA에서는 월드레티클을 생성할 수 있게 도와주며, 스킬 사용을 다시 확인할 단계를 구현할 수도 있다.
TA의 주요 함수는 다음과 같다.
virtual void StartTargeting(UGameplayAbility* Ability);
virtual void ConfirmTargetingAndContinue();
virtual void ConfirmTargeting();
virtual void CancelTargeting();
- StartTargeting: 타겟팅을 시작하는 함수, SourceActor를 초기화하기 적합하다.
- ConfirmTargetingAndContinue: 타겟팅을 확정하고 남은 프로세스를 진행한다.
- ConfirmTargeting: 태스크를 진행하지 않고 타겟팅만 확정한다.
- CancelTargeting: 타겟팅을 취소한다.
앞에서 언급했듯이 TA의 반환값은 FGameplayAbilityTargetDataHandle라는 구조체이다.
FGameplayAbilityTargetDataHandle에는 GamepalyAbilityTargetData들이 들어있다.
GameplayAbilityTargetData는 다양한 형태로 override 되어있다.
- FGameplayAbilityTargetData_LocationInfo
- FGameplayAbilityTargetData_ActorArray
- FGameplayAbilityTargetData_SingleTargetHit
TA를 사용할 때는 SpawnActorDeferred를 통해 지연 생성을 하는 것이 일반적이다.
TA에서 데이터를 준비를 끝마치면 해당 데이터를 Delegate를 통해 전달해주어야 하기 때문에 TA를 지연 생성하여 인스턴스를 만든 뒤 Delegate에 Callback을 바인딩하여 결과를 전달받을 준비를 해야 하기 때문이다.
또한, TA도 AT와 마찬가지로 비동기로 진행된다고 할 수 있다.
GA에서부터 AT 그리고 TA까지의 흐름을 보면 다음과 같다.
GE
내가 생각하기에는 GAS 시스템의 꽃이 GE라고 생각한다.
간단한 옵션 설정만으로 다양한 기능을 구현할 수 있기 때문이다.
GE는 BP로 제작하는 것이 C++로 제작하는 것보다 훨씬 생산적이다.
C++에서 따로 필요한 로직이 없지만, 옵션을 설정하는 변수 이름 같은 것들이 복잡하기 때문이다.
GE를 C++로 제작한다면 특별히 할 것은 없고 생산자에서 다음과 같은 설정만 해주면 된다.
생산자()
{
//기간 설정
DurationPolicy = EGameplayEffectDurationType::Instant;
//Modifier 설정(변화를 줄 Attribute와 어떠한 연산을 적용할 것인지)
FGameplayModifierInfo Modifier;
Modifier.Attribute = FGameplayAttribute(특정 Attribute);
Modifier.ModifierOp = EGameplayModOp::Additive;
//변화시킬 값을 설정
FScalableFloat DamageAmount(-30.f);
FGameplayEffectModifierMagnitude ModMagnitude(DamageAmount);
Modifier.ModifierMagnitude = ModMagnitude;
Modifiers.Add(Modifier);
}
하지만 BP로는 에디터에서 다음과 같이 간단하게 변경할 수 있다.
주요한 옵션들은 다음과 같다.
- Duration Policy: 기간 설정(일시적, 특정 기간, 무한)
- Components: GE가 발동할 때, 어떠한 동작을 할 것인지
- Modifier: GE를 적용할 Attribute와 설정
- Executions: GE에 대한 복잡한 로직을 담당하는 클래스를 사용하여 GE 적용
- GameplayCues: GE가 발동할 때, 실행할 GC
- Stacking: GE를 얼마나 중첩시킬 지에 대한 옵션
주요한 옵션 중 가장 중요한 것은 Components와 Modifier이다.
우선, Components는 다음과 같은 옵션이 있다.
몇 가지만 설명하자면, Target Tags Gameplay Effect Component는 해당 GE를 통해 어떠한 태그를 부여할 것인지에 대해 설정할 수 있다.
이런 식으로 간단한 설정으로 다양한 기능을 구현할 수 있다.
Modifier는 GE를 통해 영향을 받을 Attribute에 대한 명세를 하는 옵션이다.
어떠한 Attribute에 영향을 얼마나 어떻게 줄 것인지에 대해 설정하면 된다.
이때, 기존에 있는 AttributeSet의 값으로부터 데이터를 받아와 설정할 수도 있고 복잡한 계산을 따로 수행하는 클래스를 이용할 수도 있다.
GE에서 특이한 경우가 하나 있다.
예를 들어, 화상을 입어 5초간 10씩 대미지를 입는 상황을 가정해 보자.
그렇다면 GE를 통해 Health값을 5초간 10 만큼씩 바꾸면 된다.
하지만, 이렇게 구현하면 CurrentValue만 변경되고 BaseValue는 변경되지 않는다.
이는 Period의 옵션을 통해 변경해 주면 BaseValue를 바꿔 해결할 수 있다.
GE를 설정하는 것은 어느 정도 마무리하였고 실행하는 부분에 대해 알아봐야 한다.
GE를 실행하기 위해서는 FGameplayEffectSpecHandle이라는 특수한 구조체가 필요하다.
FGameplayEffectSpecHandle의 내부는 복잡한 구조체가 겹쳐져 있는데 그림으로 정리하면 다음과 같다.
GamepalyEffectContext는 GE에서 계산에 필요한 데이터를 담은 객체이다.
가해자, 가해수단, HitResult 같은 정보들이 있다.
GameplayEffectSpec에는 레벨, 모디파이어 등 각종 태그에 대한 정보가 있다.
이러한 구조체를 제작하는 함수는 다음과 같다.
FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(GE, Level);
GE를 실행하는 함수는 크게 두 가지이다.
TArray<FActiveGameplayEffectHandle> ApplyGameplayEffectSpecToTarget(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle, const FGameplayAbilityTargetDataHandle& TargetData) const;
FActiveGameplayEffectHandle ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) const;
TArray<FActiveGameplayEffectHandle> BP_ApplyGameplayEffectToTarget(FGameplayAbilityTargetDataHandle TargetData, TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
FActiveGameplayEffectHandle BP_ApplyGameplayEffectToOwner(TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
위에는 BP가 붙지 않은 버전이고, 밑에는 BP가 붙은 버전이다.
실행하는 로직은 크게 다르지 않은데, BP가 붙은 버전이 인자가 더 간단하니 BP를 사용하는 것이 좋다고 생각한다.
GE를 통해 간단하게 Cost와 Cooldown 기능을 구현할 수 있다.
Cost는 어떠한 행동을 할 때 소모되는 비용이라고 생각하면 된다.
예를 들어, 스킬을 사용하는데 필요한 마나, 기력, 에너지 등이 있다.
GE의 모디파이어를 설정한 뒤, GA의 Cost옵션에 설정하면 된다.
Cooldown도 쉽게 구현할 수 있다.
Cost와 크게 다른 부분은 없지만, 한 가지 유의해야 할 점은 Cooldown의 Duration Policy는 Has Duration이어야 한다.
어찌 보면 당연한 이야기이다. 하지만, 이렇게 설정하지 않으면 제대로 동작하지 않는다.
이렇게 설정한 GE를 GA의 Cooldown 옵션에 넣어주면 된다.
설정은 모두 마쳤다. 하지만, 이렇게 실행해 보면 Cost와 Cooldown이 동작하지 않는다.
Cost와 Cooldown은 GA가 Commit 되지 않으면 실행되지 않기 때문이다.
따라서, GA의 ActivateAbility()에서 명시적으로 Commit 해주어야 한다.
void UGA_Skill::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
...
CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo);
}
GC
GC는 시각 이펙트나 사운드와 같은 게임 로직과 무관한 시각적, 청각적 기능을 담당하는 클래스이다.
GC에는 두 가지 종류가 있다.
- 스태틱 게임플레이 큐: 일시적으로 발생하는 특수효과에 사용, Execute 이벤트 발동
- 액터 게임플레이 큐: 일정 기간 동안 발생하는 특수효과에 사용, Add/Remove 이벤트 발동
GC 역시 C++로 구현할 수 있지만, BP로 구현하는 것이 훨씬 생산적이다.
GC는 태그를 통해 발동되며, GC의 로직에는 이미터를 생성하거나 사운드를 재생하는 등의 간단한 동작을 하기 때문에 굳이 코딩을 할 이유가 없다.
만약, C++로 GC를 제작하기 위해서는 다음과 같이 하면 된다.
bool UGC_AttackHit::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const
{
const FHitResult* HitResult = Parameters.EffectContext.GetHitResult();
if (HitResult)
{
UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, HitResult->ImpactPoint, FRotator::ZeroRotator, true);
}
return false;
}
스태틱 GC이기 때문에 OnExecute를 override 하여 이미터를 생성하는 작업을 진행하였다.
이를 BP로 구현하면 다음과 같다.
로직은 동일하다.
하지만 BP에서는 에셋을 바로 참조할 수 있어 간단하게 구현이 가능하다.
GC의 재생은 GameplayCueManager가 관리하여 다른 시스템과 분리된 구조를 갖는다.
GC를 C++로 실행하기 위해서는 FGameplayEffectContextHandle를 만들고 FGameplayCueParameters에 할당하여 ASC를 통해 실행할 수 있다.
FGameplayEffectContextHandle CueContextHandle = ASC->MakeEffectContext();
CueContextHandle.AddHitResult(HitResult);
FGameplayCueParameters CueParam;
CueParam.EffectContext = CueContextHandle;
TargetASC->ExecuteGameplayCue(GameplayCueTag, CueParam);
GC를 C++로 명시적으로 실행하는 방법 외에도 GE에 설정하여 태그를 통해 실행할 수 있다.
GameplayCueTag에 원하는 GC의 태그를 설정하면 GE가 발동될 때 자동으로 GC를 실행한다.
if (!Owner->bSuppressGameplayCues)
{
for (const FGameplayEffectCue& Cue : Effect.Spec.Def->GameplayCues)
{
Owner->UpdateTagMap(Cue.GameplayCueTags, 1);
if (bInvokeGameplayCueEvents)
{
Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::OnActive);
Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::WhileActive);
}
if (ShouldUseMinimalReplication())
{
for (const FGameplayTag& CueTag : Cue.GameplayCueTags)
{
// We are now replicating the EffectContext in minimally replicated cues. It may be worth allowing this be determined on a per cue basis one day.
// (not sending the EffectContext can make things wrong. E.g, the EffectCauser becomes the target of the GE rather than the source)
Owner->AddGameplayCue_MinimalReplication(CueTag, Effect.Spec.GetEffectContext());
}
}
}
}
GE코드의 일부분인데, 이 부분에서 Tag를 통해 GC를 실행하는 것 같다.
GC의 태그를 설정할 때 주의할 점이 있다.
태그의 첫 부분이 GameplayCue로 시작하는 태그를 사용해야 한다.
GC를 사용하는 흐름은 다음과 같다.
Tag
태그는 GAS에서 만든 것이 아니라 GAS 시스템 외에도 다른 곳에서 사용될 수 있다.
하지만, 태그는 GAS 시스템에서 효과적으로 사용된다.
태그는 FName으로 관리되는 경량의 표식 데이터이다.
프로젝트 설정으로 통행 추가 및 삭제 등의 관리가 가능하며, 여기서 추가된 태그들은 DefaultGameplayTags.ini 같은 파일에 저장되어 코드로도 편집이 가능하다.
태그는 계층 구조로 구성되어 있어 체계적인 관리가 가능하다.
예를 들어, 게임에서 캐릭터는 다양한 상태를 가질 수 있다.
점프하는 상태, 공격 중인 상태, 피해를 받지 않는 상태 등 다양한 상황이 있을 수 있다.
러한 상태들을 계층구조로 나타내면 다음과 같다.
이를 태그에서는 다음과 같이 표현한다.
- Actor.State.IsJumping
- Actor.State.IsAttacking
- Actor.State.Invinsible
이러한 태그들을 따로따로 관리할 수 있지만, 하나의 컨테이너에 묶어 관리할 수도 있다.
예를 들어, 스킬을 사용하려 할 때, 공격 중이거나, 점프 상태이거나, 죽었거나, 쿨다운이 걸려있는 상태라면 스킬을 사용할 수 없게 만들고 싶을 수 있다.
이러한 상태들을 스킬 사용을 막는 태그 컨테이너에 넣어 두고 스킬을 사용하려 할 때, 해당 컨테이너에 현재 상태가 포함되어 있는지 검사하면 쉽게 스킬 사용을 방지할 수 있다.
이렇듯, 태그 컨테이너는 다수의 태그를 모아 효율적으로 관리 및 체크할 수 있는 기능을 제공한다.
- HasTagExact: A.1 태그가 있을 때 A를 검색하면 true
- HasAny: A.1 태그가 있을 때, A와 B로 찾으면 true
- HasAnyExact: A.1태그가 있을때, A와 B로 찾으면 false
- HasAll: A.1, B.1 태그가 있을 때, A와 B로 찾으면 true
- HasAllExact: A.1, B.1태그가 있을 때, A와 B로 찾으면 false
GA에는 다양한 태그 컨테이너들이 있다.
이 태그 컨테이너에 적절한 태그를 넣어 관리하면 의존성을 없애고 간편하게 게임 로직을 전개할 수 있다.
다음은 GA의 태그 컨테이너에 대한 설명이다.
- AbilityTags: 어빌리티에 지정한 태그
- CancelAbilitiesWithTag: 태그로 어빌리티 취소
- BlockAbilitiesWithTag: 태그로 어빌리티 차단
- ActivationOwnedTags: 어빌리티 실행 시 태그 설정
- ActivationRequiredTags: 태그가 있어야만 어빌리티 실행
- ActivationBlockedTags: 태그가 있으면 어빌리티 실행 차단
- SourceRequiredTags: 시전자가 태그가 있어야 어빌리티 실행
- SourceBlockedTags: 시전자가 태그가 있으면 어빌리티 차단
- TargetRequiredTags: 시전 대상에 태그가 있어야 어빌리티 실행
- TargetBlockedTags: 시전 대상에 태그가 있으면 어빌리티 차단
태그는 GAS 시스템의 다양한 곳에서 사용된다.
다음은 GAS 시스템에서 태그가 사용되는 상황들이다.
- 태그로 상태 표현
if ((GetHealth() <= 0.f) && !bOutOfHealth)
{
Data.Target.AddLooseGameplayTag(CHARACTER_ISDEAD);
}
체력이 0 이하로 떨어지면 죽었다는 상태를 표시하기 위해 태그를 부착하는 부분이다.
- 태그 이벤트를 통한 Callback함수
ASC->GenericGameplayEventCallbacks.FindOrAdd(MYTAG).AddUObject(this, &CallbackFunc);
ASC에 태그가 추가되거나 삭제되었을 때 발동하는 이벤트를 태그를 통해 찾아 Callback을 실행할 수 있게 바인딩하는 것이다.
- 태그로 GA 실행
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwnerActor, TriggerGameplayTag, PayloadData);
- GE가 발동될 때 태그부여
- 태그를 통한 GC 실행
ASC->ExecuteGameplayCue(GCTag, CueParam);
- 태그를 통한 GE가 실행될 때 실행할 GC 설정