Lava Leap: AI 페어 프로그래머와 함께 만든 끝없는 클라이머 게임 출시
요약
Claude를 페어 프로그래머로 활용하여 제작한 절차적 생성 방식의 수직 클라이머 게임 'Lava Leap'를 소개합니다. Phaser 3와 TypeScript를 기반으로 하며, AI와의 협업을 통해 설계부터 구현까지의 개발 프로세스를 공유합니다.
핵심 포인트
- Claude를 활용한 테스트 주도 개발(TDD) 및 마일스톤 기반 협업
- 절차적 생성을 통한 무한한 레벨 구성 및 파라메트릭 도달 가능성 확보
- Phaser 3, TypeScript, Vite를 활용한 기술 스택 구성
- Vitest와 Playwright를 이용한 단위 및 E2E 테스트 적용
Lava Leap는 끝없이 위로 올라가는 수직 클라이머 게임입니다. 아래에서 용암이 차오르는 동안 절차적으로 생성된 (procedurally generated) 플랫폼을 타고 올라가며, 점수는 당신의 높이와 올라가는 길에 획득한 코인의 합계로 결정됩니다. 저는 Claude를 페어 프로그래머 (pair programmer)로 활용하여 두 번의 릴리스 사이클에 걸쳐 이 게임을 제작했으며, 현재 v0.2.0 버전으로 GitHub에 공개되어 있습니다.
게임 소개
핵심 루프 (core loop)는 설명하기는 간단하지만 한 번 시작하면 멈추기 어렵습니다. 당신은 끝없이 이어지는 플랫폼 타워를 향해 달리고, 점프하고, 이단 점프하고, 벽 타기 (wall slide), 벽 점프 (wall jump), 그리고 공중 대시 (air dash)를 하며 올라갑니다. 어떤 플랫폼은 발밑에서 무너지고, 어떤 것은 움직이며, 아래의 용암은 생존 시간이 길어질수록 가속됩니다. 죽으면 점수를 확인하고, Space 키를 눌러 다시 시작하세요. 최고 기록은 localStorage에 저장됩니다.
이 루프를 중심으로, 버전 0.2.0에서는 작은 게임을 완성된 게임처럼 느껴지게 만드는 요소들을 추가했습니다. 고유한 팔레트와 용암 속도를 가진 이름 있는 구역들 (Volcanic Throat의 하위 구역은 약 1000 높이 단위마다 바뀝니다 — 0 지점의 Magma Vault, 1000 지점의 The Forge, 2000 지점의 Ashfall, 3000 지점의 Obsidian Crown — 각 구역은 용암 속도를 높이고 더 어려운 플랫폼 유형이 나타나도록 편향됩니다), 무작위 스트림에 삽입되는 수동 제작 세트피스 청크 (set-piece chunks), 업적 (achievements), 데일리 챌린지 시드 (daily challenge seed), 모은 코인을 사용하는 코스튬 상점, 절차적 칩튠 음악 (procedural chiptune music), 8가지 합성 효과음, 일시정지 및 설정 메뉴, 그리고 처리되지 않은 오류로 인해 빈 화면에 갇히지 않도록 하는 크래시 복구 오버레이 (crash-recovery overlay) 등이 포함되었습니다.
기술 스택은 Phaser 3, TypeScript, Vite이며, 단위 테스트 (unit tests)를 위한 Vitest와 엔드 투 엔드 스모크 테스트 (end-to-end smoke tests)를 위한 Playwright를 사용했습니다. 픽셀 아트 캐릭터와 타일은 PixelLab으로 생성되었습니다. 저장소를 클론(clone)하고 npm run dev를 실행하여 직접 확인해 보세요. 운동감 (momentum)이 중요한 게임은 스크린샷보다 직접 경험하는 것이 훨씬 좋습니다.
제작 과정
분업 방식은 일관적이었습니다. 제가 게임의 방향을 결정하면, Claude가 거의 모든 코드를 작성했습니다. 우리는 디자인 사양(design spec)을 도출하는 브레인스토밍 세션으로 시작하여, v1을 8개의 마일스톤(milestones)에 걸친 28개의 테스트 주도(test-driven) 작업으로 나눈 계획을 세웠습니다. Claude는 별도의 리뷰어 패스(reviewer passes)를 거쳐 각 마일스톤을 구현했고, 저는 각 단계를 승인하기 전에 빌드를 직접 플레이했습니다. v1은 31개의 커밋(commits)으로 출시되었으며, v2는 9개의 마일스톤을 추가하여 동일한 과정을 반복했습니다.
제가 가장 중요하게 생각하는 아키텍처 결정은 **파라메트릭 도달 가능성 (parametric reachability)**입니다. 생성된 모든 레벨은 증명 가능한 방식으로 등반이 가능합니다. 생성기는 플랫폼을 무작위로 배치하고 운에 맡기지 않습니다. 생성기는 물리 값과 함께 하나의 튜닝 파일에 저장된 도달 예산(reach-budget) 상수를 사용하여, 플레이어의 실제 이동 범위(movement envelope) — 점프 높이, 이단 점프 확장 범위, 대시 거리 — 내에 다음 플랫폼을 제한(clamp)합니다. 난이도 조절(difficulty scaling)에 따라 위로 올라갈수록 용암의 속도가 빨라지고 무너지는 플랫폼이나 움직이는 플랫폼의 비중이 높아지지만, 이동 시스템이 건널 수 없는 간격을 절대 만들지 않습니다. 생성기 자체에는 이를 보장하는 12개의 유닛 테스트(unit tests)가 있습니다. v2에서 수동으로 제작된 세트피스(set-piece) 청크를 추가했을 때, 이들은 도달 너비를 초과하는 템플릿을 거부하는 validateChunk 게이트를 통과해야 했습니다. 또한 리뷰어 패스를 통해, 플레이어가 이길 수 없는 속임수(sucker bets)를 만드는 '무너지는 플랫폼 위에 코인을 배치하는 템플릿' 역시 거부하도록 설정되었습니다.
주스 패스(juice pass, 시각적/청각적 효과 작업)는 마지막 단계였으며 예상보다 더 중요했습니다. 착지 시의 찌그러짐과 늘어남(squash-and-stretch), 먼지 입자, 화면 흔들림(screen shake), 떠다니는 점수 팝업, 떠다니는 불씨, 그리고 사망 시의 슬로 모션 효과 등이 포함됩니다. 오디오조차 코드입니다. tools/gen-music.mjs가 칩튠(chiptune) 루프를 처음부터 합성하므로, 리포지토리(repo)에는 구매한 제3자 에셋 팩이 전혀 포함되어 있지 않습니다. 스프라이트(sprites)는 PixelLab으로 생성되었으며, 모든 오디오는 합성 스크립트로부터 다시 생성할 수 있습니다.
주의할 점 (The gotchas)
실제로 디버깅 시간을 소모하게 만든 세 가지 사례가 있습니다.
빌드 스크립트의 단순한 tsc 호출이 소스 코드를 조용히 가려버렸습니다. 빌드는 tsc && vite build로 실행되었는데, tsc가 .ts 소스 파일 옆에 컴파일된 .js 파일들을 생성했습니다. Vite는 .ts보다 .js를 먼저 해석하므로, TypeScript를 수정하는 동안 개발 서버가 오래된 컴파일 결과물을 계속 서빙하게 되었고, 결과적으로 변경 사항이 반영되지 않았습니다. 해결 방법은 tsconfig.json에 "noEmit": true를 설정하는 것입니다 (빌드 시 tsc는 타입 체크 용도로만 필요하며, 번들링은 Vite가 수행합니다). 우리가 얻은 교훈은 다음과 같습니다: src/ 디렉토리에 정체 모를 .js 파일이 방치되도록 두지 마세요.
WebGL 게임은 스크린샷을 안정적으로 찍을 수 없으므로, 동작 방식으로 검증해야 합니다. Phaser의 캔버스(canvas)는 preserveDrawingBuffer를 설정하지 않기 때문에, 프레임 사이에 찍은 스크린샷은 빈 화면이거나 불안정하게 나옵니다. 또한 keyCode를 사용하는 합성 KeyboardEvent는 브라우저에서 무시되므로, 그런 방식으로 입력을 흉내 낼 수도 없습니다. 우리의 해결책은 다음과 같습니다: 개발 빌드에서는 게임 인스턴스를 window.__game으로 노출하고, 검증 시 픽셀 대신 씬(scene)과 물리 상태(physics state) — 플레이어의 y-위치, 용암 높이, 플랫폼 개수 등 — 를 직접 읽습니다. 입력은 Phaser의 Key.isDown 플래그를 설정하고 플레이어 업데이트를 단계별로 진행하거나, 메뉴 핸들러를 위해 scene.input.keyboard에 keydown-SPACE를 발생시키는 방식으로 제어합니다. 관련 사항: 숨겨진 브라우저 탭은 requestAnimationFrame을 중단시켜 테스트 도중 전체 게임 루프를 얼려버릴 수 있습니다. 이 경우 game.step()을 사용하여 수동으로 프레임을 진행할 수 있습니다.
scene.start()는 이를 호출한 씬을 중단시킵니다. 메인 메뉴에서 설정(Settings)을 열 때 scene.start('Settings')를 사용했는데, 이는 메뉴(Menu) 씬을 조용히 중단시켰습니다. 설정에서 뒤로 가기를 하면... 아무것도 나타나지 않았습니다. 메뉴가 더 이상 존재하지 않기 때문에 검은 화면만 보였습니다. 해결 방법은 설정 씬이 종료될 때 돌아갈 라이브 씬이 있다고 가정하는 대신, 명시적으로 scene.start('Menu')를 호출해야 한다는 것입니다. 만약 오버레이(overlay) 스타일의 화면을 위해 Phaser의 씬 매니저(scene manager)를 사용한다면, launch/pause 의미론(semantics)과 start 의미론 사이의 차이로 인해 반드시 한 번은 이 문제로 고생하게 될 것입니다.
출시된 기능 (What shipped)
v0.2.0 버전이 github.com/denrod25-del/lava-leap에 공개되었습니다. 전체 클라이밍 루프 (climb loop), 4단계 구역 진행 (four-stage zone progression), 세트피스 (set-pieces), 업적 (achievements), 데일리 시드 (daily seeds), 상점 (shop), 절차적 음악 및 SFX (procedural music and SFX), 일시정지/설정 (pause/settings), 그리고 크래시 복구 (crash recovery) 기능이 포함되어 있습니다. 테스트 스위트 (test suite)는 12개 파일에 걸친 51개의 유닛 테스트 (unit tests)와 2개의 Playwright 엔드 투 엔드 테스트 (end-to-end tests)로 구성되어 있으며, 두 릴리스의 모든 마일스톤 (milestone)은 머지 (merge) 전 라이브 상태에서 검토 및 검증되었습니다.
이 글은 이러한 방식으로 구축된 프로젝트들에 관한 시리즈의 첫 번째 게시물입니다. 대기 중인 프로젝트가 약 27개 더 있습니다. 전체 목록은 projects page에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기