이전 글에서는 MeleeCombatComponent로 근접 공격과 콤보 구현을 정리했다.
이번에는 원거리 공격과 재장전을 담당하는 RangedCombatComponent를 구현한 내용을 정리해보려고 한다.
CombatComponent가 “현재 공격할 수 있는 상태인가?”를 판단한다면,RangedCombatComponent는 “현재 장착된 총으로 실제 사격과 재장전을 어떻게 실행할 것인가?”를 담당한다.
RangedCombatComponent의 역할
이전 글들에서 정리한 것처럼, 이번 프로젝트에서는 무기 장착, 전투 상태 판단, 실제 공격 실행을 각각 다른 컴포넌트로 나누었다.
이번에 구현한 RangedCombatComponent는 그중 원거리 공격의 실제 실행을 담당한다.
즉, RangedCombatComponent는 총을 소유하거나 장착을 관리하는 컴포넌트가 아니다.
현재 장착된 GunBase를 전달받아 사격과 재장전을 실행하는 역할을 한다.
역할을 간단히 정리하면 다음과 같다.
GunBase
- 총기 데이터와 탄약 상태 관리
- 사격 가능 여부 판단
- 재장전 가능 여부 판단
- Projectile, 사거리, 탄속 정보 제공
RangedCombatComponent
- 현재 GunBase를 이용해 사격 실행
- Projectile 생성
- 사격 사운드 / 애니메이션 실행
- 재장전 몽타주 재생
- 재장전 적용 및 종료 처리
CombatComponent
- 현재 전투 상태 판단
- 공격력 계산
- RangedCombatComponent에 실행 요청
이번 글에서는 그중 RangedCombatComponent가 실제로 어떻게 사격과 재장전을 처리했는지 정리하려고 한다.
GunBase와 역할 분리
GunBase는 총기 자체의 상태와 규칙을 관리한다.
탄약 상태
- CurrentAmmo
- RemainAmmo
- MaxReserveAmmo
사격 정보
- ProjectileClass
- Range
- ProjectileSpeed
- DamageRate
- RoF
재장전 정보
- ReloadPerAmmo
- ReloadSound
- ReloadAnimation
- bReloading
즉, GunBase는 사격 가능 여부, 탄약 소모, 재장전 가능 여부, 실제 탄약 보충 같은 총기 자체의 규칙을 담당한다.
반면 RangedCombatComponent는 이 정보를 이용해서 Projectile을 생성하고,
사격/재장전 애니메이션을 실행하는 역할을 맡는다.
RangedCombatComponent
- GunBase에게 사격 가능한지 물어봄
- GunBase의 총구 위치와 ProjectileClass를 사용함
- Projectile을 실제로 Spawn함
- 사격 사운드와 애니메이션을 실행함
- 재장전 몽타주를 재생함
- 재장전 몽타주 종료 시 재장전을 마무리함
이렇게 나누면 총기 상태는 GunBase, 실제 실행은 RangedCombatComponent로 분리할 수 있다.
CombatComponent와의 연결
원거리 공격 입력은 PlayerCharacter가 직접 총을 쏘는 방식으로 처리하지 않았다.
입력은 CombatComponent로 전달하고, CombatComponent가 현재 상태와 무기를 확인한 뒤RangedCombatComponent에 실행을 요청하도록 했다.
사격 흐름은 다음과 같다.
PlayerCharacter
→ 사격 입력 전달
CombatComponent
→ 현재 전투 행동 가능 여부 확인
→ WeaponComponent에서 현재 Gun 조회
→ StatComponent에서 최종 공격력 조회
→ RangedCombatComponent::TryFire 호출
RangedCombatComponent
→ GunBase를 이용해 사격 실행
재장전도 비슷하다.
PlayerCharacter
→ 재장전 입력 전달
CombatComponent
→ WeaponComponent에서 현재 Gun 조회
→ Reloading 상태로 전환 가능한지 확인
→ RangedCombatComponent::TryReload 호출
RangedCombatComponent
→ 재장전 몽타주 재생
→ 재장전 적용 및 종료 처리
이렇게 하면 PlayerCharacter는 총기 세부 로직을 알 필요가 없다.
캐릭터는 입력만 전달하고, 전투 상태 판단은 CombatComponent, 실제 사격과 재장전 실행은 RangedCombatComponent가 담당한다.
Projectile 방식으로 사격 처리하기
원거리 공격은 기획서에서 발사체 방식으로 구현하는 방향이었다.
즉, 카메라 방향으로 LineTrace를 쏴서 맞은 대상을 즉시 판정하는 히트스캔 방식이 아니라,
총알 Actor를 실제로 생성하고, 해당 발사체가 이동하다가 충돌했을 때 데미지를 주는 구조가 필요했다.
다만 플레이어 입장에서 총알이 눈에 보이면서 천천히 날아가면 사격이 답답하게 느껴질 수 있다고 생각했다.
그래서 구조적으로는 실제 Projectile을 생성하지만, Projectile 속도를 빠르게 설정하고 메시를 보이지 않게 처리했다.
구현 구조
- Projectile Actor 생성
- ProjectileMovement로 이동
- 충돌 시 데미지 적용
플레이 체감
- 발사체가 매우 빠르게 이동
- 메시를 숨겨 총알이 거의 바로 맞는 것처럼 보이게 처리
즉, 기획에서 요구한 Projectile 구조를 따르면서도, 플레이어 입장에서는 히트스캔처럼 빠르게 맞는 느낌을 주려고 했다.
사격 흐름은 다음과 같이 정리했다.

1. Gun이 유효한지 확인한다.
2. Gun이 발사 가능한 상태인지 확인한다.
3. ProjectileClass가 있는지 확인한다.
4. 최종 데미지를 계산한다.
5. 탄약을 소모한다.
6. 총구 위치에서 Projectile을 생성한다.
7. 사격 사운드와 애니메이션을 재생한다.
데미지는 StatComponent에서 가져온 공격력에 총기의 데미지 배율을 곱해 계산한다.
ActualDamage = AttackPower * GunDamageRate
이렇게 하면 같은 총을 사용하더라도 캐릭터의 스탯에 따라 실제 데미지가 달라질 수 있다.
생성된 Projectile은 데미지, 사거리, 속도, 가해자 정보를 초기화한 뒤 날아가고, 충돌했을 때 데미지를 적용하도록 했다.
GunFireProjectile 수정
발사체는 언리얼 1인칭 템플릿의 Projectile 코드를 기반으로 만들었다.
기본적인 이동과 충돌 구조는 거의 그대로 사용했고, 현재 프로젝트에 필요한 값들만 추가했다.
추가한 값은 다음과 같다.
Damage
- 충돌 시 적용할 데미지
DamageInstigator
- 데미지를 발생시킨 컨트롤러
DamageCauser
- 데미지를 발생시킨 액터
InitializeProjectile
- 발사 시 데미지, 사거리, 속도, 가해자 정보를 초기화하는 함수
RangedCombatComponent는 Projectile을 생성한 뒤 InitializeProjectile을 호출해서 필요한 값을 넘겨준다.
Projectile 내부에서는 충돌이 발생하면 다음과 같은 흐름으로 처리한다.
Projectile 충돌
→ 자기 자신 또는 발사자라면 무시
→ 유효한 대상이면 ApplyDamage
→ Projectile Destroy
이렇게 해서 RangedCombatComponent는 Projectile 생성과 초기화만 담당하고,
실제 충돌과 데미지 적용은 GunFireProjectile에서 처리하도록 분리했다.
조준 방향 문제
Projectile 방식으로 사격을 구현하면서 조준 방향 문제가 있었다.
총구는 캐릭터 왼손에 있고, 크로스헤어는 카메라 기준 화면 중앙에 있다.
그래서 단순히 총구의 Forward 방향으로 Projectile을 생성하면 크로스헤어가 가리키는 위치와 총알 방향이 맞지 않았다.
이를 해결하기 위해 카메라 기준 조준점을 먼저 구하고,
총구에서 그 조준점을 향해 Projectile을 발사하는 방식으로 정리했다.
카메라 위치에서 화면 중앙 방향으로 Trace
→ AimPoint 계산
→ 총구 위치에서 AimPoint를 향하는 방향 계산
→ 해당 방향으로 Projectile 생성
즉, 방향 계산은 다음 개념에 가깝다.
FireDirection = AimPoint - MuzzleLocation
처음에는 AimPoint를 구할 때 TraceEnd를 총의 사거리 기준으로 잡았다.
하지만 이렇게 하니 총알이 크로스헤어보다 오른쪽으로 도착하는 문제가 있었다.

원인을 확인해보니 AimPoint가 너무 가까운 것이 문제였다.
카메라 위치와 총구 위치는 서로 다르기 때문에, 가까운 지점을 조준점으로 잡으면
총구에서 그 지점을 향하는 방향과 화면 중앙 방향의 차이가 크게 보일 수 있다.
AimPoint가 가까움
→ 카메라 위치와 총구 위치 차이가 크게 반영됨
→ 총알이 크로스헤어보다 오른쪽으로 어긋남
그래서 현재는 조준점을 구할 때 총의 사거리 대신, 카메라 전방으로 충분히 먼 거리까지 Trace하도록 변경했다.
AimTraceDistance = 10000.f
이렇게 AimPoint를 멀리 잡으니, 총구에서 조준점을 향해 발사하더라도
크로스헤어와 실제 탄착 방향이 거의 일치하게 되었다.

정리하면 다음과 같다.
기존 방식
- AimPoint를 총의 사거리 기준으로 계산
- AimPoint가 가까워 총구와 카메라 위치 차이가 크게 드러남
- 크로스헤어보다 오른쪽으로 총알이 날아감
수정 방식
- AimPoint를 카메라 전방의 충분히 먼 지점으로 계산
- 총구에서 AimPoint를 향해 발사
- 크로스헤어와 탄착 방향이 거의 일치
Projectile 수명과 사거리 문제
Projectile을 구현하면서 사거리와 수명 처리도 문제가 되었다.
처음에는 Projectile에 사거리와 속도를 넘겨주었지만, 실제 수명을 사거리와 속도에 맞춰 계산하지 않았다.
그러다 보니 Projectile이 의도한 사거리보다 더 멀리 날아가는 문제가 있었다.
예를 들어 속도가 빠른 Projectile인데 수명이 고정되어 있으면, 설정한 사거리보다 훨씬 먼 곳까지 날아갈 수 있다.
수명 고정
→ ProjectileSpeed가 빠름
→ 의도한 Range보다 멀리 이동
그래서 Projectile을 초기화할 때 Range와 Speed를 넘겨주고, 내부에서 수명을 다음과 같이 계산하도록 했다.
LifeSpan = Range / Speed
이렇게 하면 Projectile 속도가 달라져도, 설정한 사거리 기준으로 사라지게 만들 수 있다.
재장전 구조
재장전은 단순히 버튼을 누르는 순간 탄약이 채워지는 방식으로 만들고 싶지 않았다.
실제 게임에서는 탄창을 끼우는 애니메이션 중간 지점에서 탄약이 보충되는 것이 더 자연스럽기 때문이다.
그래서 재장전을 세 단계로 나누었다.
TryStartReload
- 재장전 시작 가능 여부 확인
- bReloading을 true로 변경
ApplyReload
- AnimNotify에서 호출
- 실제 탄약 보충
- 재장전 사운드 재생
FinishReload
- 재장전 몽타주 종료 시 호출
- bReloading을 false로 변경
- CombatComponent의 Reloading 상태 해제

사진처럼 재장전 몽타주 중 실제로 탄약이 채워져야 하는 타이밍에 AnimNotify를 배치했고,
해당 시점에 ApplyReload를 호출하도록 했다.
전체 흐름은 다음과 같다.
재장전 입력
→ CombatComponent가 Reloading 상태로 전환
→ RangedCombatComponent::TryReload 호출
→ GunBase::TryStartReload 호출
→ 재장전 몽타주 재생
→ AnimNotify에서 ApplyReload 호출
→ 몽타주 종료 시 FinishReload 호출
→ OnReloadFinished Broadcast
→ CombatComponent가 Reloading 상태 해제
이렇게 하니 재장전 애니메이션과 실제 탄약 보충 타이밍을 분리할 수 있었다.
재장전 몽타주 종료 처리
재장전은 시간이 걸리는 행동이다.
따라서 재장전 몽타주가 끝났을 때 Reloading 상태를 해제해야 한다.
이를 위해 근접 공격에서 사용했던 것처럼 몽타주 종료 델리게이트를 사용했다.
PlayReloadAnimation
→ Montage_SetEndDelegate
→ HandleReloadMontageEnded
→ FinishReload
재장전 중인 총과 재장전 몽타주도 따로 저장했다.
CurrentReloadingGun
CurrentReloadingMontage
이 값을 저장한 이유는 현재 종료된 몽타주가 정말 재장전 몽타주인지 확인하기 위해서이다.
다른 몽타주 종료 이벤트가 들어왔을 때 재장전 종료로 잘못 처리하면 안 되기 때문이다.
종료된 Montage 확인
→ CurrentReloadingMontage와 같은지 검사
→ 맞다면 FinishReload 호출
이렇게 해서 재장전 종료 타이밍을 안정적으로 처리할 수 있었다.
실행 예시

정리
이번에는 원거리 공격과 재장전을 담당하는 RangedCombatComponent를 구현했다.
구조를 정리하면 다음과 같다.
GunBase
- 총기 데이터와 탄약 상태 관리
- 사격 / 재장전 가능 여부 판단
- ProjectileClass, Range, ProjectileSpeed 제공
RangedCombatComponent
- 현재 GunBase를 받아 사격 실행
- Projectile 생성 및 초기화
- 카메라 기준 AimPoint 계산
- 총구에서 AimPoint 방향으로 발사
- 사격 사운드 / 애니메이션 실행
- 재장전 몽타주 재생
- 재장전 종료 이벤트 발생
GunFireProjectile
- 데미지 / 가해자 정보 저장
- 충돌 시 ApplyDamage 호출
- Range / Speed 기준으로 수명 설정
CombatComponent
- 현재 전투 상태 판단
- 공격력 계산
- 사격 / 재장전 실행 요청
마무리
이번에 RangedCombatComponent를 구현하면서 3인칭 사격을 어떤 방식으로 처리해야 하는지 조금 더 알게 되었다.
근접 공격은 콤보, 입력 타이밍, 공격 판정 구간 같은 부분이 복잡했다면, 원거리 공격은 상대적으로 구조가 단순해 보였다.
하지만 실제로 구현해보니 3인칭 사격에서는 탄착점과 크로스헤어를 어떻게 맞출지가 가장 큰 고민이었다.
특히 이번 프로젝트의 캐릭터는 왼손을 뻗어서 총을 발사하는 구조라서, 총구 위치가 화면 중앙에서 꽤 떨어져 있었다.
그래서 단순히 총구의 Forward 방향으로 발사하면 크로스헤어가 가리키는 위치와 실제 총알 방향이 맞지 않았다.
이를 해결하기 위해 카메라를 기준으로 먼저 조준점을 계산하고, 총구에서는 그 조준점을 향해 발사체를 날리는 방식으로 구현했다.
카메라 기준으로 AimPoint 계산
→ 총구 위치에서 AimPoint 방향 계산
→ 해당 방향으로 Projectile 발사
처음에는 이 방식으로 구현했음에도 총알이 크로스헤어와 어긋나는 문제가 있었다.
원인을 확인해보니 조준점을 너무 가까운 거리로 잡고 있었기 때문이었다.
카메라 위치와 총구 위치가 서로 다르기 때문에, 조준점이 가까우면 그 위치 차이가 크게 드러난다.
그래서 조준점을 충분히 먼 거리로 잡도록 수정했고, 이후에는 크로스헤어와 실제 탄착 방향이 거의 맞게 되었다.
또 Projectile의 수명 처리도 한 번 문제가 있었다.
처음에는 총알에 사거리와 속도를 넘겨주었지만,
수명을 사거리와 속도에 맞춰 계산하지 않아서 총알이 의도한 사거리보다 더 멀리 날아가는 문제가 있었다.
그래서 이후에는 다음처럼 수명을 계산하도록 변경했다.
LifeSpan = Range / Speed
이렇게 하니 Projectile 속도가 달라져도 사거리 기준으로 총알이 사라지게 만들 수 있었다.
이번 작업을 통해 원거리 공격은 단순히 발사체를 생성하는 것만으로 끝나는 것이 아니라,
카메라 기준 조준점, 총구 위치, 발사 방향, Projectile 속도와 수명까지 함께 맞춰야 한다는 것을 알게 되었다.
근접 공격에 비해 콤보나 판정 타이밍은 적었지만,
3인칭 사격에서는 “크로스헤어가 가리키는 곳에 총알이 맞는 느낌”을 만드는 것이 중요했다.
결과적으로 카메라 기준 AimPoint를 구하고,
총구에서 그 방향으로 Projectile을 발사하는 구조로 정리하면서 원하는 형태에 가깝게 구현할 수 있었다.
다음 글에서는 전투 UI와 연출을 연결한 내용을 정리해보려고 한다.
'언리얼 엔진 > 프로젝트' 카테고리의 다른 글
| [언리얼 엔진] 팀 프로젝트 기록 10 - AnimNotifyState로 전투 모션 타이밍 제어하기 (0) | 2026.05.27 |
|---|---|
| [언리얼 엔진] 팀 프로젝트 기록 09 - 전투 UI와 데미지 팝업 구현하기 (0) | 2026.05.22 |
| [언리얼 엔진] 팀 프로젝트 기록 07 - MeleeCombatComponent로 근접 공격과 콤보 구현하기 (0) | 2026.05.20 |
| [언리얼 엔진] 팀 프로젝트 기록 06 - CombatComponent로 전투 행동 상태 관리하기 (0) | 2026.05.19 |
| [언리얼 엔진] 팀 프로젝트 기록 05 - 무기 장착을 위한 WeaponComponent 구현 (0) | 2026.05.18 |