새로운 프로젝트로 GAS를 적극 활용하게 되었기 때문에, GAS가 무엇인지 공부해보면서 개인적으로 정리하는 글입니다.
잘 정리된 위의 글들을 한글로 옮겨 정리하면서 공부한 내용을 업로드합니다.
Gameplay Ability System의 약자.
RPG 또는 MOBA류 게임에서 많이 볼 수 있는, 캐릭터의 능력이나 속성 등을 편리하게 관리할 수 있게 도와주는 프레임워크의 일종.
다음과 같은 기능을 제공한다고 정리할 수 있다.
GameplayAbilites)Attributes)GameplayEffects)GameplayTag 를 적용하기 (GameplayTags)GameplayCue)
Gameplay Abilities 플러그인을 프로젝트에 추가
.Build.cs 파일의 PrivateDependencyModuleNames 에 "GameplayAbilities", "GameplayTags", "GameplayTasks" 추가
(4.24 ~ 5.2 버전의 경우) TargetData 를 사용하기 위해서 UAbilitySystemGlobals::Get().InitGlobalData() 을 호출
AbilitySystemComponent (이하 ASC) 는 액터 컴포넌트의 일종이며 모든 시스템의 상호작용을 책임진다. GameplayAbilities, Attribute, GameplayEffects 등을 사용하려는 액터는 반드시 ASC가 부착되어 있어야 한다.
그리고 이렇게 ASC가 부착된 액터는 그 ASC의 OwnerActor 로 간주된다. 또한 ASC의 '물리적인 존재' (physical representation)은 AvatarActor 라고 지칭한다. 이 둘은 같을 수도 있고 다를 수도 있다.
예를 들어 리그오브레전드에서 '미니언' 은 스스로 ASC를 가지고 있으며 AI에 따라 움직이는데 이는 두 개가 같은 케이스이고, 유저가 조종하는 챔피언의 경우 OwnerActor는 PlayerState이고, 챔피언의 캐릭터 클래스는 AvatarActor가 된다. 따라서 이는 두 개가 서로 다른 케이스이다. 캐릭터가 죽고 나서 리스폰되는 등의 경우에 애트리뷰트나 이펙트 등은 일관되게 유지되어야 하는 경우가 있기 때문에, ASC의 이상적인 위치는 PlayerState인 것이다.
ASC는 현재 활성화된 GameplayEffects를 FActiveGameplayEffectsContainer ActiveGameplayEffects 에 저장하며, 부여된 Gameplay Abilities는 FGameplayAbilitySpecContainer ActivatableAbilities 라는 구조에 저장하는데 (이름은 알 필요 없고 그냥 이러한 이름의 이런 자료구조가 있다 정도), 만약 이 아이템들을 순회할 일이 있다면 먼저 ABILITYLIST_SCOPE_LOCK() 을 잡아야 한다. 이는 순회 도중 어빌리티 리스트 정보가 변경될 우려가 있기 때문인데, 이와 비슷한 맥락으로 순회하는 과정에서 리스트를 훼손해선 안 된다.
내부적으로는 AbilityScopeLockCount 라고 하는, 세마포어와 비슷한 역할을 하는 값이 존재하며, 리스트를 clear 하는 등의 함수에서 이 값을 확인하여 현재 락이 잡혀있는지 판단하는 데 사용한다.
ASC가 정의하는 Replication Mode에는 3가지 종류가 있다.
여기서 다루는 Replication의 대상은 GameplayEffects, GameplayTags, GameplayCues 이다. 예외적으로 Attributes는 그들의 AttributeSet에 의해 복제된다.
Full : 싱글플레이어에서 사용. 모든 이펙트가 모든 클라에 복제.Mixed : 멀티플레이어, 플레이어에 의해 컨트롤되는 액터에서 사용. 이펙트는 owning client에게만 복제되며, 태그와 큐는 모두에게 복제됨.Minimal : 멀티플레이어, AI에 의해 컨트롤되는 액터에서 사용. 이펙트는 복제되지 않고 태그와 큐는 모두에게 복제.Mixed 모드를 사용할 때 주의사항은 OwnerActor의 Owner가 컨트롤러여야 한다는 것. PlayerState의 Owner는 기본적으로 컨트롤러이지만 캐릭터는 그렇지 않다. 만약 PlayerState가 아닌 다른 액터를 OwnerActor로 사용할 때에는 SetOwner()를 통해 유효한 컨트롤러를 직접 지정해 주어야 함.
OwnerActor의 생성자에서 ASC에 대한 초기화가 이루어지는 것이 보편적. 이 작업은 반드시 C++에서 이루어져야 함.
AGDPlayerState::AGDPlayerState()
{
// Create ability system component, and set it to be explicitly replicated
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
//...
}
ASC는 반드시 서버와 클라이언트 모두에서 초기화되어야 한다.
ASC가 Pawn에 부착되어 있는 플레이어 컨트롤-캐릭터의 경우, 서버에 대해선 폰의 PossessedBy() 함수에서, 클라이언트에 대해선 컨트롤러의 AcknowledgePossession() 에서 초기화하는 편.
void APACharacterBase::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
// ASC MixedMode replication requires that the ASC Owner's Owner be the Controller.
SetOwner(NewController);
}
void APAPlayerControllerBase::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
APACharacterBase* CharacterBase = Cast<APACharacterBase>(P);
if (CharacterBase)
{
CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
}
//...
}
ASC가 PlayerState에 부착되어 있는 플레이어 컨트롤-캐릭터의 경우, 서버에 대해선 폰의 PossessedBy() 그리고 클라이언트에 대해선 폰의 OnRep_PlayerState() 에서 초기화하는 편.
// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}
//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
}
// ...
}
태그는 GameplayTagManager에 등록되며, 점 . 으로 구분되는 계층 구조를 가진다. 이러한 구조는 태그를 이해하고 분류하는 데 효과적이다.
예를 들어 플레이어가 '스턴' 상태에 빠졌다고 가정하면, State.Debuff.Stun 태그를 적용할 수 있다. 그럼 태그만 보고도 '디버프의 일종인 스턴 상태가 적용돼 있다' 는 것을 알 수 있다.
태그를 적용할 때는 ASC에 존재하는 IGameplayTagAssetInterface 를 통해 함수를 사용하는 것이 보편적
FGameplayTagContainer 라는 자료구조에 태그들을 저장할 수 있는데, 단순한 TArray 보다 효율적이다.
프로젝트 세팅에서 Fast Replication 이 활성화 돼있다면 FGameplayTagContainer는 태그들을 효율적으로 복제할 수 있는데, Fast Replication을 켠다고 해서 문제될 건 보통 없으므로 웬만해선 켜는 것이 좋다. Fast Replication는 클라이언트와 서버가 같은 태그 리스트를 갖고 있어야 한다.
FGameplayTagCountContainer 에 저장된 태그는 TagMap이 있으며 이 태그의 인스턴스 개수를 저장하고 있다. TagMapCount 라는 변수는 HasTag(), HasMatchingTag() 등의 함수에서 태그가 있는지 확인하는 데에 사용된다. (잘 이해 못 함)
태그는 DefaultGameplayTags.ini에 정의되어 있어야 한다. 언리얼 엔진은 프로젝트 세팅에서 이 태그들을 관리할 수 있는 인터페이스를 제공한다.
태그들은 GameplayEffect에 의해 복제가 되지만 LooseGameplayTag 는 복제되지 않게 할 수 있으며 이 경우 직접 관리해 주어야 한다.
태그의 레퍼런스 얻기
FGameplayTag::RequestGameplayTag(FName("Your.GameplayTag.Name"))
GameplayTagManager는 많은 복잡한 함수들을 제공하며, UGameplayTagManager::Get().FunctionName 와 같이 사용한다.
ASC는 태그가 추가되거나 삭제됨에 따른 델리게이트를 가지고 있으며 EGameplayTagEventType 라는 타입을 통해 추가됐을 때/삭제됐을 때 등의 조건을 걸 수 있다.
콜백 함수는 태그와 새로운 TagCount를 파라미터로 갖는다.
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
FGameplayAttributeData 라는 구조체에 정의된 float 값이다. 캐릭터의 HP, 가지고 있는 포션의 수, 현재 레벨 등 다양한 값들을 표시할 수 있다. 만약 액터와 관련된 float 값을 사용할 것이라면 애트리뷰트의 사용을 고려해봄직하다.
애트리뷰트는 AttributeSet 에서 정의되며 존재한다. 이 셋은 복제 대상 애트리뷰트를 복제하는 역할을 수행한다.
애트리뷰트는 두 종류의 값으로 구성된다. BaseValue 와 CurrentValue 이다. BaseValue는 영구적인 값이며 CurrentValue는 BaseValue에 더해 특정한 수정이 들어간 값이라고 보면 된다.
예컨대 캐릭터가 600unit/s의 이동 속도를 BaseValue로 가지고 있다고 가정해 보자. GameplayEffects에서 어떠한 효과도 적용하지 않았다면 당연히 CurrentValue도 600unit/s 이다. 여기에 50unit/s 의 이동속도를 더해주는 버프를 받았다고 하면, CurrentValue 값만 650unit/s로 바뀐다.
간혹 BaseValue를 애트리뷰트의 '최댓값' 으로 생각하고, 또 그렇게 사용하는 경우가 있는데 이는 틀린 접근이라고 한다.
변경될 수 있는 최댓값 등은 별개의 애트리뷰트로 처리하는 것이 좋고, 만약 클램핑을 위해 최댓값/최솟값을 하드코딩해야 한다면 FAttributeMetaData를 이용해 DataTable을 만드는 방법이 있다. 아니면 이 또한 애트리뷰트 셋에 저장하는 방법도 있다.
UI나 다른 게임플레이를 업데이트하는 애트리뷰트의 변화에 대한 콜백 함수를 델리게이트 바인딩할 수 있다.
UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute) 함수를 통해 애트리뷰트가 바뀔 때마다 호출되는 델리게이트를 얻을 수 있고, FOnAttributeChangeData 등의 파라미터를 제공한다.
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);
애트리뷰트를 정의하고, 저장하고 변화를 관리하는 셋이다. UAttributeSet 타입의 TSubclass로 사용할 수 있다.
하나의 ASC는 하나 이상의 애트리뷰트 셋을 가질 수 있다. 무시할 만한 수준의 낮은 메모리 오버헤드를 갖고 있기 때문에 몇 개의 애트리뷰트 셋을 가지느냐는 디자인 재량이다. 하나의 큰(monolithic한) 셋을 가지고 액터들끼리 공유하게 할 수도 있고 또는 필드별로 세부적으로 셋을 분리할 수도 있다. 예컨대 체력과 관련된 애트리뷰트 셋, 마나와 관련된 애트리뷰트 셋 이런 식으로.
그러나 둘 이상의 애트리뷰트 셋을 사용할 것이라면 ASC에 같은 클래스의 애트리뷰트 셋을 둘 이상 사용해선 안 된다. 만약 같은 클래스의 애트리뷰트 셋을 여럿 가지고 있다면 이들 중 어떤 것을 사용할지 지정할 수 없다.
(애트리뷰트 셋의 '클래스' 개념이 아직은 모호한데 아마 Subclass 언급이 있는 것으로 보아 하나의 애트리뷰트셋 클래스에서 파생하는 식으로 사용하는 듯)
애트리뷰트 셋은 런타임에서 ASC에 추가/삭제 될 수 있다.
그러나 셋을 삭제하는 것은 다소 위험할 수 있다. 예컨대 애트리뷰트 셋이 서버보다 클라이언트에서 먼저 삭제되었는데 이 때 애트리뷰트 값의 변경이 클라이언트로 복제되지 않은 상황이라면 애트리뷰트 값은 셋을 찾지 못하게 되므로 크래쉬가 발생한다.
애트리뷰트는 오직 C++에서만, 애트리뷰트셋의 헤더 파일에서만 정의될 수 있다. 모든 애트리뷰트 셋의 헤더 파일에서 아래와 같은 매크로 블록을 정의하는 것이 추천된다.
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
만약 복제되는 체력 애트리뷰트라면 다음과 같이 정의될 수 있다:
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
FGameplayAttributeData 형 데이터를 선언하고 ATTRIBUTE_ACCESSORS 에 바인딩한다.
그리고 RepNotify 함수는 아래와 같이 Prediction system에서 사용되는 GAMEPLAYATTRIBUTE_REPNOTIFY 를 명시
void UGDAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGDAttributeSetBase, Health, OldHealth);
}
또한 복제의 대상이기 때문에 GetLifetimeReplicatedProps 에도 추가해 주어야 함.
void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}
Attribute를 정의할 때 ATTRIBUTE_ACCESSORS 매크로를 사용한 경우 C++에서 호출할 수 있는 각 AttributeSet에 자동으로 초기화 함수가 생성된다.
// InitHealth(float InitialValue) is an automatically generated function for an Attribute 'Health' defined with the `ATTRIBUTE_ACCESSORS` macro
AttributeSet->InitHealth(100.0f);
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) 는 애트리뷰트의 Current value의 변경이 일어나기 전 반응할 수 있는 함수이다.
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data) 함수는
애트리뷰트의 BaseValue가 변경된 이후 트리거된다.
애트리뷰트나 태그에 변화를 줄 수 있다. 데미지를 가하거나 회복을 시키는 등 즉각적인 변화, 그리고 이로운 버프를 제공하거나 해로운 디버프를 가하는 등 일정 시간동안 변화를 주는 이펙트들이 포함된다.
UGameplayEffect 클래스는 data-only 클래스여야 하며 하나의 게임플레이 이펙트만 정의해야 한다. 다른 추가적인 로직이 추가되면 안 된다.
Instant : 애트리뷰트의 BaseValue의 영구적인 변화Duration : 애트리뷰트의 CurrentValue의 일시적인 변화. 스스로 만료되거나 직접 제거할 수 있다.Infinite : 애트리뷰트의 CurrentValue의 일시적인 변화. GameplayEfffect가 제거되면 함께 제거된다. 스스로 만료되진 않으며 반드시 직접 제거해야 한다.GameplayAbilites, 그리고 ASC 의 다양한 함수들을 이용해 적용할 수 있다.
또한 ASC에 infinite/duration 이펙트가 적용된 것을 listen할 수 있는 델리게이트 및 콜백 함수가 존재한다.
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
Apply와 마찬가지로 델리게이트 및 콜백 함수가 존재
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);
Modifier는 애트리뷰트를 변경할 수 있으며, 이펙트는 모디파이어를 갖지 않을 수 있고 여러 개도 가질 수 있다.
하나의 모디파이어는 특정한 operation를 통한 하나의 애트리뷰트 변화만을 담당한다.
Add : 모디파이어의 지정된 애트리뷰트 값을 더함Multiply : 모디파이어의 지정된 애트리뷰트 값을 곱함Divide : 모디파이어의 지정된 애트리뷰트 값을 나눔Override : 모디파이어의 지정된 애트리뷰트 값을 오버라이드 (덮어씀)액터가 게임 내에서 할 수 있는 특정한 액션 또는 스킬. 동시에 여러 어빌리티를 수행할 수 있다. (예: 달리면서 총 쏘기) 점프, 달리기, 총쏘기, 문 열기, 포션 마시기 등등 정말 많은 기능들이 있을 수 있다.
기본 움직임 인풋이나 UI와의 상호작용은 GameplayAbilities 에서 구현하지 않는다. (꼭 그런건 아니지만, 추천 사항)

간단한 어빌리티의 실행 흐름을 플로우차트로 나타낸 것.
어빌리티를 활성화 할 수 있는지를 검증하고, 활성화하고, GameplayEffects에 적용하고 끝마치는 등의 순서를 따른다.

좀 더 복잡한 어빌리티의 예시. 어빌리티들 중에서는 여러 어빌리티와 연관될 수 있다. 예컨대 기존의 동작을 취소하고 다른 동작을 취한다든가 하는 것들이 해당한다.
GameplayAbilities는 Simulated Proxy에서 동작하지 않는다. AbilityTasks, GameplayCues 를 통해 Simulated Proxy에게 복제된 것처럼 시각 효과를 적용한다.
(아직 잘은 모르겠으나, 에픽 측의 코멘트를 보았을 때 어빌리티들은 클라이언트들에 복제되지 않는 것이 바람직하다는 듯 보임)
ASC에 다이렉트로 인풋을 바인딩해서 특정한 어빌리티를 수행하게 할 수 있다. 먼저 enum 클래스를 만들어 인풋 액션을 바이트 단위로 매핑하는 작업이 필요하다. 예시 코드를 보면 :
UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
// 0 None
None UMETA(DisplayName = "None"),
// 1 Confirm
Confirm UMETA(DisplayName = "Confirm"),
// 2 Cancel
Cancel UMETA(DisplayName = "Cancel"),
// 3 LMB
Ability1 UMETA(DisplayName = "Ability1"),
// 4 RMB
Ability2 UMETA(DisplayName = "Ability2"),
// 5 Q
Ability3 UMETA(DisplayName = "Ability3"),
// 6 E
Ability4 UMETA(DisplayName = "Ability4"),
// 7 R
Ability5 UMETA(DisplayName = "Ability5"),
// 8 Sprint
Sprint UMETA(DisplayName = "Sprint"),
// 9 Jump
Jump UMETA(DisplayName = "Jump")
};
ASC가 캐릭터에 존재한다면 SetupPlayerInputComponent() 에서 ASC에 대한 바인딩을 수행할 수 있고,
PlayerState에 존재한다면 잠재적으로 Race condition이 발생할 수 있으므로 SetupPlayerInputComponent() 와 OnRep_PlayerState() 에서 인풋 바인딩을 진행하는 것이 좋다.
GameplayAbilities 는 오직 한 프레임만 실행한다. 따라서 시간이 지난 후 발생하는 이벤트, 또는 델리게이트의 fire에 반응해야 하는 포인트에서는 적합하지 않다. 이러한 작업을 수행하기 위해서 Ability Task 라는 동작을 사용한다.
GAS는 다음과 같은 태스크를 지원한다 :
RootMotionSource 를 사용한 캐릭터의 이동이외에도 많은 태스크를 지원한다.
게임플레이 큐는 게임플레이와 직접적 연관이 없는 것들, 예컨대 사운드, 파티클 이펙트, 카메라의 흔들림 등을 실행한다. 이 큐들은 보통 복제 및 예측된다.
적절한 게임플레이 태그를 이벤트 타입과 함께 보냄으로써 큐를 트리거할 수 있다.

트리거되어야 하는 게임플레이 큐 태그를 명시한다.

/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();
위 블루프린트 노드들 또는 C++ 함수들을 이용해 트리거할 수 있다.
게임플레이 큐를 트리거하기 위한 함수들은 기본적으로 복제의 대상이 된다. 모든 큐 이벤트는 멀티캐스트 RPC이다. 이것은 상당히 많은 RPC 호출을 초래하게 되는데, 따라서 GAS는 한 번의 net update 동안 최대 두 번의 같은 게임플레이 태그를 시행한다. 따라서 사용할 수 있는 한 Local GameplayCue를 사용하여 이것을 피할 수 있다.
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
void UPAAbilitySystemComponent::ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Executed,
GameplayCueParameters);
}
void UPAAbilitySystemComponent::AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters);
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::WhileActive, GameplayCueParameters);
}
void UPAAbilitySystemComponent::RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Removed, GameplayCueParameters);
}
큐는 추가적인 정보를 담고 있는FGameplayCueParameters 구조체 타입의 파라미터를 받는다. 만약 큐를 직접 트리거할 일이 있다면 이 FGameplayCueParameters를 직접 채워서 전달해야 한다. 게임플레이 이펙트에 의해 큐가 트리거 될 때는 아래 변수들이 자동으로 파라미터를 채워 주게 된다.
파라미터 구조체 내부의 SourceObject 변수는 큐를 수동으로 트리거할 때 임의 데이터를 전달하기에 좋은 장소가 된다.
가끔 큐를 트리거하고 싶지 않을 때가 있다. 예를 들어 공격을 가드했을 때는 충돌 이벤트와 바인딩 된 충돌 이펙트를 트리거하지 않을 수 있다.
이것을 GameplayEffectExecutionCalculations 내부에서 OutExecutionOutput.MarkGameplayCueHandleManually() 함수를 통해 큐 이벤트를 타겟 또는 소스의 ASC에 직접 전달함으로써 구현할 수 있다.