Youngki Jeong | Game Dev Lab
HomeProjectsEvidenceJourneyAbout
SearchGitHub글쓰기

Youngki Jeong | Game Dev Lab

Unreal C++ 게임개발 포트폴리오 & 개발 기록

Projects·Evidence·Journey·About·GitHub Portfolio·GitHub

빠른 검색

프로젝트, Evidence, 디버깅 노트, 학습 기록을 검색합니다.

← 목록으로
← 목록으로
RIT-B06

Ability Activation 문제를 First Bad Boundary로 좁히는 방법

2026년 9월 7일
·
JEONGYOUNGKI
Description

Ability 실행 실패를 Input, Grant, AbilitySpec, Activation State의 경계로 나누어 First Bad Boundary를 찾는 Debug Playbook입니다.

Project

RiteSeekers

ArticleType

Debugging

SourceBoundary

Diagnostic Guide

EvidenceLevel

Verification Guide

Priority

B

Tags

Debugging

ProofType

Source

Audience

Technical Interviewer

CareerTrack

Gameplay

목차

RIT-B06 | RiteSeekers — Ability Activation이 끊긴 지점을 Runtime에서 찾는 과정

공격 키를 눌렀는데 Ability가 실행되지 않는 상황은 겉으로 보면 단순했습니다.

예전에는 이런 일이 생기면 해당 GameplayAbility부터 열어봤습니다. Montage가 제대로 실행되는지 보고, 조건문을 따라가고, 심하면 Damage 처리까지 내려가기도 했습니다.

그런데 디버거를 걸어보면 Ability 안쪽 코드는 아직 한 번도 실행되지 않은 경우가 있었습니다.

ActivateAbility까지 들어오지 않았다면 그 아래 Montage나 Trace를 보고 있어도 원인과는 거리가 먼 셈이었습니다.

그래서 이후부터는 Ability 내부보다 먼저 입력이 어디까지 들어왔는지를 확인하게 됐습니다.

Text
InputAction
→ InputTag
→ ASC
→ AbilitySpec
→ Activation 조건
→ GameplayAbility

이 흐름을 기준으로 보면 Ability가 실행되지 않는다는 하나의 증상도 조금씩 나눠서 볼 수 있었습니다.


Input에서 ActivateAbility까지

현재 RiteSeekers에서 Input 기반 Ability를 따라가면 대략 이런 순서로 이어집니다.

Text
InputAction
→ InputConfig
→ InputTag
→ HeroComponent
→ ASC AbilityInputTagPressed / Released
→ AbilitySpec / DynamicAbilityTags Match
→ Input SpecHandle State
→ PlayerController PostProcessInput
→ ASC ProcessAbilityInput
→ AbilitiesToActivate
→ TryActivateAbility
→ Activation 조건
→ ActivateAbility

처음 Source를 봤을 때는 단계가 꽤 많아 보였습니다.

하지만 실제로 문제가 생겼을 때 이 모든 함수를 처음부터 읽을 필요는 없었습니다.

입력이 ASC까지 들어왔는지, 현재 ASC에 기대한 AbilitySpec이 있는지, 그 Spec이 지금 누른 InputTag와 연결되어 있는지부터 확인하면 범위가 금방 줄었습니다.


InputAction과 InputTag

InputAction과 InputTag도 처음에는 비슷하게 느껴졌습니다.

제가 지금 구분해서 보는 기준은 이 정도입니다.

Text
InputAction
→ Player가 실제로 누르는 입력
 
InputTag
→ Gameplay에서 그 입력을 어떤 명령으로 볼지 나타내는 Tag

RiteSeekers에서는 InputAction이 InputConfig를 거쳐 InputTag와 연결되고, HeroComponent에서 그 Tag를 ASC로 전달합니다.

Text
InputAction
→ InputConfig
→ InputTag
→ HeroComponent
→ ASC AbilityInputTagPressed / Released

여기까지 정상이라고 해서 Ability가 실행된 것은 아닙니다.

ASC 입장에서는 이제 들어온 InputTag와 현재 자신이 가지고 있는 AbilitySpec 중 어떤 것을 연결할지 다시 찾아야 합니다.


InputTag와 AbilitySpec이 실제로 연결되는지 확인했다

입력이 ASC까지 들어오면 AbilityInputTagPressed()에서 현재 InputTag와 ASC가 가지고 있는 AbilitySpec의 DynamicAbilityTags를 비교합니다.

Debugger에서는 OneHandSword의 MainHand 공격 입력을 기준으로 실제 Runtime 값을 확인했습니다.

RIT-B06_Runtime_AbilityInputTagPressed_AbilitySpecMatch_Annotated.png

📸 Screenshot 1. AbilityInputTagPressed()에서 InputTag.Attack.MainHand와 GA_MeleeCombo_OneHandSword_1의 DynamicAbilityTags가 일치하고, 해당 AbilitySpec이 입력과 연결된 Runtime 상태.

이때 확인한 값은 다음과 같았습니다.

Text
InputTag
→ InputTag.Attack.MainHand
 
AbilitySpec.Ability
→ GA_MeleeCombo_OneHandSword_1
 
DynamicAbilityTags
→ InputTag.Attack.MainHand
 
AbilitySpec.Handle
→ 30

ProcessAbilityInput에서는 Activation 후보가 되는지 확인했다

AbilityInputTagPressed()에서 입력과 맞는 AbilitySpec을 찾았다고 해서 바로 Ability가 실행되는 것은 아닙니다.

입력 상태가 쌓인 뒤 PlayerController::PostProcessInput에서 ASC의 ProcessAbilityInput()이 호출됩니다.

Text
AbilityInputTagPressed
→ AbilitySpec Handle 저장
 
PlayerController
→ PostProcessInput
 
ASC
→ ProcessAbilityInput
→ AbilitiesToActivate
→ TryActivateAbility

앞에서 GA_MeleeCombo_OneHandSword_1이 AbilitySpec Handle 30이라는 것을 확인했기 때문에, 여기서는 같은 Handle이 실제 Activation 후보까지 이어지는지를 확인했습니다.

RIT-B06_Runtime_ProcessAbilityInput_TryActivateAbility_Handle30_Annotated.png

📸 Screenshot 2. ProcessAbilityInput()에서 AbilitySpec Handle 30이 AbilitiesToActivate에 들어가 있고, 같은 Handle이 TryActivateAbility()로 전달되는 Runtime 상태.

첫 번째 Screenshot에서 GA_MeleeCombo_OneHandSword_1과 연결됐던 Handle 30이 ProcessAbilityInput()에서도 Activation 후보로 이어지는 것을 확인했습니다.

여기까지 정상이라면 입력과 AbilitySpec 연결보다 이후 Activation 조건 쪽으로 확인 범위를 좁힐 수 있습니다.


AbilitySet과 AbilitySpec을 따로 본 이유

B03에서 AbilitySet과 AbilitySpec의 차이를 정리했는데, 실제 디버깅에서는 이 구분이 더 직접적으로 느껴졌습니다.

Text
AbilitySet
= 어떤 Ability를 줄지 구성한 데이터
 
AbilitySpec
= 그 Ability가 특정 ASC에 실제로 들어간 Runtime 상태

AbilitySet Asset에 Ability가 들어 있다고 해서 현재 Player가 그 Ability를 가지고 있는 것은 아닙니다.

실제로 ASC에 Grant되어 FGameplayAbilitySpec이 만들어져 있어야 합니다.

Lyra 계열 AbilitySet에서는 Entry에 설정한 InputTag도 AbilitySpec으로 같이 넘어갑니다.

Text
AbilitySet Entry
→ GameplayAbility Class
→ InputTag
→ FGameplayAbilitySpec
→ DynamicAbilityTags.AddTag(InputTag)
→ ASC GiveAbility

그래서 AbilitySpec이 보인다고 바로 끝내지 않고, 그 안의 DynamicAbilityTags에 기대한 InputTag가 들어 있는지도 같이 봤습니다.


InputConfig 쪽 값과 AbilitySpec 쪽 값을 비교했다

Input 쪽에서는 이런 값이 들어옵니다.

Text
InputAction
→ InputConfig
→ InputTag

반대로 Ability 쪽에는 이미 이런 상태가 만들어져 있습니다.

Text
AbilitySet
→ AbilitySpec
→ DynamicAbilityTags

Runtime에서는 AbilityInputTagPressed()에서 들어온 InputTag와 각 AbilitySpec의 DynamicAbilityTags를 비교해 입력과 연결된 Spec을 찾습니다.

Text
현재 들어온 InputTag
↕
AbilitySpec의 DynamicAbilityTags

이걸 보고 나니 Ability Class가 존재하는 것과 현재 입력으로 실행할 수 있는 것은 완전히 다른 문제라는 게 더 분명해졌습니다.

Text
Ability Class
→ AbilitySet
→ ASC Grant
→ AbilitySpec
→ InputTag 연결

여기까지 실제 Runtime에서 이어져 있어야 입력을 눌렀을 때 Activation 후보가 될 수 있습니다.

B02에서 InputData를, B03에서 AbilitySet 설정을 이미 보여줬기 때문에 이 글에서는 같은 Editor Screenshot을 또 넣지 않았습니다.

이번 글에서는 Debugger에서 실제 AbilitySpec이 만들어진 이후의 상태를 보여주는 편이 더 의미가 있다고 봤습니다.


AbilitySpec이 맞다면 Activation 조건을 봤다

InputTag와 AbilitySpec이 정상적으로 연결됐다면 그다음에는 Ability가 현재 실행 가능한 상태인지 확인했습니다.

여기에는 Ability마다 여러 조건이 들어갈 수 있습니다.

Text
ActivationPolicy
ActivationGroup
Required / Blocked Tag
TagRelationshipMapping
Cost
Cooldown

ActivationPolicy는 Ability가 어떤 방식으로 실행되는지와 연결되고, ActivationGroup은 다른 Ability와 동시에 실행할 수 있는지 같은 실행 관계에 영향을 줄 수 있습니다.

GameplayTag도 마찬가지입니다.

특정 상태에서 공격을 막도록 되어 있다면 현재 Ability에 걸린 Required / Blocked 조건과 ASC가 가지고 있는 GameplayTag를 같이 봐야 했습니다.

TagRelationshipMapping이 추가 조건을 만들고 있다면 그것도 같이 확인합니다.

Cost나 Cooldown을 사용하는 Ability라면:

Text
CheckCost
→ 필요한 자원이 있는가
 
CheckCooldown
→ 아직 Cooldown 중인가

도 Activation 전에 걸릴 수 있습니다.

모든 Ability가 똑같은 조건을 사용하는 것은 아니어서, 이 부분은 실제 Ability 설정을 기준으로 봤습니다.


AbilitySpec은 있는데 InputTag가 없다면

Source 구조를 보면서 이런 경우도 생각해볼 수 있었습니다.

Text
InputAction
→ 정상
 
InputTag
→ 정상
 
ASC
→ 입력 전달 정상
 
AbilitySpec
→ 존재
 
DynamicAbilityTags
→ 기대한 InputTag 없음
 
Ability
→ 실행되지 않음

이 상태라면 Montage나 Damage부터 볼 이유가 없습니다.

입력은 ASC까지 들어왔고 AbilitySpec도 존재하지만, 현재 InputTag와 그 AbilitySpec을 이어줄 Tag가 없기 때문입니다.

Text
InputTag가 ASC까지 들어옴
→ AbilityInputTagPressed
→ InputTag와 맞는 AbilitySpec 탐색
→ 기대한 DynamicAbilityTag가 없음
→ 해당 SpecHandle이 입력 상태에 들어가지 않음
→ Activation 후보로 이어지지 않음

이건 RiteSeekers에서 실제로 발생했던 버그를 기록한 내용은 아닙니다.

현재 Source 구조를 따라가면서 이런 상태라면 어디에서 흐름이 끊길 수 있는지 설명하기 위해 든 예시입니다.

이 정도 경계는 공개 글에서도 남겨두는 게 좋다고 봤습니다. 실제로 해결한 버그처럼 보이면 안 되기 때문입니다.


Asset 설정과 Runtime Spec을 같이 봤다

Runtime AbilitySpec에서 InputTag가 예상과 다르게 보인다면 바로 C++를 고치기보다 Asset 쪽부터 다시 확인할 수 있습니다.

AbilitySet 자체에 잘못된 InputTag가 들어 있을 수도 있습니다.

Text
AbilitySet Asset
→ 잘못된 InputTag

반대로 Asset에는 올바르게 들어 있는데 AbilitySpec을 만드는 과정에서 기대한 Tag가 남지 않았을 수도 있습니다.

Text
AbilitySet Entry
→ 올바른 InputTag
 
Grant
→ Runtime AbilitySpec의 Tag가 기대와 다름

그래서 실제로는:

Text
AbilitySet Asset
→ Grant 과정
→ Runtime AbilitySpec

순서로 값을 비교하는 편이 훨씬 빨랐습니다.

이렇게 보면 Asset을 고칠지 Source를 볼지도 자연스럽게 나뉩니다.


ActivateAbility까지 왔다면 Combat 쪽으로 넘어갔다

TryActivateAbility() 이후 실제 ActivateAbility()까지 진입했다면, 그때부터는 확인할 위치가 달라집니다.

근접 공격이라면 이후에:

Text
ActivateAbility
→ Montage
→ Notify
→ Trace
→ GameplayEffect

같은 흐름이 이어질 수 있습니다.

즉 키를 눌렀는데 ActivateAbility 자체가 호출되지 않는 상황과, Ability는 시작됐는데 Montage나 Trace 이후가 이어지지 않는 상황은 확인할 Source가 다릅니다.

후자의 경우에는 B04에서 정리한 Combat Flow 쪽을 다시 따라갔습니다.

예전에는 Ability가 안 된다는 이유로 Montage나 Damage까지 한 번에 내려갔다면, 지금은 ActivateAbility에 들어왔는지만 확인해도 어느 쪽을 봐야 할지 많이 좁힐 수 있었습니다.


정상 상태와 비교해 처음 달라지는 지점을 찾았다

디버깅할 때 도움이 됐던 것은 정상 상태의 Runtime 값을 먼저 확인해 두는 것이었습니다.

이번 공격 입력에서는 다음 흐름까지 확인했습니다.

Text
InputTag.Attack.MainHand
→ GA_MeleeCombo_OneHandSword_1
→ AbilitySpec Handle 30
→ AbilitiesToActivate
→ TryActivateAbility(Handle 30)

문제가 생겼을 때는 처음부터 Source 전체를 다시 읽기보다 정상 상태와 비교했습니다. 어디까지 같은 값이 유지되고 어느 지점부터 달라지는지를 먼저 찾으면 확인할 범위를 빠르게 줄일 수 있었습니다. 저는 이렇게 마지막 정상 지점과 처음 달라지는 지점 사이를 First Bad Boundary라고 정리해서 봤습니다.


Equipment Ability도 결국 같은 ASC로 들어왔다

B05에서는 Equipment가 바뀌면서 AbilitySet이 ASC에 Grant되는 흐름을 확인했습니다.

그래서 Weapon은 정상적으로 바뀌었는데 새 Ability가 실행되지 않는 상황이라면 이런 순서로 볼 수 있습니다.

Text
Equipment
→ AbilitySet
→ ASC AbilitySpec
→ DynamicAbilityTags
→ Input
→ Activation

Weapon Mesh는 바뀌었는데 ASC에 새 AbilitySpec이 없다면 아직 Input 쪽을 볼 단계가 아닙니다.

반대로 AbilitySpec까지 정상적으로 들어와 있다면 그다음에 InputTag와 Activation 조건을 확인할 수 있습니다.

장비 교체 전후 ASC State 자체는 B07에서 따로 이어서 정리합니다.


Source에서 다시 찾은 위치

이 흐름을 Source에서 다시 찾아갈 때 제가 주로 본 위치는 다음 정도였습니다.

Text
ULyraInputConfig
→ InputAction / InputTag
 
ULyraHeroComponent
→ ASC AbilityInputTagPressed / Released 호출
 
ULyraAbilitySystemComponent
→ InputTag / DynamicAbilityTags Matching
→ Input SpecHandle State
 
ALyraPlayerController
→ PostProcessInput
 
ULyraAbilitySystemComponent
→ ProcessAbilityInput
→ AbilitiesToActivate
→ TryActivateAbility
 
ULyraAbilitySet
→ AbilitySpec 생성
→ DynamicAbilityTags
 
ULyraGameplayAbility
→ ActivationPolicy / ActivationGroup
→ Tag / Cost / Cooldown

현재 Source에서는 HeroComponent → ASC Pressed / Released, PostProcessInput → ProcessAbilityInput, AbilitySet → AbilitySpec / DynamicAbilityTags로 이어지는 부분을 확인했습니다.

Class 이름 자체를 외우기보다 Input이 어느 Object를 지나 ASC Runtime State까지 들어오는지 알고 있는 편이 다시 Source를 찾을 때 더 편했습니다.


프로젝트를 진행하며 달라진 부분

예전에는 Ability가 실행되지 않으면 GameplayAbility부터 열었습니다.

지금은 먼저:

Text
Input
→ ASC
→ AbilitySpec
→ InputTag Match
→ Activation

중 어디까지 실제로 지나왔는지를 봅니다.

AbilitySpec이 없다면 Ability 내부를 볼 이유가 없고, Spec은 있는데 InputTag가 맞지 않는다면 Montage까지 내려갈 이유가 없습니다.

반대로 ActivateAbility까지 들어왔다면 그때부터 Montage, Trace, GameplayEffect 쪽으로 넘어갑니다.

이렇게 나눠서 보기 시작한 뒤부터 Ability가 안 나간다는 하나의 문제가 예전보다 훨씬 작게 느껴졌습니다.


정리

RiteSeekers에서 Ability Activation 흐름을 따라가며 가장 크게 바뀐 것은 Ability가 존재하는 것과 지금 누른 입력으로 실행할 수 있는 상태는 다르다고 보게 된 점입니다.

전체 흐름은 이렇게 이어집니다.

Text
InputAction
→ InputTag
→ HeroComponent
→ ASC Input State
→ ProcessAbilityInput
→ AbilitySpec
→ DynamicAbilityTags
→ Activation 조건
→ ActivateAbility

문제가 생겼을 때도 처음부터 GameplayAbility 전체를 읽지 않고:

Text
Input
→ ASC
→ AbilitySpec
→ Activation

순서로 실제 Runtime 값을 확인합니다.

그리고 정상 상태와 비교했을 때 처음 값이 달라지는 위치가 보이면 그 주변 Source만 다시 봅니다.

처음에는 Ability 내부에서 답을 찾으려고 했다면, 지금은 Ability가 시작되기 전에 어떤 Runtime State가 먼저 준비돼 있어야 하는지부터 보는 쪽으로 많이 바뀌었습니다.

빠른 검색

프로젝트, Evidence, 디버깅 노트, 학습 기록을 검색합니다.

목차