언리얼 엔진

[언리얼 엔진] GAS GameplayAbility와 GameplayEvent 정리

dhlee-dev 2026. 6. 18. 21:41

이전 글에서는 GAS의 기본 구성 요소를 간단히 정리했다.

GAS는 AbilitySystemComponent, GameplayAbility, GameplayEffect,
AttributeSet, GameplayTag, GameplayCue 같은 요소들이 서로 연결되어 동작하는 시스템이었다.

이번 글에서는 그중에서도 GameplayAbilityGameplayEvent 흐름을 정리해보려고 한다.

강의 실습을 진행하면서 처음에는 GameplayAbility를 단순히 스킬이라고 생각했다.
하지만 실습을 해보니 공격, 회피 같은 행동뿐만 아니라
피격 반응, 사망 처리, 특정 이벤트를 기다리는 리스너 역할도 Ability로 분리할 수 있었다.

또한 공격자가 피격 대상의 함수를 직접 호출하는 대신,
GameplayEvent를 보내고 대상의 Ability가 그 이벤트에 반응하도록 만들 수 있었다.

이번 글에서는 GameplayAbility가 어떤 단위로 사용되는지,
그리고 GameplayEvent를 이용해 Ability끼리 어떻게 느슨하게 연결할 수 있는지 정리해보려고 한다.


GameplayAbility는 행동 단위

GameplayAbility는 캐릭터가 수행할 수 있는 행동 단위이다.
예를 들어 다음과 같은 기능들을 Ability로 만들 수 있다.

  • 기본 공격
  • 회피
  • 스킬 사용
  • 피격 반응
  • 사망 처리
  • 타겟 탐색
  • 특정 이벤트 감시

처음에는 Ability라고 하면 공격 스킬이나 액티브 스킬만 떠올리기 쉽다.
하지만 GAS에서 Ability는 단순히 스킬만 의미하지 않는다.

캐릭터가 수행하는 하나의 게임플레이 동작을 독립적인 단위로 분리한 것에 가깝다.
예를 들어 피격 반응도 하나의 Ability로 만들 수 있다.

공격자가 직접 피격 몽타주를 재생시키는 것이 아니라,
피격 대상이 GA_HitReact 같은 Ability를 가지고 있다가
피격 이벤트를 받으면 자기 방식대로 반응하게 만들 수 있다.

이렇게 하면 행동 단위를 Ability로 나누고,
각 Ability가 자신이 담당하는 흐름을 처리하도록 구성할 수 있다.


Ability는 ASC에 부여되어야 실행할 수 있다

GameplayAbility는 클래스만 만든다고 바로 실행되는 것이 아니다.
해당 Ability를 사용하려면 먼저 AbilitySystemComponent, 즉 ASC에 Ability를 부여해야 한다.

보통 서버에서 GiveAbility를 통해 Ability를 ASC에 등록한다.

FGameplayAbilitySpec AbilitySpec(AbilityClass, AbilityLevel, InputID);
AbilitySystemComponent->GiveAbility(AbilitySpec);

이렇게 등록된 Ability는 ASC의 ActivatableAbilities 목록에 들어가고,
이후 입력이나 태그를 기준으로 실행할 수 있다.

흐름을 간단히 보면 다음과 같다.

Ability 클래스 준비
-> FGameplayAbilitySpec 생성
-> ASC에 GiveAbility
-> ASC의 AbilitySpec 목록에 등록
-> TryActivateAbility 또는 TryActivateAbilitiesByTag로 실행

즉, GameplayAbility 클래스는 Ability의 원본이고,
ASC에 등록된 실제 Ability 정보는 FGameplayAbilitySpec으로 관리된다고 볼 수 있다.


FGameplayAbilitySpec이란?

FGameplayAbilitySpec은 ASC에 등록된 Ability 정보를 담는 구조체이다.

UGameplayAbility 클래스가 스킬의 원본이라면,
FGameplayAbilitySpec은 해당 ASC에 실제로 부여된 Ability 정보라고 볼 수 있다.

같은 공격 Ability 클래스라도 캐릭터마다 Ability 레벨이 다를 수 있다.
또 어떤 입력과 연결되는지, 현재 등록 상태가 어떤지,
Ability를 식별하기 위한 Handle이 무엇인지도 필요하다.

이런 정보를 담기 위해 FGameplayAbilitySpec이 사용된다.

간단히 정리하면 다음과 같다.

구분 의미
UGameplayAbility Ability 클래스 원본
FGameplayAbilitySpec ASC에 등록된 실제 Ability 정보
FGameplayAbilitySpecHandle 등록된 Ability를 식별하기 위한 핸들

예를 들어 Ability 레벨을 변경해야 한다면 Ability 클래스 자체를 바꾸는 것이 아니라,
ASC에 등록된 AbilitySpec의 Level 값을 변경하는 방식으로 처리할 수 있다.


GameplayAbility 실행 흐름

Ability를 실행할 때는 보통 ASC를 통해 실행한다.
대표적으로 TryActivateAbility 또는 TryActivateAbilitiesByTag 같은 함수를 사용할 수 있다.

특정 태그를 가진 Ability를 실행한다면 다음과 같은 흐름으로 생각할 수 있다.

입력 또는 GameplayTag 처리
-> ASC가 AbilitySpec 목록에서 Ability 검색
-> TryActivateAbility 호출
-> CanActivateAbility 확인
-> ActivateAbility 실행
-> Ability 작업 수행
-> EndAbility 호출

여기서 중요한 점은 Ability가 끝났다면 EndAbility를 호출해야 한다는 것이다.

Ability가 정상적으로 종료되지 않으면 GAS는 해당 Ability가 아직 실행 중이라고 판단할 수 있다.
그러면 같은 Ability가 다시 실행되지 않거나 다른 Ability가 막히는 문제가 생길 수 있다.
따라서 몽타주 재생이 끝났거나 필요한 이벤트 처리가 끝났다면 명확하게 EndAbility를 호출해야 한다.


Ability Instancing Policy

GameplayAbility에는 Instancing Policy가 있다.
이 설정은 Ability가 실행될 때 인스턴스를 어떻게 만들지 결정한다.

대표적으로 다음 세 가지가 있다.

Instancing Policy 설명
Non Instanced 별도 인스턴스를 만들지 않음
Instanced Per Actor Actor마다 Ability 인스턴스 하나를 만들어 재사용
Instanced Per Execution Ability 실행마다 새 인스턴스를 생성

Non Instanced

Non Instanced는 Ability 실행마다 별도 인스턴스를 만들지 않는다.
가볍게 동작할 수 있지만 Ability 내부에 실행 중 상태를 멤버 변수로 저장하면 위험하다.

인스턴스가 따로 없기 때문에 상태를 저장해야 하는 Ability에는 적합하지 않다.
그래서 단순하고 상태를 가지지 않는 Ability에 어울린다.

Instanced Per Actor

Instanced Per Actor는 ASC를 가진 Actor마다 Ability 인스턴스 하나를 만든다.

같은 Ability를 여러 번 실행해도 같은 인스턴스를 재사용한다.
따라서 Ability 내부에 멤버 변수로 상태를 저장할 수 있다.

다만 같은 인스턴스를 반복해서 사용하므로,
Ability 실행이 끝난 뒤 필요한 상태를 정리해주는 것이 중요하다.

예를 들어 타겟을 저장하거나, Ability 실행 중 사용한 Task를 변수로 들고 있다면
종료 시점에 정리하지 않으면 다음 실행에 영향을 줄 수 있다.

Instanced Per Execution

Instanced Per Execution은 Ability가 실행될 때마다 새 인스턴스를 만든다.

실행마다 독립적인 상태를 가질 수 있다는 장점이 있다.
하지만 실행할 때마다 인스턴스를 만들기 때문에 비용은 더 클 수 있다.

그래서 자주 실행되는 기본 공격보다는,
상대적으로 실행 빈도가 낮고 실행별 상태를 독립적으로 관리해야 하는 Ability에 어울릴 수 있다.


GiveAbility와 GiveAbilityAndActivateOnce

ASC에 Ability를 부여할 때는 보통 GiveAbility를 사용한다.

GiveAbility
-> Ability를 ASC에 등록
-> 필요할 때 나중에 활성화

즉, GiveAbility는 Ability를 등록만 하고 자동으로 실행하지 않는다.
반면 GiveAbilityAndActivateOnce는 Ability를 부여하고 한 번 실행한 뒤 제거되는 흐름에 적합하다.

GiveAbilityAndActivateOnce
-> Ability 부여
-> 한 번 실행 시도
-> 실행 후 제거

따라서 일회성 Ability라면 GiveAbilityAndActivateOnce를 사용할 수 있다.
하지만 계속 ASC에 남아 있으면서 이벤트를 기다려야 하는 Ability에는 어울리지 않는다.

이런 경우에는 Ability를 계속 보유한 상태로 자동 활성화하는 구조가 더 적합하다.


ActivateOnGiven 방식

실습에서는 피격 반응을 GA_HitReact라는 별도의 GameplayAbility로 만들었다.

이 Ability는 피격될 때마다 새로 부여해서 실행하는 방식이 아니라,
캐릭터가 시작할 때 미리 부여해두고 활성화된 상태에서
GameplayEvent를 기다리는 반응형 Ability로 구현했다.

그래서 GA_HitReact는 부여되자마자 자동으로 활성화될 필요가 있었고,
이를 처리하기 위해 ActivateOnGiven이라는 태그를 사용했다.

이 태그는 GAS에서 기본으로 제공되는 자동 활성화 옵션이라기보다는,
커스텀 ASC에서 해당 태그를 가진 Ability를 부여 시점에 자동 활성화하도록 만든 실습용 규칙에 가깝다.

흐름은 다음과 같다.

GA_HitReact 부여
-> AbilitySpec에 ActivateOnGiven 태그 존재
-> ASC가 자동으로 TryActivateAbility 호출
-> GA_HitReact 활성화
-> Wait Gameplay Event로 피격 이벤트 대기

이 구조를 사용하면 GA_HitReact는 시작 시 한 번 활성화된 뒤,
Wait Gameplay Event로 특정 이벤트를 계속 기다릴 수 있다.

실습에서는 커스텀 ASC를 만들어 OnGiveAbility 시점에 ActivateOnGiven 태그를 확인하고
해당 Ability를 자동으로 활성화하도록 처리했다.

또 클라이언트에서는 AbilitySpec 목록이 복제되는 타이밍이 다를 수 있으므로,
복제 이후에도 필요한 처리를 할 수 있게 구성했다.

정리하면 다음과 같다.

일회성 실행
-> GiveAbilityAndActivateOnce

계속 보유하면서 이벤트 대기
-> GiveAbility + ActivateOnGiven 태그 방식

GameplayEvent란?

GameplayEvent는 Ability에 특정 이벤트가 발생했다는 사실을 전달하기 위한 방식이다.

공격자가 적을 맞췄을 때 공격자가 직접 적의 PlayHitReact() 함수를 호출할 수도 있다.
하지만 이렇게 하면 공격 Ability가 Enemy 클래스와 함수 이름을 직접 알아야 한다.

즉, 공격자가 피격 대상의 구체적인 클래스에 강하게 의존하게 된다.

Enemy->PlayHitReact();

GAS에서는 대신 GameplayEvent를 사용할 수 있다.

공격자
-> 피격 대상에게 GameplayEvent 전송
-> 피격 대상의 ASC가 이벤트 수신
-> 해당 이벤트를 기다리던 Ability가 반응

이렇게 하면 공격자는 “HitReact 이벤트가 발생했다”는 사실만 전달하면 된다.
피격 대상은 자기 ASC와 Ability를 통해 그 이벤트를 어떻게 처리할지 결정한다.

즉, GameplayEvent는 Ability끼리 직접 함수 호출로 연결되는 대신,
GameplayTag 기반 이벤트로 느슨하게 연결되도록 도와준다.


FGameplayEventData

GameplayEvent를 보낼 때는 추가 정보를 함께 전달할 수 있다.
이때 사용하는 구조체가 FGameplayEventData이다.

대표적으로 다음과 같은 정보를 담을 수 있다.

필드 의미
EventTag 어떤 이벤트인지 나타내는 태그
Instigator 이벤트를 발생시킨 주체
Target 이벤트 대상
EventMagnitude 수치 정보가 필요할 때 사용
OptionalObject 추가 객체 정보가 필요할 때 사용
ContextHandle HitResult 같은 추가 컨텍스트 전달에 활용

HitReact에서는 Instigator를 이용해 공격자가 어느 방향에 있는지 계산했다.

피격 대상 위치
-> Instigator 위치 확인
-> 공격 방향 계산
-> Front / Back / Left / Right 몽타주 섹션 결정

즉, GameplayEvent는 단순히 이벤트 태그만 보내는 것이 아니라,
그 이벤트를 처리하는 데 필요한 정보도 함께 전달할 수 있다.


HitReact를 GameplayEvent로 처리하기

실습에서는 피격 반응을 GameplayEvent 기반으로 처리했다.
중요한 점은 공격 Ability가 직접 피격 몽타주를 재생하지 않는다는 것이다.

공격 Ability는 히트박스나 Trace로 맞은 대상을 찾고,
그 대상에게 HitReact GameplayEvent를 보낸다.

피격 대상은 이미 활성화되어 있는 GA_HitReact에서
Wait Gameplay Event로 해당 이벤트를 기다리고 있다가 반응한다.

흐름은 다음과 같다.

1. 피격 대상에게 GA_HitReact 부여
2. ActivateOnGiven 태그로 GA_HitReact 자동 활성화
3. GA_HitReact가 Wait Gameplay Event로 HitReact 이벤트 대기
4. 공격자가 공격 Ability 실행
5. 히트박스 또는 Trace로 맞은 대상 탐색
6. 맞은 대상에게 HitReact GameplayEvent 전송
7. GA_HitReact가 이벤트 수신
8. Instigator 기준으로 피격 방향 계산
9. 방향에 맞는 HitReact 몽타주 재생

이 구조를 사용하면 공격자는 피격 대상이 어떤 클래스인지 몰라도 된다.
공격자는 이벤트만 보내고 피격 대상은 자신의 Ability에서 자기 방식대로 반응한다.

또한 플레이어와 적이 같은 HitReact 이벤트를 받더라도,
각자 다른 몽타주나 이펙트를 재생하도록 만들 수 있다.


Death Ability 흐름

사망 처리도 Ability로 분리할 수 있다.

실습에서는 Health가 0 이하가 되었을 때 Death 태그를 가진 Ability를 실행하는 구조를 사용했다.
흐름은 다음과 같다.

1. Health Attribute 변경 감시
2. Health가 0 이하인지 확인
3. TryActivateAbilitiesByTag로 Death Ability 실행
4. GA_Death 실행
5. Death Montage 재생
6. GameplayEffect로 Status.Death 태그 부여
7. 필요한 정리 후 EndAbility 호출

이 구조를 사용하면 사망 처리를 캐릭터 클래스에 직접 넣는 대신,
GA_Death라는 Ability로 분리할 수 있다.

Status.Death 같은 GameplayTag를 부여하면 다른 Ability에서 현재 사망 상태인지 확인할 수 있다.

HitReact Ability에서는 대상이 이미 Status.Death 태그를 가지고 있다면
피격 반응을 실행하지 않도록 처리할 수도 있다.

HitReact 이벤트 수신
-> ASC에 Status.Death 태그가 있는지 확인
-> 죽은 상태라면 피격 반응 무시
-> 살아 있다면 HitReact 실행

여기서 주의할 점은 Activation Blocked Tags만으로는
이미 활성화되어 이벤트를 기다리는 리스너형 Ability의 내부 반응까지 막기 어려울 수 있다는 점이다.

예를 들어 GA_HitReact가 이미 활성화되어 Wait Gameplay Event로 대기 중이라면,
이후 Status.Death 태그가 추가되어도 이미 대기 중인 이벤트 처리 흐름은 별도로 막아줘야 할 수 있다.

그래서 이벤트를 받은 시점에 직접 Status.Death 태그를 확인하고,
죽은 상태라면 피격 반응을 실행하지 않도록 처리하는 방식이 필요했다.


KillScored GameplayEvent 흐름

실습에서는 사망 처리 이후 공격자에게 킬 이벤트를 보내는 흐름도 다뤘다.

데미지 GameplayEffect로 Health가 감소하고,
피격자의 AttributeSet에서 Health가 0 이하가 된 것을 확인하면
공격자에게 KillScored GameplayEvent를 보낼 수 있다.

흐름은 다음과 같다.

1. 공격자가 대상에게 데미지 GameplayEffect 적용
2. 대상의 Health Attribute 감소
3. Health가 0 이하인지 확인
4. EffectContext에서 공격자 Instigator 확인
5. 공격자에게 KillScored GameplayEvent 전송
6. 공격자의 Ability가 Wait Gameplay Event로 이벤트 수신
7. 킬 메시지 출력 또는 보상 처리

이 구조를 사용하면 피격 대상이 죽었을 때 공격자 쪽의 Ability가 킬 보상이나 메시지 처리를 담당할 수 있다.

즉, 사망한 대상이 공격자 클래스를 직접 알 필요 없이,
GameplayEvent를 통해 “킬이 발생했다”는 사실을 전달할 수 있다.


정리

이번 글에서는 GameplayAbilityGameplayEvent 흐름을 정리했다.

GameplayAbility는 단순히 공격 스킬만 의미하는 것이 아니라,
캐릭터가 수행하는 행동 단위를 분리하기 위한 클래스라고 볼 수 있다.

Ability는 ASC에 FGameplayAbilitySpec 형태로 등록되고 ASC를 통해 실행된다.

또 Instancing Policy에 따라 Ability 인스턴스를 만들지,
Actor마다 하나를 만들지, 실행마다 새로 만들지 결정할 수 있다.

GameplayEvent는 Ability끼리 직접 함수를 호출하지 않고,
GameplayTag와 Payload를 통해 이벤트를 전달하는 방식이다.

이를 이용하면 공격자는 피격 대상의 구체적인 클래스를 몰라도
HitReact, Death, KillScored 같은 이벤트를 전달할 수 있다.

간단히 정리하면 다음과 같다.

개념 역할
GameplayAbility 캐릭터의 행동 단위
FGameplayAbilitySpec ASC에 등록된 실제 Ability 정보
Instancing Policy Ability 인스턴스 생성 방식
ActivateOnGiven 부여 시점에 자동 활성화하기 위해 실습에서 사용한 커스텀 태그
GameplayEvent Ability에 이벤트를 전달하는 방식
FGameplayEventData 이벤트와 함께 전달할 Payload

마무리

이번에 GameplayAbility와 GameplayEvent 흐름을 정리하면서
GAS에서는 행동을 Ability 단위로 나누고,
Ability끼리는 GameplayEvent를 통해 느슨하게 연결할 수 있다는 점이 인상적이었다.

그리고 각 객체가 서로의 구체적인 클래스를 몰라도 되고,
같은 이벤트를 대상마다 다른 방식으로 처리할 수 있다는 점이 GAS의 강력한 점이라는 것을 알 수 있었다.

아직은 강의 실습을 통해 이해한 단계라 실제 프로젝트에 적용하려면 더 고민이 필요하지만,
HitReact나 Death 같은 흐름을 Ability와 GameplayEvent로 분리하는 방식은
앞으로 전투 시스템을 설계할 때도 참고할 수 있을 것 같다.

다음 글에서는 AttributeSet과 GameplayEffect를 중심으로,
Attribute 값이 어떻게 변경되고 UI 갱신까지 어떻게 연결되는지 정리해보려고 한다.