three 하나뿐. 둘째, 사람이 코드를 거의 안 썼다. 단 한 개의 프롬프트로 시작해, 여러 AI 에이전트가 오케스트레이션(지휘) 아래 11개 서브시스템·약 66,000줄을 나눠 썼다.
mshumer/Claude-of-Duty · 라이선스 ISC · 버전 v1.0.0 · TrendShift 데일리 12위 · Three.js r180 + WebGL2 + Vite 7 · 아트 에셋 0개 · 약 66k 줄 / 11개 서브시스템 · 헤드리스 회귀 테스트 하네스 포함)
Claude of Duty는 웹 브라우저에서 바로 실행되는 1인칭 슈팅 게임이다. npm install 후 npm run dev만 하면 http://127.0.0.1:5173에서 돌아간다. 캔버스를 클릭하면 마우스가 잠기고(FPS 조작 시작), WASD로 이동·마우스로 조준·좌클릭 사격·우클릭 조준(ADS)·R 재장전·Shift 질주 같은 표준 FPS 조작을 지원한다.
보통 게임은 아티스트가 미리 만든 그림·3D 모델·효과음 파일을 불러와 조립한다. 마치 완성된 레고 블록 상자를 열어 설명서대로 쌓는 것처럼.
Claude of Duty엔 블록 상자 자체가 없다. 대신 "콘크리트 벽은 이런 수학 공식으로 생기고, 총소리는 이런 파형으로 합성한다"는 레시피(코드)만 있고, 게임이 켜지는 순간 그 레시피로 블록을 즉석에서 찍어낸다. 그래서 저장소 용량이 3MB밖에 안 되는데도 총·적·하늘·소리가 다 나온다.
기술적으로 요약하면 이렇다. 렌더링은 Three.js r180 + WebGL2 기반의 본격 HDR 파이프라인(그림자·앰비언트 오클루전·안티에일리어싱·블룸·톤매핑)을 직접 구현했고, 물리 엔진·오디오 합성·AI 적 캐릭터까지 전부 외부 라이브러리 없이 자작했다. 그리고 이 모든 걸 사람이 아니라 AI 에이전트 함대가 ARCHITECTURE.md라는 "계약서" 하나를 공유하며 병렬로 작성했다.
이 레포의 가치는 "AI가 게임을 만들 수 있다"는 신기함을 넘어선다. README와 ARCHITECTURE.md에 담긴 협업 방법론과 정직한 실패 기록이 개발자 커뮤니티에서 화제가 됐다. 세 가지 이유가 있다.
prompt.md에 이 저장소를 만든 프롬프트 전문이 그대로 들어있다. 핵심은 "서브 에이전트를 펼쳐(fan out) 각자 하나씩 맡게 하고, 별도의 아주 혹독한 비평가 에이전트가 시각적으로 검사해 AAA 수준이 아니면 계속 반복(loop)하라"는 것. 즉 사람이 코드를 짜는 게 아니라, 에이전트들에게 역할을 나눠주고 서로 검증시키는 구조를 프롬프트 한 방으로 지시했다.
건축가 한 명에게 "빌딩을 지어줘"라고 말하는 대신, "현장소장 1명 + 각 층 담당 인부 여러 명 + 깐깐한 감리 1명 팀을 꾸려서, 감리가 합격 도장을 찍을 때까지 반복하라"고 지시한 셈이다. 사람은 팀 구성 방식만 정하고, 실제 벽돌은 에이전트들이 쌓았다.
README가 직접 "이 레포에서 흥미로운 부분은 게임이 아니라 하네스(harness)일 것"이라고 못 박는다. tools/ 폴더엔 헤드리스 Chromium으로 스크린샷을 찍고, 픽셀 단위로 비교해 회귀를 잡아내는 검증 도구 모음이 있다. AI가 화면을 "본다"고 착각하지 않게, 기계적으로 측정 가능한 게이트를 만든 것이 이 프로젝트의 진짜 엔지니어링이다.
대부분의 화려한 AI 데모가 잘 된 장면만 보여주는 것과 달리, 이 레포는 어디서 실패했는지를 조목조목 적어뒀다. 11명의 적대적 비평가 에이전트가 매긴 점수는 10점 만점에 3.59 → 4.14 → 4.05 → 5.05로 올랐지만, 블라인드 A/B 테스트에서 모든 비평가가 매번 진짜 콜 오브 듀티 화면을 골라냈다. 손(손가락이 총을 제대로 못 쥠), 재질의 리얼함, 원거리 적 캐릭터, 간접광(GI) 근사, 프레임레이트 등 부족한 점을 구체적 수치와 함께 나열한다.
"순차 단일 소유(sequential single-owner) 방식이 병렬 팬아웃(parallel fan-out)을 압도했다"는 발견이 대표적이다. 6개 에이전트가 디렉토리를 하나씩 병렬로 맡았을 땐 점수가 +0.46 오르는 데 그치고 오히려 결함이 늘었다. 반면 서로 얽힌 문제(톤매핑·하늘·간접광은 한 몸)를 한 명이 순차로 담당하니 +1.00 오르고 결함이 66→26으로 줄었다. AI 협업을 설계하려는 사람에게 값진 교훈이다.
이 프로젝트의 스택은 극단적으로 얇다. 게임이 실제로 돌 때 필요한 외부 라이브러리는 three 딱 하나다. 이것이 ARCHITECTURE.md의 "하드 룰(Hard rules)" 3번 — "새 npm 의존성 금지, three만, CDN·외부 이미지·HDRI·모델·오디오 파일 금지. 게임은 완전히 오프라인으로 돌아야 한다" — 때문이다.
| 기술 | 역할 — 초보자 설명 |
|---|---|
| Three.js r180 | WebGL을 쉽게 다루게 해주는 3D 라이브러리. 이 게임의 유일한 런타임 의존성 |
| WebGL2 | 브라우저가 GPU를 직접 쓰게 해주는 저수준 그래픽 API. 3D를 실시간으로 그린다 |
| HDR 파이프라인 | 밝기를 넓은 범위로 계산(High Dynamic Range)해, 눈부신 햇빛·어두운 그늘을 자연스럽게 표현 |
| CSM 그림자 | Cascaded Shadow Maps — 가까운 곳은 선명, 먼 곳은 대충 그려 넓은 야외 그림자를 효율적으로 |
| GTAO / TAA | 구석을 어둡게(앰비언트 오클루전) + 계단현상 제거(안티에일리어싱). 화면을 "덜 CG스럽게" |
| Bloom / AgX | 밝은 부분 번짐 효과 + 영화 같은 색보정(톤매핑). 최종 화면의 "때깔"을 담당 |
| 영역 | 무엇을 코드로 만드나 |
|---|---|
| materials | 콘크리트·벽돌·금속·나무·유리 등 19종 표면 텍스처를 GPU에서 노이즈로 생성. 이음매 없이 반복(타일링) |
| sky | 대기 산란·시간대별 하늘·태양/달·볼류메트릭 안개를 물리 기반으로 계산 |
| weapons | 총 3종(rifle·smg·pistol)의 메시·조준·반동·재장전 애니메이션을 전부 코드로 |
| audio | Web Audio API로 총소리·발자국·반향(reverb)을 합성 — 사운드 파일 0개 |
| 물리 엔진 | 라이브러리 없이 자작. BVH 브로드페이즈·스윕 캡슐 캐릭터 컨트롤러·강체·래그돌·관통 |
| AI | 스킨드 병사·내비메시 길찾기·시야 감지·엄폐 행동·래그돌 사망 |
| Vite 7 | 개발 서버 & 번들러. npm run dev로 즉시 실행 |
| Playwright | 헤드리스 Chromium 자동화. 스크린샷 캡처·회귀 테스트 하네스의 엔진 |
| pngjs | 캡처한 PNG를 픽셀 단위로 읽어 imagediff.mjs가 비교 |
dependencies에 three 하나, devDependencies에 playwright·pngjs·vite 셋. "손님에게 내는 요리엔 재료 1개만, 주방 뒤편엔 검사 장비 3개"인 셈이다. 검사 장비(Playwright 등)는 게임에 안 실리고, AI가 자기 결과물을 채점할 때만 쓴다.
이 게임의 아키텍처는 AI 에이전트 병렬 작업을 안전하게 만들기 위해 설계됐다. 핵심 아이디어는 "엔진(Engine)은 각 서브시스템이 무슨 일을 하는지 전혀 모르고, 오직 실행 순서만 지휘한다"는 것. 각 에이전트는 자기 디렉토리만 소유하고, 남의 모듈을 import하지 않는다. 필요하면 런타임에 ctx.get('fx')로 꺼내 쓴다. 이 규칙이 병렬 작업 중 서로의 코드를 망가뜨리지 않게 하는 열쇠다.
weapon:fire를 방송하면, fx는 총구 화염을, audio는 총소리를, physics는 탄환을 각자 처리한다. 서로 몰라도 협력이 된다.모든 서브시스템은 아래 뼈대를 따른다. 이 통일된 계약 덕에 엔진은 순서만 돌리면 된다.
export class MySystem {
static id = 'mysystem'; // 고유 이름 — 남들이 나를 찾는 열쇠
static deps = ['render']; // 나보다 먼저 init 돼야 하는 것들
async init(ctx) {} // 리소스 생성 (await 가능)
fixedUpdate(h, ctx) {} // 120Hz 결정론적 게임플레이
update(dt, ctx) {} // 프레임당 1회
lateUpdate(dt, ctx) {} // 모든 update 후
dispose() {} // 내가 만든 것 정리
}
하드 룰 4번: "게임플레이나 비주얼에서 Math.random() 금지. ctx.rng를 써라." 왜? 캡처 재현성 때문이다. 같은 시드(seed)에서 항상 같은 결과가 나와야, 스크린샷 회귀 테스트가 "이 픽셀이 바뀌었다"를 신뢰할 수 있다. 엔진은 config.deterministic이면 고정 시드 0x5eed1234를 쓴다.
하드 룰 5번: "프레임당 아무것도 새로 할당하지 마라." update() 안에서 new THREE.Vector3()를 만드는 건 버그로 취급한다. 벡터·행렬·배열은 init()에서 미리 만들어 재사용한다. 매 프레임 60번씩 객체를 새로 만들면 가비지 컬렉터가 돌며 프레임이 뚝뚝 끊기기(hitch) 때문이다.
물리는 120Hz 고정 스텝(fixedUpdate)으로 돌아 프레임레이트와 무관하게 항상 같게 동작하고, 화면은 time.alpha(0~1)로 마지막 두 물리 스텝 사이를 보간(interpolate)해 부드럽게 그린다. 게임 물리의 정석 패턴이다.
정적 카메라 벤치마크는 94 fps라고 보고했지만, 실제 게임은 플레이 불가능했다. 레티나 해상도 실제 플레이에선 12~17 fps에 728~1236ms짜리 멈춤(stall)이 있었다. 원인은 34개 이상의 WebGL 셰이더 프로그램이 프레임 도중에 게으르게(lazily) 컴파일된 것.
해법은 profile.mjs가 p50/p95/p99 분포를 재고 각 끊김의 원인을 지목한 것, 그리고 src/core/prewarm.js로 셰이더를 미리 데워두기(pre-warm)였다. 최적화 후: p50 28~30fps, 최악 프레임 66~82ms, 플레이 중 셰이더 컴파일 0회.
디렉토리 구조가 곧 작업 분담표다. ARCHITECTURE.md의 "소유권 지도(Ownership map)"가 각 폴더의 주인을 못 박아, 에이전트끼리 남의 영역을 건드리지 못하게 한다. 아래는 실제 소스 라인 수까지 집계한 것.
일반 오픈소스는 "누가 어디를 담당하는지"가 암묵적이지만, 여기선 폴더 = 부서, ARCHITECTURE.md = 조직도 겸 업무규정이다. 부서끼리는 직접 전화(import) 금지, 오직 사내 방송(events)으로만 소통. 이렇게 해야 여러 명(에이전트)이 동시에 일해도 충돌이 안 난다.
| 도구 | 용도 |
|---|---|
| capture.mjs | GPU 헤드리스 Chromium으로 지정한 장면 스크린샷 1장 캡처 |
| shotset.mjs | 11개 장면을 한 세션에 — 빠른 리뷰용 |
| baseline.mjs | 재현 가능한 캡처: 각 장면을 격리된 페이지에서·고정 프레임 예산으로. 실행마다 비트 단위 동일 |
| imagediff.mjs | 픽셀 단위 게이트. 한 픽셀이라도 움직이면 0이 아닌 코드로 종료(=테스트 실패) |
| profile.mjs | 실제 DPR로 게임플레이 프로파일링. 프레임타임 분포(p50/p95/p99)와 끊김 원인 지목 |
| playtest.mjs | 스크립트로 이동·사격하는 스모크 테스트 |
shotset.mjs는 11개 장면에 한 페이지를 재사용해, 파티클 수명·데칼 버퍼·노출 상태가 다음 장면으로 새어나갔다(leak). 그 결과 동일한 두 번의 실행이 11장 중 10장에서 달랐다. baseline.mjs는 각 장면을 새 페이지에 격리해 비트 단위로 동일하게 만들었고, 그래야 imagediff.mjs가 쓸모 있는 게이트가 된다.
가장 값진 부분. 순차 단일 소유 vs 병렬 팬아웃의 실증 비교(§2 참고)는 AI로 큰 코드베이스를 만들려는 모든 사람에게 적용된다. 핵심 원리: "강하게 결합된 문제(coupled concern)는 한 에이전트가 순차로 맡아야 한다." 톤매핑·하늘·간접광처럼 서로 물린 문제를 여러 에이전트가 나누면 서로의 가정을 계속 깨뜨린다.
작은 프로젝트라도 (1) 모듈 인터페이스, (2) 디렉토리 소유권, (3) 모듈 간 이벤트 어휘, (4) 공유 타입 — 이 4가지를 한 문서에 못 박아보라. AI에게 여러 부분을 맡길 때 충돌이 급감한다.
materials/는 노이즈 함수·Sobel 높이→법선 변환·시차 오클루전 매핑·삼중평면(triplanar) 투영으로 19종 표면을 만든다. audio/는 파형 합성·컨볼루션 리버브·HRTF 공간화로 소리를 만든다. 파일 없이 콘텐츠를 만드는 기법은 웹 게임·데모신(demoscene)·생성 예술에 직결된다.
HDR → MRT 프리패스(깊이/법선/속도) → GTAO → TAA(YCoCg 분산 클리핑) → 모션블러 → 블룸 → EV100 자동노출 → LUT → AgX. 이 후처리 체인 전체를 실제 코드로 읽을 수 있는 희귀한 오픈소스다. 게임 그래픽 프로그래밍 교재로 손색없다.
"AI가 만든 UI/그래픽이 안 망가졌는지"를 어떻게 자동으로 검증하는가에 대한 실전 답. 고정 시드 + 격리된 페이지 + 픽셀 diff 조합은 프론트엔드 비주얼 회귀 테스트에도 그대로 쓸 수 있다.
"중앙값이 아니라 분포(p50/p95/p99)를 보라", "정적 벤치마크가 아니라 실제 사용 시나리오를 재라"는 교훈은 웹·백엔드·모바일 모든 성능 최적화에 공통이다.
BVH 브로드페이즈(29k 삼각형 → 22ms에 14k 노드, 레이캐스트당 0.25µs)·스윕 캡슐 캐릭터 컨트롤러·CCD 강체·PBD 래그돌·다층 탄환 관통을 자작. 충돌 검출·공간 분할의 정석 자료구조를 실제 구현으로 학습할 수 있다.
| 항목 | 요구사항 |
|---|---|
| 브라우저 | WebGL2 지원 최신 크롬/엣지/사파리/파이어폭스. GPU 가속 필수 |
| 개발 환경 | Node.js (Vite 7 실행용) · npm install → npm run dev |
| GPU | Apple silicon 노트북 기준 ultra 프리셋에서 28~30 fps(최적화 후). 저사양은 low 프리셋 |
| 화질 프리셋 | low(그림자 1024, 파티클 2000) → medium → high → ultra(그림자 4096, 파티클 24000) |
| 부팅 시간 | 최적화 후 3.7~4.6초 (셰이더 pre-warm 포함). 최적화 전엔 9~12초 |
| 네트워크 | 불필요 — 완전 오프라인 실행 (외부 에셋 로딩 0) |
README가 명시하듯 실제 화질은 AAA에 못 미치고(비평가 5.05/10), 레티나 해상도에서 28~30fps다. 아트 패스가 지오메트리를 3배(5.9M→11.3M 삼각형)로 늘려 최적화로 절반만 회복했다. "단일 프롬프트로 이 정도가 나온다"는 실험의 증거물로 보는 게 정확하다.
1. 실행하고 하드 룰 읽기. 레포를 클론해 npm install && npm run dev로 게임을 켜본다. 그다음 ARCHITECTURE.md의 "Hard rules" 7가지를 읽고, 각 규칙이 왜 AI 병렬 작업에 필요한지 한 줄씩 정리해본다.
2. 프리셋 바꿔보기. src/core/config.js에서 quality를 ultra→low로 바꿔 fps가 얼마나 오르는지, 화면이 어떻게 달라지는지 관찰한다.
3. 스크린샷 하네스 돌려보기. node tools/capture.mjs로 한 장면을 캡처하고, baseline.mjs → imagediff.mjs 흐름을 실행해 "픽셀 게이트"가 어떻게 작동하는지 본다. 코드를 일부러 조금 바꿔 diff가 실패하게 만들어본다.
4. 새 텍스처 추가. materials/에서 기존 절차적 표면 하나(예: 벽돌)를 복제·변형해 새 재질을 만들고, world에 적용해본다.
5. 새 서브시스템 만들기. ARCHITECTURE.md의 인터페이스를 따라 static id·init·update를 가진 새 시스템(예: 날씨 효과)을 추가하고, ctx.events로 기존 시스템과 연동한다. 남의 모듈을 import 없이.
6. 이 방법론을 내 프로젝트에 이식. 내가 만들려는 앱에 대해 ARCHITECTURE.md 스타일 계약서를 쓰고, AI 에이전트에게 "순차 단일 소유" 방식으로 서브시스템을 하나씩 맡겨본다.
| 주차 | 주제 · 무엇을 할까 |
|---|---|
| 1주차 WebGL·Three.js | Three.js 기초(Scene·Camera·Mesh·Material) → 이 레포의 core/engine.js가 어떻게 렌더 루프를 짜는지 대조. WebGL2의 MRT·sampler2DArray 개념 학습 |
| 2주차 실시간 렌더링 | PBR·톤매핑(AgX)·그림자맵(CSM)·앰비언트 오클루전(GTAO)·안티에일리어싱(TAA) 이론 → render/ 코드로 확인. "LearnOpenGL" 후처리 챕터 병행 |
| 3주차 게임 물리·AI | 고정 스텝 루프·BVH·캡슐 충돌·래그돌(PBD) → physics/ 읽기. 내비메시 길찾기(A*)·시야/엄폐 AI → ai/ |
| 4주차 AI 에이전트 협업 | 이 레포의 진짜 주제. prompt.md·ARCHITECTURE.md·README의 "Process note"를 정독하고, 순차 vs 병렬·적대적 비평가·자동 채점 게이트를 내 워크플로에 적용 |
Math.random() 대신 ctx.rng.prompt.md·ARCHITECTURE.md·README를 꼭 읽을 것