← 블로그 목록 2026-05-19

Quake식 무기 픽업: 인벤토리 없는 단순함

이전 버전 Dogkov는 Tarkov에서 영감을 받아 36칸 그리드 인벤토리, 슬롯 장착(헬멧/방탄복/탄약/조준경/소음기 등), 우클릭 컨텍스트 메뉴까지 다 만들었습니다. 다 합쳐서 ~600줄. 그런데 5분짜리 데스매치엔 너무 무거웠습니다. Q3식 자동 픽업으로 단순화한 결과를 정리합니다.

이전: Tarkov 식

몰입감은 좋지만, 한 매치 동안 인벤토리 만지는 시간이 사격하는 시간보다 길었습니다.

이후: Q3식

완전히 갈아엎고 두 개의 dict만 남겼습니다:

player = {
  hasWeapon: { gauntlet:true, machinegun:true, shotgun:false, ... },
  weapons:   { gauntlet:0,    machinegun:100,  shotgun:0,     ... },
  currentWeapon: 'machinegun',
  ...
}

hasWeapon은 "들고 있나", weapons는 탄약. 픽업 닿으면 둘 다 갱신. 끝.

무기 정의

const WEAPON_DEFS = {
  gauntlet: {
    kind: 'melee', damage: 50, rateMs: 400, range: 36,
    maxAmmo: Infinity, alwaysOwned: true, ...
  },
  rocket: {
    kind: 'projectile', damage: 100, splashDamage: 80, splashRadius: 110,
    rateMs: 800, projSpeed: 900, maxAmmo: 50, pickupAmmo: 5, ...
  },
  ...
};

alwaysOwned:true면 스폰 시 자동 보유 (gauntlet, MG). 나머지는 픽업해야 들 수 있습니다.

픽업 처리

_tryGrabPickup(p, pk, now) {
  const def = pk.def;
  if (def.type === 'weapon') {
    const wd = WEAPON_DEFS[def.sub];
    const before = p.weapons[def.sub];
    p.hasWeapon[def.sub] = true;
    p.weapons[def.sub] = Math.min(wd.maxAmmo, p.weapons[def.sub] + wd.pickupAmmo);
    // 처음 줍는 강한 무기는 자동 전환
    if (!before && p.currentWeapon === 'machinegun' && def.sub !== 'machinegun') {
      p.currentWeapon = def.sub;
    }
    return true;
  }
  if (def.type === 'health') {
    const cap = def.sub >= 100 ? 200 : 100;
    if (p.hp >= cap) return false;
    p.hp = Math.min(cap, p.hp + def.sub);
    return true;
  }
  // ammo / armor / powerup 동일 패턴
}

HUD 동적 슬롯

이전엔 9개 슬롯 고정 표시였는데, 보유 안 한 무기는 회색이라 시각적으로 시끄러웠습니다. 보유한 것만 표시하도록 변경:

WEAPONS.forEach((k, i) => {
  if (!me.hasWeapon[k]) return;          // 안 들고 있으면 건너뜀
  const wd = WEAPON_DEFS[k];
  ws.appendChild(createWeaponBox(k, wd, me, i));
});

마우스 휠 / Q · E 순환도 같은 필터를 거칩니다. 결과적으로 막 시작했을 땐 슬롯 2개 (gauntlet + MG), 픽업할수록 점점 늘어남.

탄약 표시: Infinity 처리

gauntlet은 무한 사용입니다 (maxAmmo: Infinity). 그러나 JSON.stringify로 wire 전송하면 null이 됩니다. 클라가 isFinite로 체크해서 "∞" 표시:

const wd = WEAPON_DEFS[me.cw];
$('hud-ammo').textContent = isFinite(wd.maxAmmo) ? me.weapons[me.cw] : '∞';

플레이어의 weapons[k] 값이 아니라 무기 정의의 maxAmmo로 판정하는 게 핵심. JSON 전송 시 null로 변환된 값을 기준으로 하면 잘못된 결과 나옴.

잃은 것 / 얻은 것

인벤토리 UI 코드: 600줄 → 0줄. 픽업 코드: 200줄 → 60줄. 신규 플레이어 이해 시간: 5분 → 30초. 다만 무기 부착물·헬멧 같은 깊이는 포기. 단순함이 데스매치 컨셉에 잘 맞습니다.

← FOV 시야 안개 이펙트 시스템 →