바이브코딩으로 좀비 러너 게임 제작
나는 3D 애니메이터로서, 오히려 3D 게임은 피하려고 했다. 절대 일주일만에 나올수 없을뿐더러 기획부터 컨셉, 모델링, 텍스쳐, 라이팅, 환경과 레벨 디자인, 애니메이션, 그리고 구현해볼 프로그램을 모두 바이브 코딩으로 진행한다해도 제안하고 체크해 나가면서 신경쓸게 한두가지가 아니었다. 그런데 이번엔 궁금해졌다. 내가 직접 작업하지 않고서, 바이브코딩으로만 그래픽을 어디까지 끌어올릴 수 있을까?
그래서 3인 팀 과제로 1인칭 좀비 생존 러너(Into the Dead 느낌) MVP를 Godot 4로 만들기로 했다. 마감은 10/1(목) 24:00 코드 동결, 10/2(금) 10:00 최종 제출이다.
먼저 역할을 분명히 적어 둔다. 나중에 다시 읽을 때 헷갈리면 안 되니까.
| 누가 | 한 일 |
|---|---|
| 나 | 방향 결정, 콘셉트 이미지·카드 시트·하늘 파노라마 생성(OpenAI·Gemini), 무료 3D 에셋 수집(Sketchfab·Pixabay), 분위기 피드백, 폰 실기기 확인, PR 머지 — 컨트롤 타워·슈퍼바이저 |
| Claude (Claude Code) | 기획 문서 작성, 도구 설치, Blender 스크립트로 모델 34종 생성, Godot 스테이지 빌더 코드, 배치 알고리즘, 셰이더, 에셋 최적화, 성능 측정·최적화, 모바일 버그 추적, 라이선스 대조, git·PR — 실제 작업자 |
나는 화면을 보고 “여기 떠 있다”, “너무 비었다”, “톤이 붉다”라고 말했고, 코드와 배치와 원인 추적은 전부 Claude가 했다.
1일차 (9/28) — 게임 만들기 전에 문서부터
플랫폼과 스택
처음엔 Steam과 구글 플레이 사이에서 고민했는데, 안드로이드 폰(갤럭시 S24 울트라)이 있고 팀원도 테스트하기 쉬운 구글 플레이로 정했다. Claude가 스택을 짰다.
- Godot 4.7 (Mobile 렌더러, GDScript) — 가볍고 무료, 안드로이드 내보내기가 쉬움
- Blender 5.2 — GUI 대신 파이썬 스크립트로 모델 생성 (
blender -b -P script.py) - Mixamo — 좀비 캐릭터·애니메이션
- Supabase — 회원가입·게스트 로그인, 관리자 화면
- 토스페이먼츠 테스트 결제 (웹·앱) + OpenAI 문의·버그 제보 채팅
문서 4종
“코드 전에 합의부터”라는 방향으로 Claude가 문서를 먼저 만들었다. PRD.md(무엇을), TECH_SPEC.md(어떻게), WORK_UNITS.md(작업 단위), FINAL_CHECKLIST.md(최종 점검). 좀비 4종(워커·러너·탱커·매복), 총알 0발로 시작해 낙하산 보급을 주워야 하는 권총, 스테이지당 한 번만 쓰는 칼 같은 규칙은 내가 정했다.
역할 분담
- A 월드·비주얼 / B 게임플레이(+테스트 패널) / C 서비스·UI·배포(+관리자·보안 점검)
- 담당 사이는 연결 규칙(파일 이름·신호·크기)으로만 연결 → 각자 가짜 부품으로 먼저 만들고 진짜가 오면 같은 이름으로 교체 (TECH_SPEC 13.3)
도구 설치와 폰 연결
Claude가 설치 상태를 확인하고 없는 것만 순서대로 깔았다(안드로이드 SDK 라이선스 동의는 내가 직접). 폰은 블루투스로는 안 되고 USB 디버깅으로 연결했다. 저장소는 aiffel_work에서 빼서 독립 공개 저장소(Lightnine999/3D_Game_MVP)로 분리했고, 비밀키·중요 문서가 올라가지 않게 .gitignore와 pre-commit 비밀키 검사 훅을 넣었다.
2일차 (9/29) — 맵을 말로 만들기
v1: 1,000m 흑백 들판, 그리고 영상 캡처 삽질
“1,000m 맵을 1인칭 초속 5m로 훑어 달라”고 했다. 풀·나무를 전부 3D로 심으면 무겁지 않냐고 물었더니, Claude는 “눈앞 50m만 진짜처럼” 전략을 제안했다. 50m 조각마다 풀은 MultiMesh 카드로, 멀리 있는 건 visibility_range로 아예 안 그린다.
주행 영상 녹화에서 한참 막혔다. Godot의 Movie Maker는 맥에서 창이 가려지면 그리기가 느려져서 영상이 짧거나 깨졌다(24GB 파일이 생긴 적도 있다). Claude가 찾은 방법은 SubViewport에 그리고 RenderingServer.frame_post_draw를 기다린 뒤 한 장씩 저장하는 것. 6,000프레임을 뽑아 연속 중복 프레임 0개를 확인했다. 폰에 APK를 깔아 보니 60fps.
v2: 새 콘셉트 — 분홍 노을, 안개, 폐허
콘셉트 이미지 5장(분홍 노을, 앙상한 나무, 불타는 드럼통, 폐차, 부서진 다리, 요새 정문)을 보여 주고 “이 분위기로, Blender로 필요한 건 만들어서”라고 했다. Claude가 make_stage_v2.py 하나로 모델 34종(나무·폐가·폐차·스쿨버스·다리·성벽·투광등 등)을 만들었다. 모델에 UV가 없어도 되게, 재질 이름(wood_, rust_, concrete)을 보고 월드 좌표 3방향 투영(triplanar)으로 질감을 입히는 방식을 썼다.
- Poly Haven CC0 질감 4종 + HDRI를 받자고 한 건 나, 적용은 Claude. HDRI는 파랗고 하얀 하늘이라 그대로는 안 맞아서, 밝기를 색 그라데이션(보라 → 분홍 → 살구)으로 다시 칠하고 산 능선을 합성했다.
- 폰에서 60fps 유지 확인 후 커밋.
내 피드백 → 레이아웃 재설계
여기서부터 내가 계속 방향을 잡았다.
| 내 피드백 | Claude의 처리 |
|---|---|
| 길은 필요 없다, 수풀로 채워라 | 바닥 셰이더에서 흙길 제거, 수풀을 “한 줌씩 던지는” 군락 배치 |
| 집이 너무 많다, 폐허는 듬성듬성 | 집 거리 삭제, 맵 전체 10채 이내 |
| 장애물을 콘셉트처럼 | 장면 단위 장애물(교통사고·캠프·전복차·잔해·스쿨버스·검문소), 한쪽 3.5m는 항상 비움 |
| 다리가 너무 길어 긴장감이 없다 | 강 폭 40m → 20m, 상판 6칸 → 3칸 |
| 거리를 반대로 | 1,000m 출발 → 목표 0m (화면·표지판은 남은 거리, 코드 수치는 달린 거리 그대로) |
| 누운 나무 없애기, 풀 더 높게 | 쓰러진 나무 제거, 풀 무릎-허리 높이 |
| 가운데로 조금 당겨라, 비어 보이지 않게 | 모든 배치 x 범위 약 25% 안쪽으로 |
“거리 반대로”는 PRD·TECH_SPEC·WORK_UNITS·FINAL_CHECKLIST·README까지 5개 문서를 v0.5로 같이 고쳤다. 지형 타일을 이어 붙이던 원래 약속은 StageBuilderV2 빌더 하나를 넘기는 방식으로 바꿨다(B는 build()와 obstacles 목록만 쓰면 됨).
그림 카드: AI 이미지를 오려서 세우기
AI로 뽑은 폐차·나무 시트를 카드로 세우자고 했다. 문제는 이미지가 투명 배경이 아니라 체크무늬가 그림으로 박혀 있었다는 것. Claude가 오리기 스크립트를 만들었는데 여러 번 고쳤다.
- 체크 칸 간격이 곳곳에서 달라 전체 격자 계산이 실패 → 주변 7x7이 두 회색 중 하나면 배경
- 차 안의 회색(타이어·그림자)까지 지워짐 → 밝은 칸·어두운 칸이 둘 다 있어야 배경
- 문질러진 가로 얼룩 → 세로로 값이 일정한 무채색 띠를 배경으로
- 외곽이 거칠다는 피드백 → 소프트 알파 + 섞인 배경색 빼기, 그린스크린 시트는 despill
근거리 테스트에서 12m 밖은 자연스럽고 5m 안은 떠 보여서, 카드는 원거리 전용, 근거리는 3D 에셋으로 가기로 했다. 풀은 Blender로 5종(야생풀·이삭·갈대·잡초·들꽃대)을 만들어 투명 PNG로 구웠다.
하늘과 깊이감
OpenAI로 뽑은 가로 띠 파노라마를 줬다. 정면 180°는 원본, 뒤 180°는 좌우 반전으로 이었다. 그런데 산이 안 보였다. 다른 세션(FOREST TOUR)에서는 보이는데 왜?
- 하늘에 깔면 산이 지평선에 딱 붙어 허리 높이 풀에 가려진다 → 카메라를 따라다니는 반지름 180m 원통 배경막에 붙여 산 능선을 지평선 위 6-12°로 올림
- 더 근본적인 원인: 미리보기 카메라
far가 60m였다. 초기 성능 설정이 남아 있어서 120m까지 깔았다던 먼 숲과 산이 전부 잘리고 있었다. 400m로 올리자 해결.
안개는 무작위로 흩으면 한 덩어리로 뭉개졌다. 달리는 방향을 가로지르는 줄로 14-24m마다 세우고 줄마다 벗겨진 틈을 두니 “나무 - 안개 - 나무 - 안개” 층이 생겼다. 붙여 넣은 가이드(Distance Fade Pixel Dither + Visibility Range)도 카드에 적용했다.
무료 3D 에셋 투입과 최적화
나무·폐차·폐허·드럼통을 Sketchfab·Pixabay에서 받아 넣었다. 원본 합계 약 1.4GB, 스캔 폐차는 한 대에 250만 면. Claude가 optimize_glb.py를 만들었다.
- 조각 합치기 → 면 줄이기(스캔은 이음매 점을 먼저 합쳐야 줄어듦) → 텍스처 512px → 실제 크기(차 4.6m, 나무 6-14m) → 원점 바닥 가운데
- 원본은
art/source_assets/에 두고 git·APK에서 제외 - 모델 폴더를
trees/vehicles/house/props로 정리, 코드는ModelLibrary.path("이름")로 찾게
여기서 내가 짚은 문제들:
- 떠 있는 나무: 늘어진 가지 끝이 원점이라 줄기가 떠 있었음 → Blender로 줄기 밑동 높이를 재서 모델별로 묻음
- 솟은 폐차: 옆으로 눕히는 연출이 모델마다 축이 달라 트렁크가 솟음 → 눕히기 삭제, 2.5°만
- 깨진 나무: 폴리곤을 너무 줄여 가지가 삼각형 판자·조각이 됨 → 확대 점검 후 4종 제거
- 초록 테두리: 그린스크린 기준을 60 → 35로 낮추고 가장자리 1px 깎기
성능: 44-56fps → 60fps
에셋을 넣자 폰이 44-56fps로 떨어졌다. Claude가 항목별로 꺼 보는 측정 옵션 (--perf-off=shadow,trees,...)을 만들어 면 수를 쟀다.
| 원인 | 효과 | 조치 |
|---|---|---|
| 그림자 | 끄면 면 수 -61% | 나무·카드 그림자 끄기 |
| 3D 나무 | 끄면 -70% | 50m당 30 → 18그루, 일부 면 축소 |
| 스캔 폐차 | 끄면 -15-60% | 길옆은 가벼운 차, 스캔은 15%만 |
면 수 160만-280만 → 41만-83만, 폰 60fps.
가장 오래 걸린 버그: 폰에서만 보이는 검은 쐐기
폰에서만 화면 가운데에 검은 삼각형 그림자가 계속 따라다녔다. 맥에선 멀쩡했다. 내가 “모바일과 PC 렌더러가 다르냐”고 물었다. 둘 다 Mobile 렌더러지만 맥은 Metal, 폰은 Vulkan(Adreno)이라 정밀도가 다르다는 답.
Claude가 추측을 하나씩 지워 나갔다: 해 그림자 끄기 ✗, 초소형 풀(0.0001 크기) 제거 ✗, 안개 햇빛 산란 끄기 ✗. 폰에 인자를 넘겨 항목을 끄는 방법은 실제로 전달되지 않아서 측정 자체가 무효였던 것도 발견. 결국 끄는 항목을 APK에 직접 박아 한 버전씩 설치해 범인을 풀로 좁혔고, 풀 조명 계산이 폰에서 특정 영역만 망가지는 것으로 결론 → 풀을 unshaded로 그리고 밝기를 미리 맞췄다. 쐐기가 사라지고 FPS도 올랐다.
같은 날 다리 깜빡임도 폰에서만 났다. 다리 첫 칸 상판 윗면이 바닥과 정확히 같은 높이(y=0)라 깊이 정밀도가 낮은 폰에서 두 면이 번갈아 보였던 것. 상판 아래 바닥을 뚫어 해결. 맥 영상은 다리 구간 360프레임을 한 장씩 비교해 튀는 프레임 0개, 폰은 0.6초 간격 14장으로 확인했다.
교훈: 맥 영상만 보고 끝내면 안 된다. 폰 캡처(
adb exec-out screencap)로도 꼭 본다.
라이선스: “무료”라고 다 같지 않다
“출처만 밝히면 되지 않나?”라고 물었다. Claude가 GLB 파일 안에 적힌 작가·라이선스·원본 주소(asset.extras)를 읽어서 30개를 확정했다.
- CC BY 4.0 20종 → 출처·변경 사항 기록
- CC BY-NC 2종 + BY-NC-ND 1종 → 제외 (결제가 있는 게임은 상업적, ND는 수정 금지)
- Pixabay 4종 → 무료·표기 불필요, 단 원본 단독 재배포 금지 → 최적화본만 커밋
- 모델을 목록에서 빼면 난수가 밀려 배치가 바뀌어서, 목록은 그대로 두고 놓는 순간에만 파일을 바꿔 끼우는 방식으로 레이아웃을 유지
팀 합치기
- PR #2(내 스테이지) 머지 → 팀원 PR #1(좀비 4종)이 문서 변경 이력 한 줄에서 충돌 → 두 줄 다 남겨 해결 후 머지. 탱커 키 예외(2.0-2.4m) 제안은 PRD와 맞아서 수용.
- 좀비를 맵에 세워 보니 워커가 새까맸다. glTF에 금속 값이 비어 있어 기본값 1.0(완전 금속)이 적용된 것 → 0으로, 변환 스크립트도 수정 (PR #3).
- 모션이 부족해서 WU-20b 보강 목록(잡기·물기·찔려 쓰러짐·밀려남·덮침·누워 있기)과 “못 구하면 대체 동작”을 문서에 넣었고, 탱커 돌진(달리기를 느리게 재생)을 추가했다.
느낀 점
- 3D 아티스트로서 가장 크게 느낀 건, “보는 눈”이 곧 작업 지시서가 된다는 것. “떠 있다”, “평면적이다”, “안개가 층이 안 진다” 같은 피드백만으로 Claude가 원인을 수치로 찾아서 고쳤다. 손은 안 댔지만 아트 디렉션은 계속 내 몫이었다.
- 대신 확인은 내가 해야 한다. 검은 쐐기, 다리 깜빡임, 까만 워커는 전부 내가 폰이나 화면에서 보고 짚었다. AI가 “해결됐다”고 해도 실기기에서 다시 봐야 한다.
- 바이브코딩도 결국 기술 부채가 생긴다. 카메라
far60m처럼 초기에 정한 값이 나중에 원인이 됐다. 결정한 이유를 문서에 남겨 두는 게 중요했다.
정리 (나중에 다시 볼 핵심)
- 배치:
StageBuilderV2.build()하나가 1,000m 전체를 만든다. 멀면 안 그림(visibility_range), 수풀은 50m 조각 MultiMesh - 거리 표기: 화면·표지판은 남은 거리, 코드·문서 수치는 달린 거리
- 카드: 12m 밖 원거리 전용, Y축 빌보드 + 채도·색조 누르기 + 거리 디더 페이드
- 깊이감: 카메라 따라가는 180m 배경막 + 가로 줄 안개층 + 대기 원근. 카메라
far400m,near0.15m - 모바일 전용 함정: 풀은 unshaded, 겹치는 면은 높이를 떨어뜨리거나 뚫기, 해 그림자 끔. 폰 캡처로 확인
- 성능 측정:
--perf-off=...+ 미리보기[stats]로그(면 수·그리기 호출) - 에셋:
optimize_glb.py로 줄이고, 원본은 git 제외, 라이선스는 GLB의 asset.extras로 확인 →ASSETS_LICENSE.md - 좀비 재질: 금속 값 0 (glTF 기본값 1.0 함정)
- 다음: 좀비 모션 보강(WU-20b), 좀비 AI·스테이지 연결, 탱커 돌진, 게임 안 크레딧 화면