새로운 프로젝트로 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에 직접 전달함으로써 구현할 수 있다.