언리얼 엔진/프로젝트

[언리얼 엔진] 팀 프로젝트 기록 07 - MeleeCombatComponent로 근접 공격과 콤보 구현하기

dhlee-dev 2026. 5. 20. 22:14

이전 글에서는 CombatComponent로 전투 행동 상태 관리하기를 정리했다.

CombatComponent는 현재 공격할 수 있는지, 재장전 중인지, 회피 중인지 같은 전투 상태를 판단하는 역할을 맡았다.
이번에는 그 다음 단계로, 실제 근접 공격을 실행하는 MeleeCombatComponent를 구현한 내용을 정리해보려고 한다.

CombatComponent가 “지금 공격할 수 있는가?”를 판단한다면,
MeleeCombatComponent는 “근접 공격을 실제로 어떻게 실행할 것인가?”를 담당한다.


MeleeCombatComponent의 역할

근접 전투는 단순히 공격 버튼을 눌렀을 때 데미지를 주는 기능만으로 끝나지 않는다.
공격 애니메이션을 재생해야 하고, 콤보 입력도 받아야 하며, 실제로 무기가 휘둘리는 타이밍에만 공격 판정이 발생해야 한다.

그래서 근접 전투의 역할을 다음과 같이 나누었다.

CombatComponent
- 현재 전투 상태 관리
- 공격 가능 여부 판단
- 스태미너 소모 가능 여부 확인
- 최종 공격력 계산
- MeleeCombatComponent에 공격 실행 요청

MeleeCombatComponent
- 근접 공격 실행
- 공격 몽타주 재생
- 콤보 섹션 전환
- Trace 기반 공격 판정
- 공격 종료 처리

MeleeWeaponBase
- 무기별 공격 데이터 보유
- 약공격 / 강공격 몽타주
- 콤보 섹션 이름
- 데미지 배율
- TraceStart / TraceEnd 소켓
- TraceRadius

핵심은 MeleeCombatComponent가 공격 가능 여부를 전부 판단하는 것이 아니라,
CombatComponent가 공격 가능 여부를 판단한 뒤 실제 공격 실행만 MeleeCombatComponent에 맡기는 구조이다.

이렇게 하면 상태 관리와 공격 실행의 책임을 나눌 수 있다.


기본 공격 흐름

처음에는 단순히 약공격 몽타주를 재생하는 것부터 시작했다.

전체 흐름은 다음과 같다.

1. PlayerCharacter가 공격 입력을 받는다.
2. CombatComponent가 현재 공격 가능한 상태인지 확인한다.
3. WeaponComponent에서 현재 근접 무기를 가져온다.
4. StatComponent에서 공격력과 스태미너를 확인한다.
5. MeleeCombatComponent에 현재 무기와 공격력을 전달한다.
6. MeleeCombatComponent가 공격 몽타주를 재생한다.
7. 공격이 끝나면 CombatComponent에 공격 종료를 알린다.

이 구조에서 MeleeCombatComponent는 현재 근접 무기를 받아 공격 애니메이션을 재생하고,
공격 중 필요한 콤보 상태와 판정 타이밍을 관리한다.

공격이 끝났는지는 몽타주 종료 이벤트를 이용해서 처리했다.

공격 몽타주가 종료되면 FinishAttack함수를 호출하고,
이후 OnAttackFinished 이벤트를 통해 CombatComponent에 공격이 끝났다는 사실을 알려준다.

공격 몽타주 종료
→ MeleeCombatComponent::FinishAttack
→ OnAttackFinished Broadcast
→ CombatComponent가 Attacking 상태 해제

이렇게 해서 근접 공격이 끝난 뒤에만 다시 다른 행동을 할 수 있도록 만들었다.


약공격과 강공격

근접 공격은 약공격과 강공격으로 나누었다.

약공격
- 빠른 공격
- 3단 콤보 구조

강공격
- 느리지만 강한 공격
- 2단 콤보 구조

약공격과 강공격은 서로 다른 몽타주를 사용할 수 있고, 콤보 섹션도 따로 가진다.

예를 들어 약공격은 다음과 같은 섹션을 가진다.

Basic1
Basic2
Basic3

강공격은 다음과 같은 섹션을 가진다.

Smash1
Smash2

이 섹션 이름은 무기마다 다를 수 있으므로 MeleeWeaponBase가 가지고 있도록 했다.

즉, MeleeCombatComponent는 “현재 몇 번째 콤보를 실행할지”만 판단하고,
실제 섹션 이름이나 몽타주 정보는 무기에서 가져오는 방식이다.


콤보 공격 구현

콤보 공격은 하나의 몽타주 안에서 여러 섹션을 나누는 방식으로 구현했다.

처음에는 Montage_SetNextSection을 사용해서 현재 섹션이 끝나면 다음 섹션으로 이어지도록 구현했다.
이 방식도 콤보를 연결하는 데 사용할 수는 있었지만, 실제로 적용해보니 내가 원했던 공격 느낌과는 조금 달랐다

콤보 입력 가능 구간이 열림
→ 플레이어가 공격 입력
→ 즉시 다음 콤보 섹션으로 전환

내가 원했던 것은 입력이 들어왔을 때 현재 섹션이 끝날 때까지 기다리는 것이 아니라,
바로 다음 공격으로 넘어가는 느낌이었다.

그래서 이후에는 Montage_SetNextSection 방식 대신,
입력이 들어온 순간 Montage_JumpToSection으로 다음 섹션에 바로 이동하는 방식으로 변경했다.

콤보 입력 발생
→ 다음 콤보 섹션 이름 확인
→ Montage_JumpToSection으로 즉시 이동

이렇게 하면 입력이 들어온 순간 다음 콤보 공격으로 빠르게 전환할 수 있다.


콤보 입력 가능 구간

콤보 입력은 항상 받을 수 있게 하면 안 된다고 생각했다.

공격이 시작되자마자 입력을 받아버리면 콤보가 너무 쉽게 이어질 수 있고,
반대로 너무 늦게 받으면 조작감이 답답해질 수 있다.

그래서 콤보 입력을 받을 수 있는 구간을 애니메이션에서 직접 제어하도록 했다.
이를 위해 AnimNotifyState_ComboInput을 만들었다.

즉, 애니메이션 타임라인에서 원하는 구간에 AnimNotifyState_ComboInput을 배치하면,
그 구간에서만 다음 콤보 입력을 받을 수 있다.

공격 초반 입력
→ 무시

콤보 입력 가능 구간 입력
→ 다음 섹션으로 전환

콤보 입력 구간 이후 입력
→ 무시

이렇게 하니 콤보 입력 타이밍을 코드가 아니라 애니메이션 타임라인에서 조절할 수 있었다.


공격 판정 구현

근접 공격 판정은 무기 소켓을 기준으로 잡았다.

무기에는 공격 판정에 사용할 시작 소켓과 끝 소켓을 두었다.

TraceStart
TraceEnd

처음에는 이 두 점 사이를 단순히 Line Trace로 검사하는 방식도 생각했다.
하지만 Line Trace는 두께가 없는 선 하나이기 때문에, 망치나 대검처럼 부피가 큰 무기의 느낌을 표현하기 어렵다.

그래서 TraceStartTraceEnd 사이를 Sphere Trace로 검사하는 방식으로 변경했다.

TraceStart ~ TraceEnd 사이를 Sphere Trace
무기마다 TraceRadius 설정

이렇게 하면 무기마다 판정 두께를 다르게 줄 수 있다.

작은 무기
- 작은 TraceRadius

큰 망치
- 큰 TraceRadius

즉, 소켓 위치와 Radius를 조절해서 무기별로 길이와 두께가 다른 공격 판정을 만들 수 있다.


AnimNotifyState로 공격 판정 구간 제어

테스트 단계에서는 공격 애니메이션 전체 동안 공격 판정을 켜도 동작은 한다.
하지만 실제 게임에서는 무기가 휘둘리는 구간에만 판정이 있어야 한다.
공격 준비 동작이나 후딜레이 구간에 적이 맞으면 어색하기 때문이다.

그래서 공격 판정도 AnimNotifyState로 제어했다.
이를 위해 AnimNotifyState_MeleeTrace를 만들었다.

NotifyBegin
→ BeginAttackTrace

NotifyEnd
→ EndAttackTrace

몽타주 타임라인에서 실제로 무기가 휘둘리는 구간에만 AnimNotifyState_MeleeTrace를 배치하면,
그 구간에만 Sphere Trace가 실행된다.

이렇게 해서 공격 판정 타이밍을 코드가 아니라 애니메이션에서 조절할 수 있게 되었다.


중복 타격 방지

근접 공격 판정은 일정 간격으로 반복해서 Trace를 수행한다.
그런데 한 번의 공격 구간 안에서 같은 적이 여러 번 Trace에 걸릴 수 있다.
이 상태로 바로 데미지를 주면 한 번의 공격에 같은 적이 여러 번 맞는 문제가 생길 수 있다.

그래서 TSet으로 맞은 적 목록을 관리했고, 공격 판정이 시작될 때 이미 맞은 적 목록을 초기화하고,
Trace 중 이미 맞은 적이라면 다시 데미지를 주지 않도록 했다.

BeginAttackTrace
→ HitActors 초기화

Trace 중 적 감지
→ 이미 HitActors에 있으면 무시
→ 없다면 데미지 적용 후 HitActors에 추가

이렇게 해서 한 번의 공격에서는 같은 적이 한 번만 맞도록 처리했다.

트레이스 처리 흐름은 크게 세 단계로 나누었다.

첫 번째로, 무기 메시에서 TraceStart, TraceEnd 소켓 위치를 가져온다.

두 번째로, 두 소켓 사이를 기준으로 Sphere Trace를 수행한다.
이때 디버그 확인을 위해 캡슐을 그렸고, 디버그 모드에서만 표시되도록 ENABLE_DRAW_DEBUG를 사용했다.

마지막으로, 충돌한 액터가 적인지 확인하고 이미 맞은 적이 아니라면 데미지를 적용한다.


JumpToSection과 NotifyState End 문제

Montage_SetNextSection 방식에서 Montage_JumpToSection 방식으로 변경하면서 또 다른 문제가 생겼다.

현재 섹션에서 AnimNotifyState_MeleeTraceAnimNotifyState_ComboInput이 열린 상태에서
바로 다음 섹션으로 점프하면, 이전 섹션의 NotifyEnd가 정상적으로 호출되지 않을 수 있다.

그러면 공격 판정이나 콤보 입력 상태가 남아 있을 수 있다.

Light_1의 Trace 구간 시작
→ 콤보 입력으로 Light_2로 Jump
→ Light_1의 NotifyEnd가 호출되지 않을 수 있음
→ Trace나 ComboInput 상태가 남을 수 있음

그래서 콤보 전환 전에 열려 있을 수 있는 상태를 명시적으로 정리하도록 했다.

EndAttackTrace
CloseComboInput
Montage_JumpToSection

여기서 중요한 것은 순서였다.

처음에는 JumpToSection 이후에 정리해도 되지 않을까 생각했다.
하지만 그렇게 하면 다음 섹션에서 새로 시작된 NotifyState까지 바로 꺼버릴 수 있다.

그래서 최종 순서는 다음과 같이 정리했다.

현재 섹션 상태 정리
→ 다음 섹션으로 Jump

이 부분은 콤보 전환에서 꽤 중요한 문제였다.


공격 방향과 전진 처리

이후 공격 조작감을 위해 몇 가지 NotifyState를 추가했다.

AnimNotifyState_DirectionInput
- 공격 준비 중 입력 방향 저장

AnimNotifyState_AttackTurn
- 저장된 방향으로 공격 중 회전

AnimNotifyState_MoveForward
- 공격 중 전진 처리

공격 버튼을 누른 뒤에도 플레이어가 방향키를 입력하면,
공격이 나가는 방향을 어느 정도 보정할 수 있게 하기 위한 구조이다.

또 공격 중 앞으로 살짝 이동하도록 해서 근접 공격에 전진성을 줄 수 있도록 했다.

다만 이 부분은 MeleeCombatComponent 자체보다는 PlayerCharacter의 이동과 회전 제어에 더 가까운 내용이다.

그래서 이번 글에서는 자세히 다루기보다는, 근접 공격의 타이밍 제어를 위해 NotifyState를 확장했다는 정도로만 정리하려고 한다.


공격 종료 처리

공격이 끝나면 내부 상태를 초기화해야 한다.
공격 타입, 콤보 인덱스, 콤보 입력 가능 여부, 현재 무기, 현재 데미지, 맞은 적 목록 등을 정리해야 다음 공격이 정상적으로 시작된다.

공격 종료 흐름은 다음과 같다.

공격 몽타주 종료
→ FinishAttack
→ 공격 Trace 종료
→ 공격 상태 초기화
→ OnAttackFinished Broadcast
→ CombatComponent가 Attacking 상태 해제

MeleeCombatComponent는 자신의 공격 상태를 정리하고,
CombatComponent는 전투 행동 상태를 정리한다.

이렇게 역할을 나누면 근접 공격 컴포넌트가 전투 전체 상태까지 직접 관리하지 않아도 된다.


실행 예시


전체 흐름 정리

현재 근접 공격 흐름은 다음과 같이 정리할 수 있다.

PlayerCharacter
- 공격 입력

CombatComponent
- 현재 상태 확인
- 스태미너 확인
- 공격력 계산
- MeleeCombatComponent에 공격 요청

MeleeCombatComponent
- 현재 무기 확인
- 공격 몽타주 재생
- 콤보 입력 처리
- 공격 판정 Trace 실행
- 공격 종료 이벤트 발생

AnimNotifyState
- 콤보 입력 가능 구간 제어
- 공격 판정 구간 제어
- 방향 입력 / 회전 / 전진 타이밍 제어

MeleeWeaponBase
- 무기별 공격 몽타주
- 콤보 섹션 이름
- 데미지 배율
- Trace 정보 제공

정리하면 MeleeCombatComponent는 근접 공격의 실제 실행을 담당하고,
공격 가능 여부와 자원 처리는 CombatComponent, 무기별 데이터는 MeleeWeaponBase, 타이밍 제어는 AnimNotifyState가 담당하는 구조이다.


구현하면서 느낀 점

이번에 MeleeCombatComponent를 구현하면서 가장 많이 고민한 부분은 타이밍이었다.

근접 공격은 단순히 몽타주를 재생하는 것으로 끝나지 않았다.

언제 공격 판정을 켤지
언제 콤보 입력을 받을지
SetNextSection으로 이어갈지, JumpToSection으로 즉시 넘길지
언제 공격 상태를 초기화할지
JumpToSection 시 이전 NotifyState를 어떻게 정리할지

이런 타이밍이 조금만 어긋나도 공격이 나가지 않았는데 스태미너만 줄어들거나,
콤보 입력이 이상하게 남거나, 공격 판정이 의도하지 않은 구간에 남는 문제가 생길 수 있었다.

그래서 공격 가능 여부는 CombatComponent가 판단하고,
공격 실행과 콤보, 판정 처리는 MeleeCombatComponent가 담당하도록 나누었다.

또 공격 판정과 콤보 입력 구간은 코드에서 고정하기보다 AnimNotifyState를 이용해 애니메이션 타임라인에서 조절하도록 했다.

이 구조 덕분에 코드의 책임을 나누면서도, 실제 공격 감각은 애니메이션에서 조정할 수 있게 되었다.


정리

이번에 MeleeCombatComponent를 구현하면서 근접 공격은 단순히 몽타주를 재생하는 기능만으로 끝나지 않는다는 것을 많이 느꼈다.

처음에는 공격 입력이 들어오면 몽타주를 재생하고, 필요한 구간에서 판정만 넣으면 될 것이라 생각했다.
하지만 실제로 구현해보니 콤보 입력 타이밍, 섹션 전환, 공격 판정 구간, 공격 종료 처리까지 서로 맞물려야 했다.

특히 콤보 공격을 구현하면서 예상하지 못한 문제들이 있었다.

공격 사운드는 재생되는데 몽타주가 이어지지 않는 문제
몽타주 재생 자체가 실패하는 문제
콤보 입력은 들어왔지만 다음 섹션으로 전환되지 않는 문제
JumpToSection 시 NotifyState가 정상적으로 정리되지 않는 문제

처음에는 Montage_SetNextSection 방식으로 콤보를 구현했다.

하지만 실제로 적용해보니 다음 섹션을 미리 연결해두는 느낌에 가까웠고,
내가 원했던 “입력한 순간 바로 다음 공격으로 넘어가는 느낌”과는 조금 달랐다.

그래서 이후에는 콤보 입력이 들어왔을 때 Montage_JumpToSection으로 바로 다음 섹션으로 이동하는 방식으로 변경했다.
이 과정에서 콤보 입력 구간, 섹션 전환 타이밍, NotifyState 정리 순서가 중요하다는 것을 알게 되었다.

AnimNotifyState를 사용해서 콤보 입력 구간과 공격 판정 구간을 나누면서,
공격 로직을 전부 코드에서 고정하는 것보다 애니메이션 타임라인에서 타이밍을 조절하는 방식이 왜 필요한지도 느꼈다.

이번 작업을 통해 근접 전투는 상태, 입력, 애니메이션, 판정 타이밍이 모두 맞아야 자연스럽게 동작한다는 것을 알게 되었다.
문제가 많아서 디버깅하는 데 시간이 걸렸지만, 그만큼 몽타주 섹션과 NotifyState 기반 근접 공격 구조를 많이 배울 수 있었던 작업이었다.

다음 글에서는 원거리 공격과 재장전을 담당하는 RangedCombatComponent를 구현한 내용을 정리해보려고 한다.