개발 노트 — 무기 시스템 설계
데미지 숫자만 다르면 재미없다
슈팅 게임에서 무기 종류를 늘릴 때 가장 손쉬운 방법은 데미지, 연사 속도 같은 숫자만 바꾸는 것입니다. 하지만 이렇게 하면 결국 “숫자가 더 높은 무기가 항상 좋은 무기”가 되어버려서, 여러 무기가 있는 의미가 퇴색됩니다.
개인 프로젝트 Enter the Room에서는 무기 4종을 데미지 차이가 아니라 발사 패턴의 차이로 구분했습니다.
| 무기 | 발사 방식 | 특징 |
|---|---|---|
| 권총 | 단발 격발 | 기본 무기, 정확한 조준 |
| 기관단총 | 70발 연사 | 높은 화력, 낮은 정확도 |
| 소총 | 3발 점사 | 화력과 정확도의 중간 |
| 샷건 | 5발 부채꼴 동시 발사 | 근거리 광역, 원거리 비효율 |
이 글에서는 이 네 가지 발사 패턴을 어떻게 하나의 무기 시스템 구조 안에서 구현했는지 정리합니다.
공통 인터페이스로 무기 묶기
무기마다 완전히 다른 클래스를 만들기보다, 공통 인터페이스를 정의하고 “발사 시 어떻게 탄환을 내보내는지”만 다르게 구현하는 방식이 확장에 유리합니다.
public interface IWeapon
{
void Fire(Vector2 origin, Vector2 aimDirection);
float FireRate { get; }
}
Fire() 메서드 하나로 통일해두면, 플레이어 컨트롤러 쪽 코드는 무기 종류를 몰라도 됩니다.
public class PlayerShooting : MonoBehaviour
{
private IWeapon _currentWeapon;
private float _cooldown;
private void Update()
{
if (Input.GetMouseButton(0) && _cooldown <= 0f)
{
_currentWeapon.Fire(transform.position, GetAimDirection());
_cooldown = _currentWeapon.FireRate;
}
_cooldown -= Time.deltaTime;
}
}
무기별 구현: 발사 패턴이 곧 정체성
권총: 단발 조준
public class Pistol : IWeapon
{
public float FireRate => 0.3f;
public void Fire(Vector2 origin, Vector2 aimDirection)
{
SpawnProjectile(origin, aimDirection);
}
}
가장 단순한 형태입니다. 조준 방향으로 탄환 하나를 발사합니다. 낮은 화력이지만 방향 제어가 정확해서, 원거리에서 정밀하게 노려야 하는 상황에 유리합니다.
기관단총: 연사
public class SMG : IWeapon
{
public float FireRate => 0.05f;
private float _spreadAngle = 8f;
public void Fire(Vector2 origin, Vector2 aimDirection)
{
// 매 발마다 약간의 랜덤 편차를 줘서 "연사의 부정확함"을 표현
float randomOffset = Random.Range(-_spreadAngle, _spreadAngle);
Vector2 spreadDirection = Rotate(aimDirection, randomOffset);
SpawnProjectile(origin, spreadDirection);
}
}
FireRate를 매우 짧게 잡아 빠른 연사를 구현하고, 대신 매 발에 약간의 탄퍼짐(spread)을 줘서 “화력은 높지만 정확도는 낮다”는 특징을 살렸습니다. 발사 속도만 빠르고 탄퍼짐이 없다면 사실상 강화된 권총과 다를 게 없어지므로, 이 탄퍼짐이 무기 정체성의 핵심입니다.
소총: 점사
public class Rifle : IWeapon
{
public float FireRate => 0.5f;
private int _burstCount = 3;
private float _burstInterval = 0.08f;
private MonoBehaviour _coroutineRunner;
public void Fire(Vector2 origin, Vector2 aimDirection)
{
_coroutineRunner.StartCoroutine(BurstFire(origin, aimDirection));
}
private IEnumerator BurstFire(Vector2 origin, Vector2 aimDirection)
{
for (int i = 0; i < _burstCount; i++)
{
SpawnProjectile(origin, aimDirection);
yield return new WaitForSeconds(_burstInterval);
}
}
}
한 번의 격발 입력으로 짧은 간격의 3연발이 나가도록 구성했습니다. 권총(단발 정확)과 기관단총(연사 부정확) 사이의 중간 지점 역할을 맡깁니다.
샷건: 부채꼴 동시 발사
public class Shotgun : IWeapon
{
public float FireRate => 0.8f;
private int _pelletCount = 5;
private float _spreadAngle = 30f;
public void Fire(Vector2 origin, Vector2 aimDirection)
{
float angleStep = _spreadAngle / (_pelletCount - 1);
float startAngle = -_spreadAngle / 2f;
for (int i = 0; i < _pelletCount; i++)
{
float angle = startAngle + angleStep * i;
Vector2 pelletDirection = Rotate(aimDirection, angle);
SpawnProjectile(origin, pelletDirection);
}
}
}
한 번의 격발로 5개의 탄환이 부채꼴 각도(_spreadAngle)에 균등하게 분산되어 동시에 나갑니다. 근거리에서는 대부분의 탄환이 명중해서 순간 화력이 매우 높지만, 원거리에서는 탄환이 퍼져서 명중률이 급격히 떨어지는 자연스러운 거리 페널티가 생깁니다. 발사 속도(FireRate)를 가장 느리게 잡아, 연사가 아니라 “한 방의 판단”이 중요한 무기로 자리잡게 했습니다.
설계 원칙: 숫자가 아니라 “언제 유리한지”로 구분하기
네 무기를 정리하면, 각자 명확한 사용 상황이 갈립니다.
- 권총: 원거리 정밀 사격이 필요할 때
- 기관단총: 물량으로 밀어붙여야 할 때 (근거리 다수의 적)
- 소총: 정확도와 화력 사이 타협이 필요할 때
- 샷건: 근거리에서 확실한 한 방이 필요할 때
이렇게 설계하면 “이 무기가 무조건 좋다”는 서열이 생기지 않고, 상황에 따라 무기를 바꿔 쓰는 재미가 생깁니다. 데미지 수치를 조정하는 것보다, 발사 패턴(탄퍼짐, 발사 수, 연사 여부)으로 차별화하는 게 장기적으로 더 풍부한 무기 다양성을 만듭니다.
정리
무기 시스템을 설계하실 때, 데미지나 연사 속도 같은 스칼라 값 조정에 앞서 이렇게 자문해보시길 추천합니다.
“이 무기는 다른 무기로 대체할 수 없는 자신만의 상황을 갖고 있는가?”
답이 “그렇다”라면, 그 차이가 발사 패턴이든 탄퍼짐이든 사거리든, 코드로 구현할 가치가 있는 차별점입니다.