캐릭터 입력
GAS를 이용하면 원하는 시점에 부여한 Ability를 실행시킬 수 있다.
이를 캐릭터 입력에 적용하는 법을 알아보자.
UE5는 기본적으로 EnhancedInput을 이용하여 캐릭터에서 입력을 처리한다.
샘플 프로젝트를 열어보면 캐릭터에 점프까지 구현이 되어 있을 것이다.
이번 예시에서는 GAS를 이용하여 점프나 공격 같은 특수한 행동을 실행하도록 설계할 것이다.
물론 캐릭터에서도 구현할 수 있지만 그렇게 하면 캐릭터 클래스의 코드가 방대해져 관리가 힘들 수 있다.
또한, 특수한 기능이나 효과를 넣고 싶을 때 불필요한 의존성이 발생할 수도 있다.
따라서, GAS를 통해 Ability로 분리하여 관리하는 것이 효과적일 것이라는 생각이 든다.
캐릭터 설계
GAS를 적용하려면 AbilitySystemComponent를 가지고 있어야 한다.
캐릭터의 경우에는 실제 게임에서 보여지는 액터는 맞지만 실제로 유저를 나타내는 액터는 아니다.
캐릭터는 상황에 따라 언제든지 변경되거나 삭제되고 또 새로 생성되기도 한다.
이를 PlayerContoller에서 빙의하여 조종한다.
그렇기 때문에 캐릭터에서 ASC를 생성하여 관리하는 것보다 플레이어 스테이트에서 생성한 뒤 캐릭터에서 이를 받아와 사용하는 것이 좋다.
추가적으로, 멀티 플레이어 게임을 생각해보면 Client에서 보는 캐릭터는 복사본에 지나지 않는다.
따라서, 실제 데이터를 관리하기에는 무리가 있다.
하지만, 플레이어 스테이트는 컨트롤러에 붙어 있기 때문에 적합하다고 생각된다.
구현
앞에서 말했듯이 캐릭터는 플레이어 스테이트의 ASC를 포인터로 받아온다.
//PlayerState
//생성자에서 ASC 생성 & GetAbilitySystemComponent에서 반환
UCLASS()
class Project_API AMyPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AMyPlayerState();
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
}
//Character
void ACharacterPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AMyPlayerState* PS = Cast<AMyPlayerState>(GetPlayerState());
if (PS)
{
ASC = GASPS->GetAbilitySystemComponent();
ASC->InitAbilityActorInfo(PS, this);
...
}
}
플레이어 스테이트에서 ASC를 받아오기 적합한 시점은 PossessedBy이다.
PossessedBy는 PlayerController에서 Character에 빙의했을 때 실행되는 함수이기 때문이다.
ASC->InitAbilityActorInfo()를 보면 이전과 다른 것을 볼 수 있다.
이전에 자동으로 회전하는 액터를 만들 때는 두 매개변수 모두 this를 주었다.
하지만 여기서는 첫 번째 인자를 PS로 전달했다.
InitAbilityActorInfo의 매개변수를 살펴보면 알 수 있는데, 첫 번째 매개변수는 OwnerActor이고 두 번째 매개변수는 AvatarActor이다.
OwnerActor는 ASC를 실제로 소유한 액터이고 AvatarActor는 눈에 보이는 액터라고 생각하면 된다.
기본적인 세팅은 되었으니 실제로 입력을 처리하는 부분을 살펴보자.
캐릭터 코드를 보면 SetupPlayerInputComponent라는 함수를 볼 수 있다.
이는 EnhancedInputComponent에 Action을 바인딩하는 역할을 하는 함수이다.
하지만 GAS 인풋은 따로 함수로 분리하여 관리하고 이를 호출하는 것이 편리할 것이다.
void ACharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
SetupGASInputComponent();
}
void ACharacterPlayer::SetupGASInputComponent()
{
if (IsValid(ASC) && IsValid(InputComponent))
{
UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacterPlayer::GASInputPressed, 0);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacterPlayer::GASInputReleased, 0);
EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &ACharacterPlayer::GASInputPressed, 1);
}
}
BindAction에 추가적인 매개변수를 넣어 어떤 입력인지 구분할 수 있다.
이를 이용하여 동일한 코드로 다른 Ability를 실행시킬 수 있다.
이때, 뒤에 들어오는 매개변수를 inputId로 설정할 것이기 때문에 주의해서 설정해야 한다.
Bind 된 Action들이 실제로 처리되는 함수들은 다음과 같다.
void ACharacterPlayer::GASInputPressed(int32 InputId)
{
FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
if (Spec)
{
Spec->InputPressed = true;
if (Spec->IsActive())
{
ASC->AbilitySpecInputPressed(*Spec);
}
else
{
ASC->TryActivateAbility(Spec->Handle);
}
}
}
void ACharacterPlayer::GASInputReleased(int32 InputId)
{
FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
if (Spec)
{
Spec->InputPressed = false;
if (Spec->IsActive())
{
ASC->AbilitySpecInputReleased(*Spec);
}
}
}
InputId를 점프는 0, 공격은 1로 설정하였다.
그 매개변수를 통해 ASC에 부여된 Ability를 받아올 수 있다.
그리고 ASC에 해당 Ability의 실행을 요청하면 된다.
AbiltiySpecInpuPressed는 이미 실행 중인 Ability에 입력이 들어왔다고 알려주는 함수이다.
TryActivateAbiltiy는 Ability의 실행을 요청하는 함수이다.
그렇다면 Abiltiy를 InputId와 함께 부여하는 방법을 알아야 한다.
해당 부분은 PossessedBy에서 ASC를 초기화하며 같이 진행해 주면 된다.
void ACharacterPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
...
for (const auto& StartAbility : StartInputAbilities)
{
FGameplayAbilitySpec StartSpec(StartAbility.Value);
StartSpec.InputID = StartAbility.Key;
ASC->GiveAbility(StartSpec);
}
...
}
Spec에 InputId라는 멤버 변수가 있고 이를 아까 설정하기로 했던 수에 맞춰 설정하면 된다.
이제 Jump Ability를 구현하기만 하면 된다.
이전 포스트에서 했듯이 GA의 여러 함수들을 override하면 된다.
...
UABGA_Jump::UABGA_Jump()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UABAT_JumpAndWaitForLanding* JumpAndWaitForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
JumpAndWaitForLandingTask->OnComplete.AddDynamic(this, &UABGA_Jump::OnLandedCallback);
JumpAndWaitForLandingTask->ReadyForActivation();
}
void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
Character->StopJumping();
}
bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags);
if (!bResult) return false;
const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
return (Character && Character->CanJump());
}
void UABGA_Jump::OnLandedCallback()
{
bool bReplicateEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}
생성자에 나오는 InstancingPolicy는 해당 Ability가 인스턴싱되는 옵션을 고르는 것이다.
InstancedPerActor 같은 경우는 Ability를 소유한 액터에 하나씩 생성된다.
ActiveAbility를 보면 Ability Task를 생성하여 실행을 요청하는 부분을 볼 수 있다.
Ability Task가 무엇인지 알아보자.
Ability Task
Ability Task란 Ability가 여러 프레임에 거쳐 실행될 수 있게 해주는 클래스이다.
예를 들어, 점프는 같이 뛰어 올랐다가 땅에 떨어지기까지로 정의할 수 있다.
하지만, 일반적인 Ability는 땅에 떨어지는 시점을 체크할 수 없다.
이를 도와주는 것이 Ability Task라고 생각하면 된다.
Ability Task를 사용하는 방법은 다음과 같다.
- Ability에서 Ability Task를 생성한다.
- Ability Task의 완료 Delegate에 Callback을 설정한다.
- Abilit Task의 ReadyForActivation()를 호출한다.
Ability Task는 ReadyForActivation()에 의해 실행이 요청되어 Activate()가 실행된다.
Activate() 함수에서 실질적인 동작이 이루어진다.
이후에 수행이 완료되면 OnDestroy()함수가 실행되어 소멸된다.
캐릭터 점프에 적용해 보자면 Activate에서 캐릭터를 점프시키고 캐릭터가 땅에 떨어졌을 때, 완료 Delegate를 방송하면 된다.
void UABAT_JumpAndWaitForLanding::Activate()
{
Super::Activate();
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.AddDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Character->Jump();
SetWaitingOnAvatar();
}
void UABAT_JumpAndWaitForLanding::OnDestroy(bool AbilityEnded)
{
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.RemoveDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Super::OnDestroy(AbilityEnded);
}
void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnComplete.Broadcast();
}
}
Ability에서 AT를 만들 때, OnComplete에 Binding을 해놨기 때문에 Broadcast를 받는 순간 Ability를 종료시킨다.
공격 구현
공격또한 점프와 동일한 구조로 실행된다.
다른 점은 InputId 하나이다.
그리고 이미 언리얼에서 유용한 AT를 지원해 준다.
공격의 경우에는 UAbilityTask_PlayMontageAndWait라는 AT를 생성하여 Montage를 실행하면 된다.
//Attack Ability
void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), /*Montage*/, 1.f);
PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompleteCallback);
PlayAttackTask->OnCancelled.AddDynamic(this, &UABGA_Attack::OnCancelCallback);
PlayAttackTask->ReadyForActivation();
}