Galaxy Wing — 무기 시스템 + 모듈 분할 회고
단일 무기 5단계에서 5×7 무기 + 5종 보조무기로 확장하고, 1035 라인 logic.ts 를 8개 모듈로 쪼갠 이야기
시작 — 무기가 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 splash | 1.5s |
| empBurst | 화면 적탄 클리어 + 적 60f stun | 12s |
| lightning | 즉시 chain 1회 | 4s |
| drone | 10초 동안 좌우 추가 발사 | 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 라인 부근에서 한 번 쪼갰다면 더 깔끔했을 것 같습니다.
방명록
이 글에 대한 한 줄을 남겨주세요
불러오는 중...