RIT-B04 | RiteSeekers — GAS Combat Flow와 Combat Rule 책임 분리
처음에는 Combat을 비교적 단순하게 생각했습니다.
Text
공격
→ 적중
→ Damage
→ HP 감소기능만 보면 틀린 설명은 아닙니다.
그런데 GAS와 RiteSeekers Source를 따라가 보니 이 한 줄 사이에서도 역할이 여러 단계로 나뉘어 있었습니다.
Text
GameplayAbility
→ GameplayEffect
→ DamageExecution
→ IncomingDamage
→ VitalSet
→ Health
→ Death공격 Ability가 Target의 HP를 직접 줄이는 것이 아니라, 앞 단계에서 만든 결과를 다음 단계가 받아 처리하는 식으로 이어지고 있었습니다.
공격 한 번 안에서도 여러 단계가 있었다
화면에서는 공격 버튼을 누르고 적의 HP가 줄어드는 것이 하나의 동작처럼 보입니다.
조금 더 펼쳐보면 다음과 같습니다.
Text
Input
→ GameplayAbility
→ 공격 Timing
→ Target / Hit 확인
→ GameplayEffect
→ Damage 계산
→ Health 반영
→ Death / UI근접 공격에서는 그 사이에 Montage, Notify, Trace가 들어갑니다.
Text
InputAction / InputTag
→ ASC
→ GameplayAbility
→ Montage / Notify
→ Trace
→ HitResult
→ GameplayEffect
→ DamageExecution
→ IncomingDamage
→ VitalSet
→ Health
→ Death / UI다만 이걸 모든 공격이 반드시 똑같이 거치는 Call Stack으로 보지는 않았습니다.
근접 공격은 Weapon Trace에서 Target을 만들 수 있지만 Projectile은 Projectile Actor에서 Hit가 생길 수 있고, AOE는 또 다른 방식으로 Target을 찾을 수 있습니다.
공격 방식이 달라도 결국 다시 보게 되는 것은 Target이 만들어진 뒤 Damage가 어떻게 Health까지 전달되는가였습니다.
GameplayAbility가 직접 HP를 깎는 것은 아니었다
Player Input이 들어오면 ASC는 현재 가진 AbilitySpec 중 입력과 연결된 Ability를 찾습니다.
Text
InputAction
→ InputTag
→ ASC
→ AbilitySpec
→ GameplayAbility ActivationAbility가 실행되면서 공격이 시작되지만, 실제 Combat 결과를 GameplayAbility 하나가 전부 처리하는 것은 아니었습니다.
제가 Source를 따라가며 나눠본 역할은 이렇습니다.
Text
GameplayAbility
→ 공격 흐름 시작
Montage / Notify
→ 공격 Timing
Trace / Projectile / AOE
→ Target과 Hit 정보 생성
GameplayEffect
→ Gameplay 변화 전달
DamageExecution
→ Damage 계산
VitalSet
→ 실제 Health 반영그래서 GameplayAbility를 Target의 HP를 깎는 함수보다는 공격에 필요한 여러 단계가 이어지도록 시작하고 연결하는 쪽으로 보게 됐습니다.
Animation Timing과 실제 Hit은 따로 봤다
근접 공격에서는 GameplayAbility가 Montage를 실행하고, 실제 타격 구간에 맞춰 Notify와 Trace가 이어집니다.
Text
GameplayAbility
→ Montage
→ AnimNotify
→ Trace공격 Animation에는 준비 동작, 실제 타격 구간, 후딜레이가 있기 때문에 Trace도 버튼을 누른 순간보다는 Weapon이 실제로 휘둘러지는 Timing에 맞춰 실행됩니다.
여기서 처음에는 Animation이 정상적으로 재생되면 Hit도 정상일 거라고 생각하기 쉬웠습니다.
하지만 둘은 다른 상태였습니다.
Text
Montage 재생
→ 공격 표현 / Timing
Trace 성공
→ 실제 Target / HitResultCharacter가 정상적으로 공격 Animation을 재생하고 있어도 Trace가 Target을 찾지 못할 수 있습니다.
이 구분은 이후 Animation 문제와 Combat 문제를 따로 볼 때도 도움이 됐습니다.
Trace에서 실제 Target이 정해졌다
근접 공격에서는 Trace가 실행되면서 실제로 맞은 대상을 찾습니다.
Text
AnimNotify
→ Weapon Trace
→ HitResult
→ Target화면에서 Weapon이 Enemy와 겹쳐 보이는 것과 Engine의 Trace가 실제 그 Actor를 Hit한 것은 같은 이야기가 아닙니다.
Trace 결과에는 여러 값이 영향을 줍니다.
Text
Trace 시작 / 끝 위치
Weapon Socket
Collision Channel
Ignore 대상
HitResultTarget과 Hit 정보가 이 단계에서 만들어져야 이후 Damage 처리에서도 그 결과를 사용할 수 있습니다.
Projectile이나 AOE처럼 Target을 만드는 방식이 달라져도 누가 Target을 만들고 그 Hit 정보를 다음 단계로 넘기는지를 보는 기준은 같았습니다.
GameplayEffect를 Damage 자체로 보지 않게 됐다
GAS를 처음 볼 때는 GameplayEffect = Damage처럼 생각했습니다.
Source를 따라가면서 GameplayEffect가 그보다 더 넓은 역할을 가진다는 걸 알게 됐습니다.
상황에 따라 이런 정보들이 들어갈 수 있습니다.
Text
Modifier
GameplayTag
Duration
SetByCaller
ExecutionCalculationRiteSeekers의 대표 Damage 경로에서는 GameplayEffect가 필요한 값과 EffectContext를 전달하고, 실제 Damage 계산은 URSDamageExecution에서 이어집니다.

📸 Screenshot 1. GE_Damage_SetByCaller에서 BaseDamage를 SetByCaller 값으로 받고, RSDamageExecution을 ExecutionCalculation으로 연결한 설정.
GameplayEffect 전체 Property를 다 보여주기보다 다음 연결이 한 화면에서 읽히면 충분합니다.
Text
GE_Damage_SetByCaller
→ SetByCaller.BaseDamage
→ ExecutionCalculation
→ RSDamageExecution실제 설정을 확인해보면 GameplayEffect 자체에서 최종 Damage를 계산하는 것이 아니라, Runtime에서 전달받은 BaseDamage를 가지고 어떤 ExecutionCalculation을 사용할지 연결하고 있었습니다. 실제 계산은 그다음 RSDamageExecution Source에서 이어집니다.
DamageExecution에서 Damage를 계산했다
RiteSeekers의 Damage Source를 따라갈 때 중심이 된 부분은 URSDamageExecution이었습니다.
현재 Source에서는 대표적으로 다음 값을 사용합니다.
Text
Source
→ BaseDamage
→ Strength
→ DrainLifePercent
Target
→ Defense
→ DamageReductionPercentAttribute만 사용하는 것도 아니었습니다.
EffectContext를 통해 실제 Hit에서 만들어진 정보도 함께 사용합니다.
Text
HitResult
HitActor
PhysicalMaterial
AbilitySource
Team현재 Source의 기본 Damage 관계는 다음과 같습니다.
Text
Damage
=
(BaseDamage + Strength)
/
pow(max(Defense, 1), 0.3)여기에 PhysicalMaterialAttenuation과 공격자와 Target 사이에서 Damage를 허용할지 결정하는 DamageInteractionAllowedMultiplier가 반영되고, 마지막에 DamageReductionPercent가 적용됩니다.
Text
DamageDone
=
Damage
* PhysicalMaterialAttenuation
* DamageInteractionAllowedMultiplier
DamageDone
-= DamageDone * (DamageReductionPercent / 100)처음에는 Damage를 계산했으면 그 자리에서 바로 Health를 수정할 거라고 생각했습니다.
실제 Source에서는 그렇지 않았습니다. 계산된 결과는 IncomingDamage로 넘겨지고, 실제 Health 반영은 이후 VitalSet에서 이어집니다.
Text
DamageExecution
→ IncomingDamage
→ VitalSet
→ Health이 계산과 Output이 실제 Source에서 어떻게 이어지는지 확인하기 위해 URSDamageExecution::Execute_Implementation()을 순서대로 따라갔습니다.
Text
BaseDamage / Strength / Defense / DamageReductionPercent
↓
Damage 계산
↓
PhysicalMaterialAttenuation / DamageInteractionAllowedMultiplier 반영
↓
최종 DamageDone 계산
↓
IncomingDamage Output
↓
VitalSet에서 Health 처리로 연결
🔡 SourceCode 1. URSDamageExecution::Execute_Implementation 초반부에서 BaseDamage, Strength, Defense, DrainLifePercent, DamageReductionPercent를 캡처해 Damage 계산에 사용할 입력값을 준비하는 부분.
Damage 계산을 시작하기 전에 Source와 Target에서 필요한 Attribute를 먼저 가져옵니다. 이 부분을 보면서 최종 Damage가 BaseDamage 하나만으로 결정되는 것이 아니라 여러 전투 상태를 함께 사용한다는 점을 확인했습니다.

🔡 SourceCode 2. 준비한 입력값에 PhysicalMaterialAttenuation, DamageInteractionAllowedMultiplier, DamageReductionPercent를 반영해 최종 DamageDone을 계산하는 부분.
기본 Damage를 만든 뒤 PhysicalMaterial과 Team Damage 허용 여부를 반영하고, 마지막으로 Damage Reduction을 적용합니다. 앞에서 가져온 값들이 실제 DamageDone으로 합쳐지는 구간입니다.

🔡 SourceCode 3. 계산된 DamageDone을 URSVitalSet::GetIncomingDamageAttribute()로 출력해 Target의 Vital 처리 단계로 넘기는 부분.
여기서 계산은 끝나지만 Health를 직접 수정하지는 않습니다. AddOutputModifier를 통해 계산 결과를 IncomingDamage Attribute로 넘기고, 실제 Health 반영은 VitalSet 쪽에서 이어집니다.

🔡 SourceCode 4.
URSVitalSet에서ATTRIBUTE_ACCESSORS(ThisClass, IncomingDamage);로IncomingDamage용 Attribute Accessor가 정의되어 있고, 앞선GetIncomingDamageAttribute()호출이 이 Attribute로 이어지는 것을 확인한 화면. 여기까지 따라가면서GetIncomingDamageAttribute()가 막연한 함수 호출이 아니라 실제IncomingDamageAttribute를 가리키고 있다는 점을 확인했습니다.이후
ATTRIBUTE_ACCESSORS매크로 내부 구현까지 더 내려가지는 않았습니다. 제가 확인하고 싶었던DamageExecution → IncomingDamage Attribute연결은 이 지점에서 이미 확인할 수 있었기 때문입니다.이 네 구간을 순서대로 따라가면
Attribute 입력 → Damage 계산 → IncomingDamage 출력 → VitalSet Attribute 연결로 이어집니다.
DrainLifePercent처럼 Source ASC의 Health에 영향을 주는 별도 경로도 있었지만, 여기서는 기본 Damage가 Target의IncomingDamage로 전달되는 흐름을 중심으로 봤습니다.
Hit Context도 Damage 결과에 영향을 줄 수 있었다
Damage 계산은 숫자 Attribute만으로 끝나지 않았습니다.
현재 Source에서는 HitActor, PhysicalMaterial 같은 Context도 실제 계산에 사용합니다.
그래서 Trace 성공과 DamageExecution에서 필요한 Hit Context가 제대로 전달된 것도 따로 볼 필요가 있었습니다.
Damage 값이 예상과 다르다면:
Text
BaseDamage
Strength
Defense
DamageReductionPercent만 보는 것이 아니라:
Text
HitActor
PhysicalMaterial
Team Damage 조건도 같이 확인할 수 있습니다.
특히 현재 Source 분석에서는 HitActor가 있어야 TeamSubsystem의 Damage 허용 판단으로 이어질 수 있고, 필요한 Context가 없다면 DamageInteractionAllowedMultiplier가 기대와 다르게 나오는 경우도 Debug 후보가 됩니다.
이걸 보면서 Combat Damage가 단순히 공격력 - 방어력 한 줄로 끝나는 것이 아니라 Attribute와 실제 Hit 정보가 같이 들어가는 계산이라는 점이 더 잘 보였습니다.
IncomingDamage가 VitalSet을 거쳐 Health에 반영됐다
DamageExecution에서 나온 값은 Target의 IncomingDamage로 전달됩니다.
Text
DamageExecution
→ IncomingDamage
→ VitalSetIncomingDamage 자체가 현재 HP 값은 아닙니다.
계산된 Damage를 실제 Health에 반영하기 위해 넘겨주는 중간 값에 가깝습니다.
VitalSet에서는 이 값을 받아 Health를 변경합니다.
Text
IncomingDamage
→ PreGameplayEffectExecute
→ Damage 적용 가능 여부 확인
→ PostGameplayEffectExecute
→ Health -= IncomingDamage
→ Health Clamp
→ 후속 Damage 처리
→ IncomingDamage Reset제가 구분해서 보는 역할은 이 정도입니다.
Text
DamageExecution
= 얼마나 Damage를 줄지 계산
VitalSet
= 그 Damage를 실제 Health에 반영이 차이를 알고 나니 계산된 Damage는 정상인데 Health가 줄지 않는 경우에도 계산과 적용을 따로 볼 수 있었습니다.
Source Anchor도 서로 나뉘어 있습니다.
Text
RSDamageExecution.cpp
→ URSDamageExecution::Execute_Implementation
RSVitalSet.cpp
→ PreGameplayEffectExecute
→ PostGameplayEffectExecute위의
DamageExecutionSource에서 계산 결과가IncomingDamage로 넘어가는 지점을 확인했고, 뒤의 Runtime 결과에서는 이 값이 실제 Health 변화로 이어지는 것까지 확인했습니다.
Health가 0이 되면 Death 쪽으로 이어졌다
VitalSet에서 Health가 감소하고 OutOfHealth 상태가 만들어지면 이후 Death 처리로 연결됩니다.
Source에서는 다음 흐름을 확인했습니다.
Text
VitalSet
→ OutOfHealth
→ HealthComponent
→ GameplayEvent_Death
→ Death Ability / 후속 사망 처리Combat 결과를 만드는 위치도 역할이 나뉘어 있었습니다.
Text
DamageExecution
→ Damage 계산
VitalSet
→ Health 변경
HealthComponent
→ Health 결과를 Death 흐름으로 연결Source의 구조를 확인한 것과 모든 Death 단계를 한 번의 Runtime Sequence로 끝까지 검증했다는 것은 같은 이야기는 아닙니다.
현재 B04에서 실제로 보여주는 Runtime Evidence는 Hit 뒤에 계산된 Damage가 Health 감소까지 이어지는 결과입니다.

📸 Screenshot 2.
CombatTrainingDummy를 직접 공격해 Damage가 실제 Health 감소로 이어지는 것을 확인한 Runtime 화면.

📸 Screenshot 3. 같은 공격에서
GE_Damage_SetByCaller의BaseDamage=22.0이DamageMagnitude=37.0으로 계산되고, Health가100.00 → 63.00으로 감소한 Output Log.
Output Log에서는 DamageExecution 단계의 DamageMagnitude=37.0과 이어진 HealthChanged의 100.00 → 63.00 변화를 같은 공격에서 확인했습니다. Editor의 GameplayEffect 설정, Source의 Damage 계산, 실제 Runtime Health 변화가 여기까지 이어졌습니다.
HP Bar는 Combat 결과를 보여주는 쪽이었다
Player 입장에서는 HP Bar가 줄어드는 것이 Damage 결과를 확인하기 가장 쉽습니다.
하지만 UI가 Damage를 계산하거나 Health를 소유하는 것은 아닙니다.
Text
Damage 계산
→ Health 변경
→ UI 표시UI는 마지막 결과를 보여주는 쪽에 가깝습니다.
그래서 HP Bar가 줄었다는 것만 보고 DamageExecution의 계산이나 HitContext까지 전부 정상이라고 판단하지는 않았습니다.
반대로 Gameplay Health는 정상적으로 바뀌었는데 UI만 갱신되지 않을 수도 있습니다.
Combat Runtime State와 화면 표현을 따로 보기 시작한 것도 이 때문입니다.
추가 Combat Rule은 Melee 밖으로 나눴다
기본 GAS Combat Flow를 따라본 뒤 RiteSeekers에서 직접 확장한 부분이 있습니다.
Blood나 Execution 같은 효과를 가장 단순하게 만든다면 Melee 안에서 바로 처리할 수도 있습니다.
Text
Melee Attack
→ Base Damage
→ Blood 처리
→ Execution 처리
→ 추가 Rule...처음 몇 개는 단순합니다.
그런데 Rule이 늘어날수록 Melee가 실제 공격뿐 아니라 각각의 추가 효과 조건까지 알아야 했습니다.
그래서 실제 Hit 결과가 만들어진 뒤를 하나의 지점으로 나눴습니다.
Text
Melee Ability
→ Target / HitResult
→ Base Combat Result
→ GameplayEvent.Combat.HitConfirmed
→ CombatAugment제가 나눈 역할은 다음과 같습니다.
Text
Melee
→ 실제 공격 결과를 만든다
CombatAugment
→ 그 공격 결과를 받아 추가 Rule을 처리한다이렇게 바꾼 뒤에는 Melee가 Blood나 Execution의 세부 조건을 직접 알 필요가 없어졌습니다.
HitConfirmed는 공격 시작 Event가 아니었다
GameplayEvent.Combat.HitConfirmed는 공격 버튼을 눌렀다는 뜻이 아닙니다.
Montage가 재생됐다는 뜻도 아닙니다.
RiteSeekers에서는 실제 Target과 HitResult가 만들어지고, 후속 Rule이 사용할 수 있는 Combat 결과가 생긴 뒤에 전달합니다.
Text
Melee
→ Target / HitResult
→ HitConfirmed
HitConfirmed
→ Blood
HitConfirmed
→ Execution같은 Hit 결과를 받아도 각 Rule의 해석은 다릅니다.
Text
Blood
→ Periodic Damage
Execution
→ Health Ratio 기반 Finish RuleB04에서는 기본 Combat Flow와 후속 Rule이 갈라지는 구조까지 이어서 봤습니다.
Blood와 Execution의 실제 Trigger / Remove / Dead Target 결과는 B10에서 Runtime으로 따로 확인했습니다.
RiteSeekers에서 직접 손댄 Combat 쪽
GAS의 Ability, GameplayEffect, Attribute Framework 자체를 새로 만든 것은 아닙니다.
그 기반 흐름을 따라가며 이해한 뒤 RiteSeekers에서는 다음 Combat 영역을 직접 확장했습니다.
Text
Melee Combat Result
→ GameplayEvent.Combat.HitConfirmed
→ CombatAugment Runtime Layer
→ Blood / Execution특히 Melee가 각 Combat Rule을 직접 호출하는 대신 실제 적중 결과만 Event로 넘기도록 나눈 것이 현재 프로젝트에서 추가한 부분입니다.
Reference 범위는 B01에서 이미 따로 구분했기 때문에 여기서는 Combat Source에 필요한 내용만 남겼습니다.
프로젝트를 진행하며 달라진 부분
처음에는 Combat을:
Text
공격
→ Damage
→ HP 감소정도로 생각했습니다.
지금은 그 사이에 어떤 역할이 있는지 조금 더 나눠서 봅니다.
Text
GameplayAbility
→ 공격 흐름 시작
Target / Hit
→ 실제 공격 대상 결정
GameplayEffect
→ Gameplay 변화 전달
DamageExecution
→ Damage 계산
IncomingDamage / VitalSet
→ 실제 Health 반영
HealthComponent
→ Death 흐름 연결추가 효과가 필요할 때도 기존 Melee 안에 조건을 계속 추가하기보다:
Text
실제 Hit
→ HitConfirmed
→ 후속 Combat Rule처럼 어디에서 역할을 넘길지를 먼저 생각하게 됐습니다.
정리
RiteSeekers의 GAS Combat Source를 따라가면서 가장 크게 달라진 것은 Damage를 하나의 처리라고 생각하지 않게 된 점이었습니다.
전체 흐름은 다음처럼 볼 수 있습니다.
Text
Input
→ GameplayAbility
→ Target / Hit
→ GameplayEffect
→ DamageExecution
→ IncomingDamage
→ VitalSet
→ Health
→ Death / UI근접 공격에서는 Target이 만들어지기 전에:
Text
GameplayAbility
→ Montage
→ Notify
→ Trace
→ HitResult같은 과정이 들어갈 수 있습니다.
그리고 RiteSeekers에서는 실제 Hit 이후를 다시 나눴습니다.
Text
Melee
→ HitConfirmed
→ CombatAugment
→ Blood / Execution처음에는 공격 Animation이 나오고 Enemy HP가 줄어들면 Combat 기능이 끝났다고 생각했습니다.
Source를 따라가면서 공격을 시작하는 위치, Target을 만드는 위치, Damage를 계산하는 위치, Health를 바꾸는 위치, Death로 이어지는 위치가 서로 나뉘어 있다는 점을 이해하게 됐습니다.
그리고 추가 Rule도 기본 Melee에 계속 붙이지 않고 HitConfirmed 뒤로 나누면서, 기본 공격과 후속 Combat Rule의 역할도 따로 볼 수 있게 됐습니다.