· 15 min read

AI에게 게임 월드를 만들라고 시켰더니 — AI-Driven Game Engine Development #1

AI가 월드를 자율적으로 만들려면 뭘 준비해야 하는가. 도구를 설계하고, 시행착오를 거치고, 벽 없는 미로에서 교훈을 얻기까지.

AI가 월드를 자율적으로 만들려면 뭘 준비해야 하는가. 도구를 설계하고, 시행착오를 거치고, 벽 없는 미로에서 교훈을 얻기까지.

이 글은 AI-Driven Game Engine Development 시리즈의 첫 번째 편이다. 내가 만들고 있는 오픈 월드 게임 서버 프레임워크 OFF(Open Field Framework)를 Claude Code와 함께 개발하면서, AI를 효과적으로 활용하기 위해 어떤 고민과 시행착오를 거쳤는지 기록한다.


코드 생성을 넘어서

AI에게 코드를 짜달라고 하면 잘 짠다. 하지만 게임 엔진 개발에서는 코드를 작성하는 것만으로는 부족하다. 월드를 만들면 실제로 돌려봐야 하고, 돌려보면 예상 못한 문제가 나오고, 그걸 진단하고 고쳐야 한다.

처음에는 나도 AI에게 코드만 시켰다. “이 함수 짜줘”, “이 버그 고쳐줘”. 그러다가 OFF의 월드 시스템이 복잡해지면서 생각이 바뀌었다. 월드를 하나 만들려면 스크립트 작성 → 생성 → 시뮬레이션 → 검증 → 수정의 루프를 여러 번 돌아야 하는데, 이 루프 전체를 내가 일일이 지시하는 건 비효율적이었다.

그래서 AI가 이 루프를 자율적으로 돌 수 있게 만들기로 했다. 핵심은 도구를 만들어주는 것이었다.

/worldgen(월드 생성), /worldrun(헤드리스 시뮬레이션), /worldinspect(데이터 검사) — 이 세 가지 CLI 도구를 만들고, Claude Code 스킬로 등록해서 AI가 자유롭게 쓸 수 있게 했다. 도구 인프라의 설계 철학은 3편에서 다룬다.

이 글에서는 그 결과 실제로 어떤 일이 벌어졌는지 — 잘 된 것과 안 된 것 모두 — 를 세션 로그에서 뽑아서 보여주겠다.

사례 1: 화산 군도 — 도구가 작동하는 순간

“자유 컨셉으로 월드 하나 만들어줘. /worldgen으로 생성하고 /worldrun으로 30초 돌려서 결과 확인해줘.”

이 한 문장을 입력하고 지켜봤다. AI는 “화산 군도(Volcanic Archipelago)“를 컨셉으로 잡고, 기존 스크립트를 참고해서 21종의 커스텀 블록과 9개 바이옴을 정의한 JS 스크립트를 작성했다. 그리고 /worldgen으로 생성, /worldrun으로 시뮬레이션까지 알아서 진행했다.

순수 지형은 잘 나왔다. 4개 대륙, 1.2MB, 1.21초 만에 생성. 시뮬레이션도 평균 0.04ms/frame으로 깔끔했다.

동적 기능을 시켰더니

지형만 있으니 심심해서 “동적인 기능 world script도 추가해봐”라고 시켰다. AI가 화산 분화, 낙석, 지열 분출 같은 이벤트를 구현하려 했는데, 여기서 크래시가 연달아 발생했다.

terrain 생성 전용 API(grid, blocks)를 런타임에서 호출하면 안 된다는 걸 AI가 에러를 보면서 알아냈다. 다른 스크립트를 분석해서 런타임용 패턴도 스스로 찾아냈고.

더 흥미로운 건 타이머 문제였다. every(20, ...) — 20초마다 분화하도록 설정했는데 30초 시뮬레이션에서 한 번도 안 발동했다. AI가 디버그 로그를 삽입해서 dt를 찍어보니 고정 timestep(1/60초)이라 CLI 30초가 시뮬레이션 시간으로는 약 10초밖에 안 되는 것이었다.

이건 나도 미처 생각 못한 부분이었다. CLI의 벽시계 시간과 게임 내 시간이 다르다는 건 당연한 건데, 도구를 만들 때 그걸 명시적으로 알려주지 않았다. AI가 직접 계측해서 알아낸 셈이다. 이 경험 이후 /worldrun의 출력에 시뮬레이션 시간을 함께 표시하도록 개선했다.

사례 2: 벽 없는 미로 — 검증 도구의 가치

이 사례가 “AI에게 도구를 줘야 한다”는 내 확신을 굳힌 계기다.

탈출 “성공”

AI에게 미로 월드를 만들라고 시켰다. recursive backtracking으로 16×16 미로를 생성하고, right-hand rule로 탐색하는 explorer 5명을 스폰했다.

시뮬레이션 결과: 31 steps만에 탈출 성공.

AI도 좀 의아해했다:

“31 steps로 16×16 미로를 탈출하는 건 너무 빠릅니다.”

직감은 맞았다. 하지만 AI가 추측한 이유(“우연히 짧은 경로”)는 틀렸다.

/worldinspect가 진실을 보여줬다

AI가 /worldinspect로 그리드 데이터를 검사했다.

Block 0 (Air):       57,500개
Block 1 (MazeFloor):  2,500개
Block 2 (MazeWall):       0개

벽이 하나도 없었다.

blocks.MazeWall이 ID 0으로 매핑되면서 Air와 충돌한 것이다. 31 steps에 탈출한 게 아니라, 빈 공간에서 우회전만 반복하다 우연히 출구 좌표에 도달한 것이었다.

근본 원인은 ParseCustomBlocks(arr, null) — 기존 블록 레지스트리 없이 custom block만 정의하면 ID가 0부터 시작해서 첫 블록이 Air와 충돌하는 프레임워크 버그였다. 기존 데모들은 전부 default registry를 사용해서 한 번도 노출된 적 없었다.

이 사례가 중요한 이유

만약 /worldinspect를 안 만들어뒀다면 어떻게 됐을까. AI는 “31 steps로 탈출 성공, 잘 작동합니다”라고 보고했을 것이다. 나도 그 보고를 믿었을 것이다.

검증 도구가 없으면 AI는 “동작하는 것처럼 보이는” 결과를 진짜라고 믿는다. 그리고 사람도 마찬가지다. 이 경험 이후 나는 새로운 기능을 만들 때 “AI가 이걸 어떻게 검증할 수 있을까?”를 항상 먼저 생각하게 됐다.

수정 후 벽이 제대로 생성된 미로에서 explorer는 129 steps에 탈출했다. 이전의 4배 이상.

3×3 대륙, 1.5km² 미로 월드
3×3 대륙, 1.5km² 미로 월드

이 세션 하나에서 나온 커밋이 5개다:

커밋내용
da8920a7좌표 변환 API 추가
e2b37518custom block ID 0=Air 충돌 버그 수정
2c035d70worldinspect CLI + skill 추가
217fff5f월드 전환 시 overview color 크래시 수정
02e13cc1스크립트 메모리 한도 8MB로 확장

사례 3: 개미 군집 — 복잡한 시스템에서의 디버깅

개미 군집 시뮬레이션은 가장 긴 세션이었다. 135개의 대화 턴. 그리고 이 세션이 내 엔진 아키텍처의 문제점을 가장 많이 드러내줬다.

개미가 안 움직인다

worker ant를 스폰하고 move action을 반환해도 개미가 제자리에 머물렀다. 추적해보니 원인은 엔진의 시스템 실행 순서였다:

  1. WanderAI 기본값이 Speed=2 → 매 프레임 velocity를 덮어씌움
  2. 시스템 실행 순서: ScriptBehavior → FleeAI → ChaseAI → WanderAI → Movement
  3. 스크립트가 velocity를 설정해도 WanderAI가 나중에 덮어씀

이건 내가 AI 시스템을 설계할 때 “스크립트 기반 커스텀 행동”을 제대로 고려하지 않은 것이 원인이었다. AI가 문제를 파고들면서 드러난 내 설계의 맹점이다.

NaN 시드 — JavaScript의 조용한 함정

움직이기 시작한 개미들이 전부 같은 방향으로 뭉쳐서 이동했다. RNG 시드를 추적해보니:

// e.id는 GUID 문자열: "a3f2c1d4-..."
var hash = e.id * 2654435761;  // JS에서 문자열 × 정수 = NaN
hash = hash & 0x7fffffff;      // NaN & 0x7fffffff = 0

모든 개미의 시드가 0. JavaScript는 타입 에러를 던지지 않고 조용히 NaN을 반환하고, NaN에 비트 연산을 하면 0이 된다.

이 버그가 흥미로운 건 C#과 JavaScript의 경계에서 발생했다는 점이다. C# 쪽에서 넘긴 GUID가 JS에서 문자열로 취급되는 건 당연한데, 해싱 코드가 그걸 고려하지 않았다. Jint(JS 엔진)를 통한 C#-JS 바인딩에서 이런 함정이 여러 개 있다는 걸 이 세션에서 실감했다. entity.find()의 반환값이 C# 배열이라 .filter()가 없는 것도 같은 맥락이다.

135턴의 수확

한 마리 개미를 움직이려고 했을 뿐인데, 엔진의 여러 레이어에서 문제가 쏟아져 나왔다:

  • AI 시스템 실행 순서 문제
  • JS-C# 바인딩 경계의 타입 함정
  • 엔티티 API 타입 필터 미작동
  • RNG 시드 해싱 결함

결과적으로 이 세션이 엔진 품질에 가장 큰 기여를 했다. AI가 “개미를 움직이겠다”는 단순한 목표를 끈질기게 추적하면서 내가 미처 테스트하지 못한 경로들을 전부 밟아준 셈이다. 새로운 종류의 테스터가 생긴 느낌이었다.

복잡도가 올라가면서

도구와 API가 안정되면서 만들 수 있는 월드의 복잡도가 점점 올라갔다.

Ecosystem Survival — 9 바이옴, 주야간 사이클, 인구 균형 자동 조절. Four Civilizations — 4문명 영토 경쟁 시뮬레이션. “Crimson이 항상 즉시 승리”하는 문제를 디버깅해서 해결. Ringed Continent — 10×10(100대륙) 규모, 400MB.

초기에 화산 군도 하나 만드는 데도 연쇄 실패를 겪었던 것에 비하면, 도구가 성숙하고 AI가 이전 세션에서 배운 규칙들을 메모리에 쌓아가면서 월드 생성의 신뢰도가 분명히 올라갔다.

돌아보며

이 과정에서 내가 배운 것들:

도구가 곧 능력이다. AI의 능력은 모델 자체보다 사용할 수 있는 도구에 의해 결정된다. /worldinspect 하나의 유무가 “벽 없는 미로를 발견하는가 못하는가”를 가른다.

검증 없는 자율성은 위험하다. AI에게 자율성을 주려면 검증 수단도 같이 줘야 한다. “만들어줘”만 시키고 결과를 확인할 도구를 안 주면, 잘못된 결과를 자신 있게 보고받게 된다.

AI는 예상 못한 경로를 밟는다. 그게 장점이다. 내가 직접 테스트했으면 밟지 않았을 경로에서 엔진 버그가 쏟아져 나왔다. 새로운 형태의 퍼지 테스팅이라고 봐도 될 것 같다.

도구는 쓰면서 개선된다. CLI의 시뮬레이션 시간 표시, worldinspect의 진단 가이드 — 전부 AI가 도구를 쓰면서 부족한 점이 드러나고 그때마다 고친 것이다. 처음부터 완벽한 도구를 설계하려고 하기보다 일단 만들고 실사용 피드백으로 개선하는 게 빨랐다.


이 시리즈의 모든 사례는 실제 Claude Code 세션 로그(2026년 2~3월, 약 180개 세션)에서 추출했다. OFF 프로젝트에 대한 소개는 이전 글을 참고.

Related Posts

View All Posts »
self-hosted CI에 Claude Code 구독 물리기 — PR 자동 분석·테스트 파이프라인

self-hosted CI에 Claude Code 구독 물리기 — PR 자동 분석·테스트 파이프라인

API가 아닌 Claude Code 구독을 self-hosted runner에 물려, CI가 돌 때마다 테스트 자동화와 PR 변경 분석을 수행하는 파이프라인. AI와 함께 코딩하니 테스트가 늘고, 테스트가 늘으니 CI가 필요해진 흐름을 정리했다.

#Aethelgard #AI #Claude #GameDev

.NET 서버 메모리 누수 잡기 — dotnet-gcdump로 안 보이는 할당 폭탄

.NET 서버 메모리가 2분 만에 136MB에서 14GB로 폭증했다. dotnet-gcdump와 dotnet-counters로도 안 잡히는 할당 폭탄을 추적하고, 그 과정을 재사용 가능한 스킬로 만든 이야기.

#Aethelgard #AI #Claude #GameDev
NuGet 프라이빗 패키지로 .NET 코드 분리하기 — 멀티 레포 9개 함정

NuGet 프라이빗 패키지로 .NET 코드 분리하기 — 멀티 레포 9개 함정

OFF를 NuGet 프라이빗 패키지로 떼어, 협업자에게 프레임워크·서버 소스를 노출하지 않고도 풀스택 dev 루프를 유지한 멀티 레포 구조와, 옮기면서 밟은 9개의 함정.

#OFF #Aethelgard #GameDev #DotNet