GAS 동작원리
GAS를 적용하기 위해서는 기본적인 규칙을 지켜야 한다.
- GAS를 적용할 액터에 AbilitySystemInterface의 GetAbilitySystemComponent()를 구현해야 한다.
- GetAbilitySystemComponent에서는 액터가 소유한 ASC(AbilitySystemComponent)를 return 해줘야 한다.
이렇게 설정된 ASC에 Ability를 부여하고 원하는 타이밍에 Ability를 발동시키면 된다.
간단한 예를 들어보자.
어떤 액터가 게임이 시작함과 동시에 계속하여 반복하는 행위를 한다고 가정해보자.
그렇다면 반복되는 행위를 Ability로 만들고 BeginPlay나 특정한 시점에 Abiltiy를 활성화시키면 된다.
예시
앞에서 말한 대로 예시를 만들어보자.
게임 시작과 동시에 회전했다 멈췄다를 반복하는 액터를 만든다고 해보자.
그렇다면 다음과 같은 과정으로 구현할 수 있다.
- 액터를 회전시키는 Ability를 만든다.
- 액터의 ASC에 생성한 Ability를 부여한다.(PostInitializeComponents가 적절한 시점이다.)
- 게임을 시작함과 동시에 부여받은 Ability를 활성화한다.
- 타이머를 설정하여 일정 주기마다 체크하여 회전 여부를 결정한다.
구현
우선 GAS를 적용시킬 액터 같은 기본적인 것은 이미 있다고 가정하고 핵심적인 부분만 정리해 보자.
//Actor
//.h
#include "AbilitySystemInterface.h"
UCLASS()
class Project_API RotateActor : public AActor, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
RotateActor();
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
//...
}
//.cpp
RotateActor()
{
ASC = CreateDefaultSubobject<AbilitySystemComponent>(TEXT("ASC"));
}
UAbilitySystemComponent* RotateActor::GetAbilitySystemComponent()
{
return ASC;
}
앞에서 말했듯이 GAS가 적용된 액터끼리 ASC를 통해 통신 및 상호작용을 한다.
따라서, ASC를 받아올 수 있는 GetAbilitySystemComponent()를 구현해야 한다.
GetAbilitySystemComponent()에서는 액터가 소유한 ASC를 const로 return 한다.
여기까지 했다면, GAS를 적용시킬 준비가 된 것이다.
이제 ASC에 부여할 Ability를 구현해 보자.
Ability는 UGameAbility를 상속받아 만들어야 한다.
UGameAbility에는 이미 잘 구현된 Ability에 관한 함수나 멤버 변수들이 존재한다.
헤더를 열어보면 다음과 같은 주석이 있다.
// ----------------------------------------------------------------------------------------------------------------
//
// The important functions:
//
// CanActivateAbility() - const function to see if ability is activatable. Callable by UI etc
//
// TryActivateAbility() - Attempts to activate the ability. Calls CanActivateAbility(). Input events can call this directly.
// - Also handles instancing-per-execution logic and replication/prediction calls.
//
// CallActivateAbility() - Protected, non virtual function. Does some boilerplate 'pre activate' stuff, then calls ActivateAbility()
//
// ActivateAbility() - What the abilities *does*. This is what child classes want to override.
//
// CommitAbility() - Commits reources/cooldowns etc. ActivateAbility() must call this!
//
// CancelAbility() - Interrupts the ability (from an outside source).
//
// EndAbility() - The ability has ended. This is intended to be called by the ability to end itself.
//
// ----------------------------------------------------------------------------------------------------------------
이러한 주요 함수들을 적절히 구현하면 된다.
우선 현재 예시에서는 ActivateAbility(), CancelAbility() 정도만 있어도 괜찮다.
함수이름을 보면 직관적으로 무엇을 하는 함수인지 알 수 있다.
- ActivateAbility: Ability 시작
- CancelAbility: Ability 중지
이를 구현한 것은 다음과 같다.
//Ability
//.h
...
UCLASS()
class Project_API UGA_Rotate : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_Rotate();
protected:
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;
};
//.cpp
void UGA_Rotate::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
AActor* AvatarActor = ActorInfo->AvatarActor.Get();
if (AvatarActor)
{
//액터 회전 시키기
}
}
void UGA_Rotate::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
{
Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
AActor* AvatarActor = ActorInfo->AvatarActor.Get();
if (AvatarActor)
{
//액터 회전 중지
}
}
이제 액터를 회전시키는 Ability도 구현하였으니 적용해야 한다.
Ability를 적용하기 위해서는 액터의 ASC에 Ability를 부여한 뒤 적절한 시점에 실행시키거나 중지시키면 된다.
예시는 시작하자마자 회전하기를 원하기 때문에 BeginPlay에서 Ability를 실행하는 Timer를 동작시켜 보자.
//Actor
//.cpp
void ARotateActor::PostInitializeComponents()
{
Super::PostInitializeComponents();
//...
//Component 초기화
ASC->InitAbilityActorInfo(this, this);
for (const auto& StartAbility : StartAbilities)
{
FGameplayAbilitySpec StartSpec(StartAbility);
ASC->GiveAbility(StartSpec);
}
}
void ARotateActor::BeginPlay()
{
Super::BeginPlay();
//Timer 설정
GetWorld()->GetTimerManager().SetTimer(ActionTimer, this, &ARotateActor::TimerAction, ActionPeriod, true, 0.f);
}
void ARotateActor::TimerAction()
{
FGameplayAbilitySpec* RotateSpec = ASC->FindAbilitySpecFromClass(UGA_Rotate::StaticClass());
if(!RotateSpec) return;
if (!RotateSpec->IsActive())
{
ASC->TryActivateAbilitiy(RotateSpec->Handle);
}
else
{
ASC->CancelAbilityHandle(RotateSpec->Handle);
}
}
PostInitializeComponents를 보면 StartAbility들을 ASC에 부여하는 것을 볼 수 있다.
이렇게 등록해 놓는다고 바로 동작하는 것은 아니다.
TryActivate를 명시적으로 실행해 주어야 해당 Ability를 실행할 수 있다.
예시에는 시작부터 Ability가 실행되기를 원하기 때문에 BeginPlay에서 Ability를 실행시킬 타이머를 설정하였다.
타이머가 발동되면 TimerAction이라는 함수가 실행된다.
TimerAction에서는 ASC로부터 Ability를 읽어와 해당 Ability가 실행 중인지 체크하여 알맞은 동작을 한다.
이때, 특정 Ability의 클래스로 FGameplayAbilitySpec을 받아온 부분을 볼 수 있다.
FGameplayAbilitySpec이란 Ability를 감싸고 있는 구조체로 추가적인 정보를 넣어 편리하게 Ability를 관리할 수 있게 해 준다.
FGameplayAbilitySpec의 핸들을 통해 Ability를 실행할 수 있다.
GameplayTag
위에서 구현한 내용을 더욱 간편하게 구현하는 방법이 있다.
하지만 이를 위해서는 에디터에서 GameplayTag를 추가해야 한다.
에디터에서 프로젝트 세팅을 켜 다음과 같은 화면에서 태그를 추가할 수 있다.
태그는 위의 사진과 같이 계층형 구조를 갖는다.
이러한 태그를 적절히 이용하면 복잡한 코드 없이 여러 상황에 대한 체크도 할 수 있다.
GameplayTag 적용
위에서 추가한 태그를 적용시키기 위해서 태그를 관리하는 하나의 헤더파일로 만들어 관리하는 것이 편리한다.
//MyGamePlayTag.h
#pragma once
#include "GameplayTagContainer.h"
#define ABTAG_ACTOR_ROTATE FGameplayTag::RequestGameplayTag(FName("Actor.Action.Rotate"))
#define ABTAG_ACTOR_ISROTATING FGameplayTag::RequestGameplayTag(FName("Actor.State.IsRotating"))
이렇게 추가한 태그를 적용하려면 코드가 약간 변경되어야 한다.
우선, Ability가 태그를 가질 수 있는데 이 태그를 통해 구별할 수 있고 이를 AbilityTags라고 한다.
또한, 실행되었을 때 태그가 추가되게 할 수 있는데, 이를 ActivationOwnedTags라고 한다.
ActivationOwnedTags를 통해 실행여부를 간단히 파악할 수 있다.
//Ability
UGA_Rotate::UGA_Rotate()
{
AbilityTags.AddTag(ABTAG_ACTOR_ROTATE);
ActivationOwnedTags.AddTag(ABTAG_ACTOR_ISROTATING);
}
또한, 이 Ability를 실행될 부분을 변경해야 한다.
void ARotateActor::TimerAction()
{
FGameplayTagContainer TargetTag(ABTAG_ACTOR_ROTATE);
if (!ASC->HasMatchingGameplayTag(ABTAG_ACTOR_ISROTATING))
{
ASC->TryActivateAbilitiesByTag(TargetTag);
}
else
{
ASC->CancelAbilities(&TargetTag);
}
}
위에서는 특정 Ability의 Class를 통해 Spec를 찾아 실행 여부를 판단했는데 Ability가 실행되었을 때 적용되는 ActivationOwnedTag를 통해 실행 여부를 판단할 수 있다. (HasMatchingGameplayTag)
또한, TryActivateAbility나 CancelAbilityHandle 대신 태그를 통해 실행시키거나 중지시킬 수 있다.