
게임을 개발하다 보면 무기를 구현하는 과정에서 반복되는 로직과 복잡한 의존성이 큰 골칫거리가 되곤 합니다. 특히 총기, 근접 무기, 특수 무기처럼 형태는 달라도 공통적으로 "공격한다"는 기능을 가지기 때문에, 이를 어떻게 유연하고 확장 가능하게 설계할지가 핵심 과제입니다.
이번 글에서는 Combat Component를 중심으로 무기 시스템을 설계하고, 이를 조합하여 다양한 무기를 구현하는 방법을 소개하려 합니다. 각 무기가 고유의 동작을 가질 수 있도록 컴포넌트를 조합하고 데이터를 수정하여 새로운 무기를 만들어낼 수 있는 구조를 목표로 합니다.
무기 시스템을 설계할 때 가장 먼저 고민해야 하는 것은 발사 방식입니다. 크게 히트스캔(라인 트레이스 기반 판정) 과 투사체(액터 스폰 기반 궤적) 로 나눌 수 있습니다.
히트스캔: 즉시 판정, 대신 물리적 상호작용 표현이 제한적.
투사체: 곡사, 중력, 폭발, 충돌 규칙 같은 리치한 표현 가능.
여기에 또 다른 차원의 요구사항이 있습니다. 샷건, 멀티샷처럼 "한 번의 트리거로 여러 발이 발사되는 기능"입니다. 중요한 점은 이 기능이 히트스캔/투사체와 무관하게 공통으로 존재한다는 사실입니다.
따라서 “발사 방식(히트스캔/투사체)” 과 “발사 패턴(단발/연발/산탄)” 을 분리하면 설계와 관리가 훨씬 깔끔해집니다. 즉, 샷건이든, 버스트든, 투사체 로켓런처든, 다발 발사 로직은 하나의 공통 단계(Emission) 로 처리해야 유지보수와 확장이 쉬워집니다.
핵심은 발사를 Trigger → Emission → Ballistics → HitProcess → Effects 의 파이프라인으로 쪼개고, 각 단계를 데이터 기반으로 통합 관리하는 것입니다.
Project Escape의 무기 시스템은 “무기가 직접 공격 로직을 갖지 않고, 공격 전담 컴포넌트(AttackComponent)를 생성해서 실행” 하는 구조를 채택했습니다.
이 방식은 무기가 생성될 때 가상 팩토리(virtual factory) 를 통해 공격 방식을 결정하고, 발사 시에는 공통 AttackStats 를 AttackComponent에 전달하여 히트스캔 또는 투사체 로직을 실행하도록 합니다.
UPEAttackBaseComponent
모든 공격 컴포넌트의 공통 베이스.
입력 구조체: FPEAttackStats (데미지, 사거리, 퍼짐 각도, 채널, 투사체 클래스/속도 등).
주요 흐름:
ExcuteAttack → 공격 시작 위치·방향 계산ApplyAccuracyDeviation → 퍼짐 적용PerformAttack → 구체적 탄도/판정 실행파티클/사운드 재생 유틸 제공.
UPEAttackHitscanComponent
LineTraceSingleByChannel 로 즉시 판정.UPEAttackProjectileComponent
APEWeaponBase
PostInitializeComponents 시점에 CreateAttackComponent() 호출 → AttackComponent 생성 후 RegisterComponent().
CreateAttackComponent 는 가상 함수 → 파생 무기에서 오버라이드해야 올바른 공격 방식이 동작.
DoPrimaryAction(발사) 흐름:
BulletsPerShot 만큼 AttackComponent→ExcuteAttack 호출Interact (집기)
IPEAttackable 인터페이스에서 AttackStartPoint를 받아 AttackComponent에 전달.DoPrimaryAction (발사 입력)
AttackComponent->ExcuteAttack 호출.UPEAttackBaseComponent::ExcuteAttack
PerformAttack 호출.구현체별 PerformAttack
피드백 & 상태 갱신
World->LineTraceSingleByChannel(Start, End, AttackStats.HitscanChannel)HitResult.GetComponent()->IsA(UPEReceiveAttackComponent)SpawnActor<APEProjectileBase>(ProjectileClass, StartLocation, …)SpawnParams.Owner = GetOwner(), SpawnParams.Instigator = PawnOwner


