← DEVLOG
인터랙티브2026.04.266 min read

Galaxy Wing — 무기 시스템 + 모듈 분할 회고

단일 무기 5단계에서 5×7 무기 + 5종 보조무기로 확장하고, 1035 라인 logic.ts 를 8개 모듈로 쪼갠 이야기

canvasgameweapon-systemsubweaponrefactoringmodularization

시작 — 무기가 5 way 가 한계였습니다

기존 Galaxy Wing 은 단일 무기에 5단계 power up 만 있었습니다. 최대 5 way fan 까지 펴지는 게 끝이었고, 사용자가 발견할 수 있는 진행도 한정적이었습니다. 7단계 더 깊은 진화 + 무기 종류 변경 + 보조 화기 추가를 한꺼번에 다루기로 했습니다.


5 type × 7 level — 무기 분기 + 깊이

spread / laser / wave / plasma / charge 다섯 가지 type 을 정의하고, 각각 7 단계 진화 데이터를 한 객체에 모았습니다.

export const WEAPON_DEFS: Record<WeaponType, readonly WeaponLevelDef[]> = {
  spread: [
    /* 7 levels */
  ],
  laser: [
    /* 7 levels */
  ],
  wave: [
    /* 7 levels */
  ],
  plasma: [
    /* 7 levels */
  ],
  charge: [
    /* 7 levels */
  ],
};

각 level 은 angles / offsets / color / radius / damage / speed / fireInterval 외에 type별 special 필드(piercing / waveAmplitude / plasmaSplash / chargeTime)를 옵션으로 가집니다. 데이터-주도 정의로 두니 firePlayer 함수 자체는 단순한 spawn 루프 한 개로 정리됐습니다.

드롭은 'P' (같은 type 레벨업) / 'W' (다음 type cycle + 레벨1 시작) 두 종류로 분리했습니다.


Charge — Hold-Release 메커니즘

charge type 은 Z 키 hold 시 차징, release 시 발사하는 별도 입력 흐름을 가집니다.

function handleChargeWeapon(state, input) {
  if (input.fire) {
    if (p.chargeStartFrame === -1 && state.frame >= p.nextFireFrame) {
      p.chargeStartFrame = state.frame;
    }
  } else if (p.chargeStartFrame !== -1) {
    const fullyCharged = state.frame - p.chargeStartFrame >= def.chargeTime;
    if (fullyCharged) spawnPlayerBullets(state, def, p.x, p.y, 1.5);
    else {
      /* 약한 1발 */
    }
    p.chargeStartFrame = -1;
  }
}

EMP stun 시 차징 상태도 같이 reset 하는 게 까다로운 부분이었습니다. 시각 피드백은 player 주변 보라색 ring 으로 제공해서, 풀 충전 시 펄스 효과로 알려줍니다.


Special Bullet — 풀 재사용 시 stale 필드 함정

Bullet 풀은 단일 인터페이스를 모든 종류가 공유합니다. spread / laser / wave / plasma / missile / mine 모두 같은 Bullet 객체를 풀에서 acquire 합니다. 문제는 풀 재사용 — 이전 발사가 plasma 였다면 plasmaSplash 가 양수로 남아있고, 다음 spread 발사 시 그대로 splash 폭발이 일어나는 버그가 발생합니다.

해결: spawnPlayerBullets 가 모든 special 필드를 명시적으로 reset.

b.piercing = def.piercing ?? false;
b.waveAmplitude = def.waveAmplitude; // undefined 면 undefined
b.plasmaSplash = def.plasmaSplash;
b.lastHitIdx = undefined;
b.homingTurnRate = undefined;
b.mineExplosionRadius = undefined;
b.mineExplodeFrame = undefined;

undefined 명시 할당은 풀 재사용 stale 방지의 핵심입니다.


5종 보조 화기 — 단일 슬롯 + cycle

메인 무기와 별개로 단일 슬롯 보조 화기를 추가했습니다.

종류효과Cooldown
missile호밍 + plasma splash1.5s
empBurst화면 적탄 클리어 + 적 60f stun12s
lightning즉시 chain 1회4s
drone10초 동안 좌우 추가 발사12s
spreadMine적 근접/lifetime 후 8방향 폭발6s

'S' 드롭이 cycle 전환 (없으면 첫 type, 있으면 다음). C 키 발사. 모바일은 폭탄 버튼 옆에 SUB 버튼을 추가하고 cooldown 시각은 conic-gradient 마스크로 처리했습니다.

.subBtnCooldown {
  background: conic-gradient(rgba(0, 0, 0, 0.55) calc(var(--sub-cd) * 360deg), transparent 0);
}

--sub-cd 가 0~1 으로 변하면 시계방향 마스크가 펼쳐집니다.


logic.ts 1035 → 8 모듈 분할

이 모든 기능을 추가하는 동안 galaxyWingLogic.ts 가 1035 라인까지 부풀었습니다. 입력 / 플레이어 / charge / 보조무기 / wave dispatch / spawn 패턴 / 적 AI / 탄알 / 충돌 / 파워업 / 콤보가 한 파일에 다 있었습니다. 의존 그래프를 그려서 단방향이 되도록 분할했습니다.

score (root)
  ↓
explode → subweaponLogic, player → collision, update → logic
              ↘ spawn (독립)
              ↘ types (subweapons type only)

damagePlayer 는 collision 에서만 호출되어 player 모듈에 두는 게 자연스럽고, findNearestActiveEnemy 는 lightning 과 missile 호밍 양쪽이 쓰니 subweaponLogic 에서 export 해서 update 가 import 합니다.

분할 후 각 파일이 100~250 라인 내외로 떨어졌고, 함수 45개의 1:1 매핑이 보존됐습니다. 검증은 함수 inventory 비교 + tsc --noEmit 으로 통과 확인했습니다.

renderer 도 비슷하게 816 → 98 라인으로 정리하고, drawPlayer / drawEnemies / drawBullets / drawHUD / drawPowerUps / drawParticles 6 파일로 분리했습니다.


마무리

5×7 무기 + 5종 보조무기 + 16 모듈 분할까지 한 사이클로 마무리했습니다. 데이터-주도 무기 정의 덕분에 WEAPON_DEFS 에 한 행만 추가하면 새 무기 type 이 동작합니다. 보조무기도 SUBWEAPON_DEFS + 분기 한 줄이면 됩니다.

가장 큰 교훈은 "기능이 늘어나는 시점에 분할 시기를 놓치지 않기" 였습니다. 1035 라인까지 끌고 가지 말고 500 라인 부근에서 한 번 쪼갰다면 더 깔끔했을 것 같습니다.

이 포스트와 연결된 콘텐츠

직접 체험하기

방명록

이 글에 대한 한 줄을 남겨주세요

0 / 140

불러오는 중...