← 블로그 목록 2026-05-19

HTML5 Canvas 2D 탑다운 엔진의 구조

Dogkov Arena는 Three.js·Phaser 같은 프레임워크 없이 Canvas 2D API + 약간의 모듈 분리로 돌아갑니다. 한 번 라운드를 5분 이상 끌고 가는 게임이 아니라, 빠르게 띄워서 한 판 하고 닫는 게임이라 로딩과 코드 크기에 민감합니다. 최종 번들이 ~40KB로 빠진 렌더 파이프라인의 구조를 정리합니다.

모듈 분리

이전 버전은 단일 game.js 파일 2,973줄에 모든 것이 있었습니다. 리부트하면서 다음과 같이 분리했습니다:

src/
├── shared/    constants.js · weapons.js · map.js · geom.js
├── host/      sim.js · bots.js · room.js
└── client/    main.js · net.js · input.js · render.js · effects.js · ui.js · audio.js

CommonJS(require/module.exports)로 통일해서 Node에서도, 브라우저에서도 그대로 동작합니다. 클라이언트는 esbuild로 한 파일로 번들. 빌드 시간 2ms, 결과물 40KB 미만.

월드 → 카메라 → 화면 변환

탑다운 게임에선 카메라가 항상 플레이어를 중심으로 따라옵니다. 매 프레임 캔버스에 다음 변환을 적용:

ctx.translate(W/2, H/2);          // 화면 중앙으로
ctx.scale(zoom, zoom);             // 줌
ctx.translate(-cam.x, -cam.y);     // 카메라 위치만큼 역방향 이동

drawGrid(ctx);
drawPickups(ctx);
drawWalls(ctx);
drawProjectiles(ctx);
drawPlayers(ctx);
drawEffects(ctx);

이후 모든 draw 호출은 월드 좌표로 인자를 주면 알아서 화면 좌표로 변환됩니다. setTransform 한 번 리셋하고 fog overlay만 따로 처리합니다.

레이어 순서

드로잉 순서가 곧 z-order:

  1. 배경 그리드
  2. 픽업 (벽보다 아래)
  3. 로켓 트레일 (벽 위, 발사체 아래)
  4. 발사체 (로켓·플라즈마·BFG·유탄)
  5. 플레이어
  6. 라이트닝 빔 (플레이어보다 위 — 손에서 뻗는 느낌)
  7. 이펙트 (폭발·스파크·슬래시)
  8. FOV 안개 (모든 것 위에 알파 마스크)

트레일이 발사체 아래로 가야 자연스럽고, 이펙트가 마지막에 와야 폭발이 캐릭터를 가립니다.

FOV 안개 마스킹

플레이어 전방 90° 시야만 보이게 하는 안개는 별도 캔버스에 그립니다:

// 별도 fog canvas에 검은색 채우기
fogCtx.fillStyle = 'rgba(0,0,0,0.92)';
fogCtx.fillRect(0, 0, W, H);

// 같은 월드 변환 적용
fogCtx.translate(W/2, H/2); fogCtx.scale(zoom, zoom);
fogCtx.translate(-cam.x, -cam.y);

// destination-out으로 보이는 영역을 "지움"
fogCtx.globalCompositeOperation = 'destination-out';
drawFOVCone(fogCtx, me);   // 폴리곤 fill

// 메인 캔버스에 fog 덮기
ctx.drawImage(fogCanvas, 0, 0);

FOV cone은 120개 ray를 쏴서 벽 충돌까지의 polygon으로 그립니다. FOV 시야 안개 글에서 자세히.

리사이즈 & DPR

고해상도 디스플레이를 위해 devicePixelRatio를 캔버스의 backing-store 사이즈에 곱합니다. 좌표 계산엔 clientWidth/Height(CSS px) 사용. 둘을 헷갈리면 마우스 좌표가 미세하게 어긋나서 디버그 헬에 빠집니다.

마치며

Canvas 2D는 70년대 API의 직계 후손답게 단순합니다. 프레임워크 없이도 한 사람이 일주일 작업하면 멀티플레이 슈터가 나옵니다. WebGL이 필요해지는 순간은 동시에 그리는 객체가 수천 개를 넘기 시작할 때입니다 — 데스매치 8인 매치면 한참 멀었습니다.

Socket.io LAN 리슨 서버 →