Ability System - GameplayAbility를 태그를 이용해 Block 해보자!
개요
GameplayAbility는 Unreal Engine에서 GAS 게임을 만드는데 필수적이며,
기본적으로 게임에서는 State라고 불리우는 클래스입니다
GameplayAbility는 C++ 에서 여러 가지 기능을 상속받아서 사용할 수 있지만,
간단하게 GameplayAbility <=> GameplayAbility 관계에서 특정 GameplayAbility가
발동하지 않도록 Block 처리를 해보겠습니다
그 전에 네트워크와 관련된 코드들은 전부 무시한 채 진행해보도록 하겠습니다
(Block 처리를 하는 방식은 네트워크와 무관함)
우선 UGameplayAbility에서 C++ 클래스에서 주석을 한번 살펴보겠습니다
GameplayAbility에는 다양한 기능들이 있으며, 오버라이딩을 해 사용할 수 있습니다
함수 이름만 보면 Block 처리할 함수는 명확하게 보입니다. 바로 CanActivateAbility 함수죠
.h 파일에서 CanActivateAbility를 찾아봅시다
CanActivateAbility
/** 해당 어빌리티를 활성화할 수 있는지 확인하는 함수 */
virtual bool CanActivateAbility
(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags = nullptr,
const FGameplayTagContainer* TargetTags = nullptr,
OUT FGameplayTagContainer* OptionalRelevantTags = nullptr
) const;
1. Handle : 해당 Ability를 생성할 때 반환하는 SpecHandle(기본적인 메타데이터를 소유한다)
2. ActorInfo : AvatarActor, OwnerActor 등 Ability를 소유하는 Actor(ASC)의 정보를 가진다
3. SourceTags, TargetTags : 해당 태그들로 Block을 임의로 설정할 수 있다
4. OptionalRelevantTags : 성공했는지, 실패했는지, 확인하는 태그(FaliureTag)로 사용된다
이제 해당 함수를 .cpp에서 파헤쳐봅시다
// AvatarActor가 존재해야한다(나머지 주석은 네트워크 관련)
AActor* const AvatarActor = ActorInfo ? ActorInfo->AvatarActor.Get() : nullptr;
if (AvatarActor == nullptr || !ShouldActivateAbility(AvatarActor->GetLocalRole()))
{
return false;
}
AvatarActor가 있어야지, ASC가 원할하게 동작할 수 있기 때문에 체크해준다
사용하는 곳은 네트워크쪽 밖에 없기 때문에 넘어가도록하자
// static 변수로 FGameplayTagContainer를 만들어주고, Reset를 수행한다
static FGameplayTagContainer DummyContainer;
DummyContainer.Reset();
FGameplayTagContainer& OutTags = OptionalRelevantTags ? *OptionalRelevantTags : DummyContainer;
// make sure the ability system component is valid, if not bail out.
UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get();
if (!AbilitySystemComponent)
{
return false;
}
FGameplayTagContainer를 static 변수로 선언하고, Reset을 진행한다
그 이유를 확인하기 위해 F12를 눌렀더니.. 바로 static 변수로 선언한 이유를 알았다
변수를 계속 생성하기에는 해당 구조체가 크다보니 초기화하는 방식으로 간 것이다
이후 OutTag로 변수를 할당해주는데, OptionalRelevantTags값이 있다면 넣어주고,
아니라면 Dummy를 넣어준다
마지막으로 AbilitySystemComponent를 사용하기 위해 ActorInfo에서 가져오자
ASC의 값이 변경되지 않아야 하기 때문에 포인터 뒤에 const를 붙인다
FGameplayAbilitySpec* Spec = AbilitySystemComponent->FindAbilitySpecFromHandle(Handle);
if (!Spec)
{
ABILITY_LOG(Warning, TEXT("CanActivateAbility %s failed, called with invalid Handle"), *GetName());
return false;
}
FindAbilitySpecFromHandle로 ASC에서 해당 Handle이 사용중인지 확인하고,
Handle(고유값)에 대한 AbilitySpec을 순회해 가져온다
if (AbilitySystemComponent->GetUserAbilityActivationInhibited())
{
/**
* Input is inhibited (UI is pulled up, another ability may be blocking all other input, etc).
* When we get into triggered abilities, we may need to better differentiate between CanActivate and CanUserActivate or something.
* E.g., we would want LMB/RMB to be inhibited while the user is in the menu UI, but we wouldn't want to prevent a 'buff when I am low health'
* ability to not trigger.
*
* Basically: CanActivateAbility is only used by user activated abilities now. If triggered abilities need to check costs/cooldowns, then we may
* want to split this function up and change the calling API to distinguish between 'can I initiate an ability activation' and 'can this ability be activated'.
*/
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: %s could not be activated due to GetUserAbilityActivationInhibited"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability));
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("%s could not be activated due to GetUserAbilityActivationInhibited"), *GetNameSafe(Spec->Ability));
}
return false;
}
이 코드는 GetUserAbilityActivationInhibited를 살펴보니, 아직 미완성 코드인 것 같다
신경쓰지 않아도 될 것 같다(먼 훗날에 추가되면 확인해보도록 하자)
UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();
if (!AbilitySystemGlobals.ShouldIgnoreCooldowns() && !CheckCooldown(Handle, ActorInfo, OptionalRelevantTags))
{
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: %s could not be activated due to Cooldown"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability));
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("%s could not be activated due to Cooldown"), *GetNameSafe(Spec->Ability));
}
return false;
}
if (!AbilitySystemGlobals.ShouldIgnoreCosts() && !CheckCost(Handle, ActorInfo, OptionalRelevantTags))
{
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: %s could not be activated due to Cost"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability));
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("%s could not be activated due to Cost"), *GetNameSafe(Spec->Ability));
}
return false;
}
AbilitySystemGlobals를 가져와 Cooldown과 Cost를 체크한다
CheckCooldown : 대표적으로 스킬이 있으며, 시간초를 이용해 제어하는 함수
CheckCost : 대표적으로 마나가 있으며, 마나가 없다면 스킬을 사용할 수 없도록 제어하는 함수
해당 함수들(CheckCooldown, CheckCost) 을 확인하고 싶다면 확인해보자
if (!DoesAbilitySatisfyTagRequirements(*AbilitySystemComponent, SourceTags, TargetTags, OptionalRelevantTags))
{ // If the ability's tags are blocked, or if it has a "Blocking" tag or is missing a "Required" tag, then it can't activate.
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: %s could not be activated due to Blocking Tags or Missing Required Tags"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability));
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("%s could not be activated due to Blocking Tags or Missing Required Tags"), *GetNameSafe(Spec->Ability));
}
return false;
}
이 부분이 가장 중요한 부분이며, SourceTags, TargetTags를 사용한다
내가 직접 넣어 SourceTags, TargetTags를 이용해 Block 제어가 가능하다
DoesAbilitySatisfyTagRequirements 코드를 살펴보는데,
해당 코드는 길기 때문에 잘라서 하나씩 파헤쳐보자
bool bBlocked = false;
bool bMissing = false;
UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();
const FGameplayTag& BlockedTag = AbilitySystemGlobals.ActivateFailTagsBlockedTag;
const FGameplayTag& MissingTag = AbilitySystemGlobals.ActivateFailTagsMissingTag;
// Check if any of this ability's tags are currently blocked
if (AbilitySystemComponent.AreAbilityTagsBlocked(AbilityTags))
{
bBlocked = true;
}
bBlocked, bMissing으로 bool 값을 나누어 OutTag에 FailureTag를 보내준다
AbilitySystemGlobals에 ActivateFailTagsBlockedTag값이 있으며,
해당 값은 ActivateFailTagsBlockedName을 통해 config로 변경할 수 있다
이후 AreAbilityTagsBlocked를 통해 AbilityTags가 Block되는 지 확인한다
AbilityTags는 현재 내가 사용하고 있는 GameAbility이다
bool UAbilitySystemComponent::AreAbilityTagsBlocked(const FGameplayTagContainer& Tags) const
{
// Expand the passed in tags to get parents, not the blocked tags
return Tags.HasAny(BlockedAbilityTags.GetExplicitGameplayTags());
}
HasAny 함수는 말그대로 해당 태그가 부모와 자식에 모두 포함되어있는 지
순회를 통해 확인하는 코드다
if (ActivationBlockedTags.Num() || ActivationRequiredTags.Num())
{
static FGameplayTagContainer AbilitySystemComponentTags;
AbilitySystemComponentTags.Reset();
AbilitySystemComponent.GetOwnedGameplayTags(AbilitySystemComponentTags);
if (AbilitySystemComponentTags.HasAny(ActivationBlockedTags))
{
bBlocked = true;
}
if (!AbilitySystemComponentTags.HasAll(ActivationRequiredTags))
{
bMissing = true;
}
}
현재 ASC에서 작동하는 GameplayAbility를 가져와서 Block, Missing을 정해준다
Missing : 해당 어빌리티가 필요한 데 못찾으면 true
Block : 해당 어빌리티는 들어오면 안되는데 들어왔다면 true
if (SourceTags != nullptr)
{
if (SourceBlockedTags.Num() || SourceRequiredTags.Num())
{
if (SourceTags->HasAny(SourceBlockedTags))
{
bBlocked = true;
}
if (!SourceTags->HasAll(SourceRequiredTags))
{
bMissing = true;
}
}
}
if (TargetTags != nullptr)
{
if (TargetBlockedTags.Num() || TargetRequiredTags.Num())
{
if (TargetTags->HasAny(TargetBlockedTags))
{
bBlocked = true;
}
if (!TargetTags->HasAll(TargetRequiredTags))
{
bMissing = true;
}
}
}
SourceTags, TargetTags를 가지고 비교한다
이 때 비교변수도 Source, Target을 맞춰줘야한다
if (bBlocked)
{
if (OptionalRelevantTags && BlockedTag.IsValid())
{
OptionalRelevantTags->AddTag(BlockedTag);
}
return false;
}
if (bMissing)
{
if (OptionalRelevantTags && MissingTag.IsValid())
{
OptionalRelevantTags->AddTag(MissingTag);
}
return false;
}
return true;
Block, Missing라면 관련된 bool값에 FailureTag를 넣어주고 false를 반환
모든 예외처리가 무사하게 넘어갔다면 true를 반환한다
이렇게 되면 무사히 DoesAbilitySatisfyTagRequirements를 넘어갈 수 있다
// Check if this ability's input binding is currently blocked
if (AbilitySystemComponent->IsAbilityInputBlocked(Spec->InputID))
{
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: %s could not be activated due to blocked input ID %d"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability), Spec->InputID);
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("%s could not be activated due to blocked input ID %d"), *GetNameSafe(Spec->Ability), Spec->InputID);
}
return false;
}
SpecHandle에 있는 변수를 이용해 Input으로도 바인딩을 막아줄 수 있다
if (bHasBlueprintCanUse)
{
if (K2_CanActivateAbility(*ActorInfo, Handle, OutTags) == false)
{
if (FScopedCanActivateAbilityLogEnabler::IsLoggingEnabled())
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: CanActivateAbility on %s failed, Blueprint override returned false"), *GetNameSafe(ActorInfo->OwnerActor.Get()), *GetNameSafe(Spec->Ability));
UE_VLOG(ActorInfo->OwnerActor.Get(), VLogAbilitySystem, Verbose, TEXT("CanActivateAbility on %s failed, Blueprint override returned false"), *GetNameSafe(Spec->Ability));
}
return false;
}
}
블루프린트에서도 CanActivateAbility가 false라면 진행되지 않도록하자
이렇게 모든 예외처리가 끝나면 true를 반환하게 되며,
C++ 에서의 사용 부분은 UAbilitySystemComponent::InternalTryActivateAbility를 참고하면 된다
이후 블루프린트에서는 아주 간단하게 사용할 수 있습니다
닷지(구르기, 회피 등)를 할 때는 공격을 못하도록 설정
블루프린트, C++로 설정 가능하게 된다.
이렇게 C++에서 CanActivateAbility를 살펴봤는데요
원리는 알고 보면, 태그가 어떤 방식으로 사용되고 있는지 이해할 수 있습니다
어떻게 GameplayAbility가 Block 되는 지 원리를 알아보았습니다