콘텐츠로 이동

UE5 GAS Gameplay Ability 구현

UGameplayAbility는 GAS(Gameplay Ability System)에서 캐릭터가 수행할 수 있는 하나의 능력(스킬, 공격, 구르기 등) 을 표현하는 클래스입니다. 활성화 조건, 비용, 쿨다운, 실행 로직을 하나의 객체로 캡슐화합니다.


Build.cs에 GAS 모듈을 추가합니다.

MyGame.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine",
"GameplayAbilities",
"GameplayTasks",
"GameplayTags"
});

MyGameplayAbility.h
#pragma once
#include "Abilities/GameplayAbility.h"
#include "MyGameplayAbility.generated.h"
UCLASS()
class MYGAME_API UMyGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UMyGameplayAbility();
// 활성화: 어빌리티 실행 진입점
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;
protected:
// 어빌리티 종료 (반드시 호출해야 함)
void EndAbilityCleanup(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bWasCancelled);
};
MyGameplayAbility.cpp
#include "MyGameplayAbility.h"
#include "AbilitySystemComponent.h"
UMyGameplayAbility::UMyGameplayAbility()
{
// 인스턴스 정책: 실행마다 새 인스턴스 생성
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerExecution;
// 네트워크 실행 정책
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}
void UMyGameplayAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
// 비용·쿨다운 확인 후 커밋 (실패 시 자동으로 어빌리티 종료)
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 실제 능력 로직 실행
// ...
// 즉시 종료 (단순 능력)
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
}
void UMyGameplayAbility::CancelAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateCancelAbility)
{
Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
// 취소 시 정리 로직
}

MeleeAttackAbility.h
UCLASS()
class UMeleeAttackAbility : public UMyGameplayAbility
{
GENERATED_BODY()
public:
UMeleeAttackAbility();
void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Attack")
float _attackRange = 150.f;
UPROPERTY(EditDefaultsOnly, Category = "Attack")
float _attackDamage = 30.f;
// 히트 이벤트 태그
UPROPERTY(EditDefaultsOnly, Category = "Attack")
FGameplayTag _hitEventTag;
// 애니메이션 몽타주
UPROPERTY(EditDefaultsOnly, Category = "Animation")
UAnimMontage* _attackMontage;
private:
// 몽타주 이벤트 핸들
FDelegateHandle _montageEventHandle;
};
MeleeAttackAbility.cpp
#include "MeleeAttackAbility.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
#include "Abilities/Tasks/AbilityTask_WaitGameplayEvent.h"
UMeleeAttackAbility::UMeleeAttackAbility()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerExecution;
}
void UMeleeAttackAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 몽타주 재생 태스크
auto* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this, NAME_None, _attackMontage, 1.0f, NAME_None, true);
MontageTask->OnCompleted.AddDynamic(this, &UMeleeAttackAbility::OnMontageCompleted);
MontageTask->OnCancelled.AddDynamic(this, &UMeleeAttackAbility::OnMontageCancelled);
MontageTask->OnInterrupted.AddDynamic(this, &UMeleeAttackAbility::OnMontageCancelled);
MontageTask->ReadyForActivation();
// 히트 이벤트 대기 태스크 (애니메이션 Notify에서 발생)
auto* HitEventTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this, _hitEventTag);
HitEventTask->EventReceived.AddDynamic(this, &UMeleeAttackAbility::OnHitEvent);
HitEventTask->ReadyForActivation();
}
void UMeleeAttackAbility::OnHitEvent(FGameplayEventData Payload)
{
// 공격 범위 내 적 탐지
AActor* Owner = GetOwningActorFromActorInfo();
if (!Owner) return;
TArray<FHitResult> Hits;
FCollisionShape Sphere = FCollisionShape::MakeSphere(_attackRange);
Owner->GetWorld()->SweepMultiByChannel(
Hits,
Owner->GetActorLocation(),
Owner->GetActorLocation() + Owner->GetActorForwardVector() * _attackRange,
FQuat::Identity,
ECC_Pawn,
Sphere
);
for (const auto& Hit : Hits)
{
AActor* HitActor = Hit.GetActor();
if (HitActor && HitActor != Owner)
{
// GameplayEffect로 데미지 적용
FGameplayEffectSpecHandle DamageSpec = MakeOutgoingGameplayEffectSpec(
DamageEffectClass, GetAbilityLevel());
if (DamageSpec.IsValid())
{
DamageSpec.Data->SetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag("Data.Damage"),
_attackDamage
);
ApplyGameplayEffectSpecToTarget(Handle, CurrentActorInfo,
CurrentActivationInfo, DamageSpec,
UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(HitActor));
}
}
}
}
void UMeleeAttackAbility::OnMontageCompleted()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo,
CurrentActivationInfo, true, false);
}
void UMeleeAttackAbility::OnMontageCancelled()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo,
CurrentActivationInfo, true, true);
}

쿨다운은 GameplayEffect로 구현합니다.

// 어빌리티 생성자에서 쿨다운 GameplayEffect 지정
UMeleeAttackAbility::UMeleeAttackAbility()
{
// CooldownGameplayEffectClass: 에디터에서 GE_Cooldown_MeleeAttack 설정
}

GE_Cooldown_MeleeAttack GameplayEffect 설정:

  • Duration Policy: Has Duration
  • Duration Magnitude: 1.5 (초)
  • Granted Tags: Ability.Cooldown.MeleeAttack
// 쿨다운 남은 시간 조회
float GetCooldownRemaining()
{
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
if (!ASC) return 0.f;
FGameplayTagContainer CooldownTags;
GetCooldownGameplayEffect()->InheritableOwnedTagsContainer.GetAllGameplayTags(CooldownTags);
FGameplayEffectQuery Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
TArray<float> Durations = ASC->GetActiveEffectsTimeRemaining(Query);
return Durations.Num() > 0 ? Durations.Max() : 0.f;
}

// 캐릭터에 어빌리티 부여
void AMyCharacter::GrantAbility(TSubclassOf<UGameplayAbility> AbilityClass, int32 Level)
{
if (!AbilitySystemComponent || !HasAuthority()) return;
FGameplayAbilitySpec Spec(AbilityClass, Level, INDEX_NONE, this);
AbilitySystemComponent->GiveAbility(Spec);
}
// 입력 액션으로 어빌리티 활성화
void AMyCharacter::OnAttackInput()
{
if (!AbilitySystemComponent) return;
FGameplayTagContainer AttackTags;
AttackTags.AddTag(FGameplayTag::RequestGameplayTag("Ability.Attack.Melee"));
AbilitySystemComponent->TryActivateAbilitiesByTag(AttackTags);
}

개념역할
UGameplayAbility능력 로직 정의
CommitAbility비용·쿨다운 소모
EndAbility어빌리티 종료 (반드시 호출)
AbilityTask비동기 작업 (몽타주, 이벤트 대기)
GameplayEffect쿨다운·비용·데미지 적용
GiveAbilityASC에 어빌리티 등록

GAS Ability는 CommitAbility → 로직 실행 → EndAbility 흐름이 핵심입니다. EndAbility를 누락하면 어빌리티가 종료되지 않아 심각한 버그로 이어집니다.