들어가며 — “최종 .exe ≤ 1.44MB”라는 단 하나의 제약
게임잼 하나를 목표로 게임을 만들고 있다. 규칙은 단순하고 가혹하다. 최종 실행파일이 1.44MB(=1,474,560바이트, 옛날 플로피 한 장)를 넘으면 실격. 게임성보다 우선하는, 협상 불가능한 하드 제약이 딱 하나 걸려 있는 콘테스트다.
이 제약은 보통 짐처럼 느껴지지만, 방향을 거꾸로 잡으면 오히려 설계를 깔끔하게 만들어 준다. 용량을 잡아먹는 주범은 코드가 아니라 에셋(텍스처·사운드·폰트·맵 데이터)이다. 그래서 결론은 처음부터 정해졌다 — 에셋 파일을 0개로 만든다. 그래픽도, 사운드도, 맵도, 폰트도 전부 코드로 그 자리에서 절차 생성(procedural generation)한다.
그렇게 나온 게 네온 벡터 룩의 실시간 액션 로그라이크 / 트윈스틱 슈터다. 손상된 거대 기계 속을 빛나는 도형이 되어 끝없이 내려가며 정화하는, 한 판 5~15분짜리 게임이다. 이 글은 그걸 어떤 기술로, 어떤 과정으로 만들고 있는지를 실제 테스트 스크린샷과 함께 정리한 중간 기록이다. (게임 고유명사는 빼고 구조와 기술 위주로 쓴다.)
⚠️ 아직 개발이 진행 중인 프로젝트다. 마감은 2026-09-04이고, 지금은 핵심 루프(MVP)가 돌아가는 위에 폴리시·확장을 얹는 단계다. 아래 스크린샷도 완성본이 아니라 개발 중 빌드를 캡처한 것이다. 완성 회고가 아니라 “만들어 가는 중간 기록”이다.
순서는 ① 제약이 곧 설계 → ② no-CRT C 스택 → ③ 전부 코드로 그리기 → ④ 손맛 → ⑤ 시스템으로 콘텐츠 늘리기 → ⑥ 사운드도 파일 0개 → ⑦ AI 멀티에이전트 하네스로 만든 게임이다.
기본 플레이 화면. 텍스처는 한 장도 없다. 벽은 네온 회로 라인, 바닥은 점 그리드, 플레이어는 흰 코어 + 글로우 다이아몬드 — 전부 도형 드로우다.
1. 제약이 곧 설계
용량 제한이 하드 제약이라, 빌드 파이프라인부터 그걸 강제하게 짰다. 빌드 배치(build.bat)는 매 빌드마다 실행파일 크기를 측정해 1.44MB 한도 대비 몇 %인지 출력한다.
link /SUBSYSTEM:WINDOWS /ENTRY:WinMainCRTStartup /NODEFAULTLIB ^
/OPT:REF /OPT:ICF /OUT:build\game.exe %OBJS% ^
kernel32.lib user32.lib gdi32.lib opengl32.lib winmm.lib
REM game.exe = !SIZE! bytes → 1.44MB 대비 !PCAP!%
재밌는 건, 에셋을 0개로 두니 1.44MB가 사실상 비제약이 된다는 점이다. 코드만으로 짜면 실행파일은 수십 KB 수준으로 떨어진다(내부 목표는 64KB 이하로 잡았다). 1.44MB의 95%를 비워둔 채 게임을 만드는 셈이다. 제약이 역설적으로 여유를 만들어 줬다.
설계 원칙도 이 한 줄에서 파생됐다.
- 에셋 0개 — 그래픽·사운드·맵·폰트 전부 코드 절차 생성.
- 핸드메이드보다 시스템형 — 손으로 짠 콘텐츠를 최소화하고, 적은 규칙의 조합으로 변주를 만든다.
- MVP가 그 자체로 완결 — 강화·보스·엘리트가 없어도 “내려가며 싸우다 죽는” 루프 하나는 완전하게.
- 결정론적 재현 — 모든 랜덤은 시드 기반(xorshift). 같은 시드면 같은 던전 → 디버깅·밸런싱이 쉽다.
2. no-CRT C + Win32 + OpenGL
스택은 C(no-CRT) + Win32 + OpenGL 즉시모드, MSVC x64다. 여기서 핵심은 no-CRT — C 런타임 라이브러리(CRT)에 의존하지 않는다는 것이다.
보통 C 프로그램은 main 이전에 CRT가 초기화 코드를 잔뜩 끼워 넣고, printf·malloc 같은 표준 라이브러리가 통째로 링크된다. 이게 다 용량이다. no-CRT는 이걸 다 떼어낸다.
int _fltused = 0; // 부동소수 사용 시 CRT 대신 직접 선언
void WinMainCRTStartup(void) { // 엔트리포인트를 직접 지정 (main 아님)
// ... 윈도우 생성, GL 컨텍스트, 메인 루프 ...
ExitProcess(0); // CRT 종료 루틴 없이 직접 종료
}
링크도 /NODEFAULTLIB로 기본 라이브러리를 끄고, 진짜 필요한 것(kernel32, user32, gdi32, opengl32, winmm)만 명시한다. memset/memcpy가 필요하면 자작한다.
메모리도 마찬가지다. 힙(malloc)을 안 쓴다. 엔티티는 전부 전역 정적 배열 풀로 잡고, active 플래그 + 스왑-삭제로 재사용한다.
Player player;
Enemy enemies[256];
Bullet bullets[1024], eBullets[1024];
Particle particles[4096];
Pickup pickups[128];
큰 버퍼만 VirtualAlloc. 메인 루프는 QueryPerformanceCounter로 dt를 누산해 시뮬레이션 120Hz 고정 타임스텝, 렌더는 vsync 60fps다. 시뮬이 렌더보다 빠르니 보간 없이 최신 상태를 그린다. sqrt는 _mm_sqrt_ss, sin/cos는 다항 근사나 256엔트리 테이블 — 표준 수학 라이브러리 없이 돌린다.
정리하면, “런타임에 기대지 않고 OS API에 직접 말을 거는” 옛날 방식이다. 그 대가로 얻는 게 초소형 실행파일과, 한 프레임에 무슨 일이 일어나는지 전부 내 손에 있다는 통제감이다.
3. 전부 코드로 그린다 — 네온 벡터 렌더링
에셋이 0개라는 건 곧 모든 픽셀이 도형 드로우의 결과라는 뜻이다. 비주얼 언어를 “짙은 배경 위에 빛나는 도형”으로 통일한 이유이기도 하다 — 도형은 코드로 그리기 쉽고, 글로우만 얹으면 작아도 화려해 보인다.
글로우는 셰이더 없이 즉시모드로 만든다. 같은 도형을 밝고 작은 코어 + 반투명하게 확대한 글로우 2~3겹으로 가산 블렌딩 중첩하면, 네온이 번지는 느낌이 난다.
- 플레이어: 흰 코어 + 컬러 글로우 다이아몬드
- 벽: 네온 회로 라인 + 어두운 채움
- 바닥: 미세 도트 그리드(기판 느낌)
- 깊이가 깊어질수록 팰릿이 5단계로 순환(시안 → 마젠타 → 앰버 → 그린 → 바이올렛)
폰트조차 파일이 없다. 3×5 비트맵 글리프를 코드에 비트로 인코딩해 네온 쿼드로 그린다. HUD의 숫자도, 화면의 짧은 단어도 전부 이 글리프를 스케일업한 것이다. OS 커서는 숨기고(ShowCursor(FALSE)) 마우스 위치에 커스텀 네온 십자선을 직접 렌더한다.
4. 손맛(juice) — 같은 메커닉을 두 배로
도형만으로 만든 게임이 밋밋하지 않으려면 체감(juice)에 투자해야 한다. 같은 “대시”라도 잔상과 쿨다운 표시가 있느냐 없느냐가 손맛을 가른다.
대시 순간. 진행 경로에 잔상이 남고, 무적 프레임이 붙고, 발밑엔 쿨다운 링이 그려진다. 이런 작은 피드백이 손맛의 8할이다.
juice 요소는 전부 수치로 파라미터화했다.
| 요소 | 사양 |
|---|---|
| 화면 셰이크 | trauma 0~1, 오프셋 = 20px × trauma² × 노이즈, 감쇠 1.5/s |
| 히트스톱 | 처치 0.03s · 피격 0.08s · 보스 사망 0.2s (시뮬 정지, 렌더는 지속) |
| 파티클 | 피격 8개 / 사망 16개+링 / 대시 잔상 / 총구 플래시 / 폭발 24개 (가산) |
| 플래시 | 피격 시 흰 플래시 + 화면 적색 비네트 펄스 |
피격하면 화면이 흔들리고(trauma +0.5), 적을 죽이면 0.03초간 시간이 멈췄다가 풀린다. 코드 몇 줄이지만 타격감이 확 산다.
5. 시스템으로 콘텐츠 늘리기
혼자 만드는 게임에서 콘텐츠를 손으로 다 짜면 스코프가 폭발한다. 그래서 적은 규칙 × 조합으로 변주를 만드는 데 집중했다.
적 — 4가지 아키타입 + 엘리트 어픽스
적은 딱 4종이다. 각자 도형과 AI가 다르다.
적 4종. 삼각형=직선 추격, 사각형=거리 유지 사격, 다이아몬드=고속 벽 반사, 육각형=죽으면 둘로 분열. 도형만으로 역할이 읽힌다.
- 삼각형 — 플레이어로 직선 추격 + 동족 분리. 가장 흔하다.
- 사각형 — 거리를 유지하며 텔레그래프 후 발사하는 포탑형.
- 다이아몬드 — 고속으로 벽을 튕기며 날아다니는 도탄형.
- 육각형 — 느리지만 죽으면 작은 적 둘로 분열한다.
여기에 엘리트 어픽스를 얹는다. 일반 적이 스폰될 때 낮은 확률로 보호막·신속·폭발사망·자가분열 같은 접두사를 받아 엘리트로 등장한다. 4종 × 어픽스 조합으로, 적은 코드로 무한히 변주된다.
전투방에 들어가면 문이 네온 배리어로 잠기고, 적 웨이브를 전멸시켜야 열린다.
강화 — 스택형 모듈 빌드
무기는 하나로 고정하고, 대신 강화 모듈로 무기를 변형해 빌드 다양성을 만든다. 데미지·연사·관통·멀티샷·유도·폭발·치명 등 커먼 14종 + 레어 6종. 대부분 무한 스택이라 멀티샷+산포보정, 관통+신속탄, 폭발+과부하 같은 창발적 빌드가 나온다. 3택1 추첨은 균등이 아니라 현재 빌드와 시너지가 강한 모듈에 가중치를 줘서, 빌드가 한 방향으로 굴러가도록 유도한다.
보스 — 2종 로테이션
몇 층마다 보스방이 나오고, 성격이 정반대인 보스 2종이 번갈아 등장한다.
구체 탄막형 보스. 방사형 탄막 링 + 레이저 스윕 + 소환. “탄막 암기”형 패턴이다. 페이즈가 내려가면 화면이 적색으로 점멸한다.
회전 프레임형 보스. 공전 세그먼트로 공간을 빼앗고 돌진으로 압박한다. 탄막형과 대비되는 “공간 압박”형이다.
하나는 탄막을 외워 피하는 타입, 다른 하나는 공전 세그먼트와 돌진으로 공간을 좁혀 오는 타입 — 같은 보스방이라도 체감이 완전히 다르다.
던전 — random-walk 절차 생성
맵도 에셋이 아니라 알고리즘이다. 9×9 논리 그리드 위에 random-walk로 방을 배치하고, 막다른 길을 완화하려 인접 방 쌍에 15% 확률로 문을 추가한다. 시작점에서 BFS 최장거리가 다음 층으로 내려가는 포트, 나머지 leaf가 강화방, 그 외가 전투방이 된다. 각 방은 다시 11×9 ~ 17×13 타일로 전개된다.
1. 중앙 셀에 시작방, frontier 추가
2. N개까지: 빈 이웃을 가진 방을 랜덤 선택 → 새 방 + 문 기록 (N = min(12, 4 + 깊이))
3. 인접 방 쌍 15% 확률로 추가 문
4. 시작점 BFS 최장거리 = 하강 포트, 임의 leaf = 강화방, 나머지 = 전투방
난이도는 깊이에 따라 적 HP·속도·수·엘리트 확률이 수식으로 올라간다. 빌드 성장이 적 HP 증가를 추격하면 더 내려가고, 정체되면 자연스럽게 죽는다. 클리어가 없는 엔드리스 구조라 “더 깊이”가 목표가 된다.
6. 사운드도 파일 0개
사운드 파일도 0바이트다. waveOut으로 44100Hz 모노 16bit PCM을 직접 합성한다. 오실레이터(사인·사각·톱니·삼각·노이즈)와 지수 감쇠 엔벨로프를 조합해 효과음을 만들고, 파라미터는 전부 코드 상수로 박아 둔다.
osc_square(ph) = ph < 0.5 ? +1 : -1
env_ad(t,a,d) = t < a ? t/a : exp(-(t-a)/d) # attack→exp decay
sweep(t,f0,f1,T) = f0 + (f1-f0)·min(1, t/T) # 주파수 스윕
# 예) 사격음 = square, 700→400Hz 스윕, 0.06s, 피치 ±5% 지터
16개 보이스를 매 샘플 합산해 [-1,1]로 클램프 후 int16으로 내보낸다. 동일 효과음이 8ms 내 중복되면 하나로 병합해 스팸을 막는다. 데이터는 0바이트, 전부 수식이다.
7. AI 멀티에이전트 하네스로 만든 게임
이 프로젝트에서 개인적으로 가장 흥미로웠던 건 개발 방식이다. 코드를 한 줄씩 직접 친 게 아니라, Claude Code 위에 개발팀을 흉내 낸 멀티에이전트 하네스를 짜서 굴렸다.
| 역할 | 하는 일 |
|---|---|
| PM 오케스트레이터 | 계획·분해·디스패치·진행 추적·핸드오프 |
| 기획 검토 | 설계 문서 red-team — 모순·누락 적발 |
| 기획 보강 | 공백·누락 데이터·상세 스펙 채우기 |
| 게임플레이 엔지니어 | no-CRT C 구현(용량 인지) |
| 아트 디렉터 | 비주얼 검토 + PNG 목업으로 사용자 컨펌 |
| QA 플레이테스터 | 실제 빌드 플레이테스트, 버그 적발 |
| 코드 리뷰어 | 정확성 + 용량 + no-CRT 준수 검증 |
| 용량 가디언 | 실행파일 바이트 측정·예산 강제·감축 제안 |
진행은 SDD(스펙 주도) 방식으로, 기획서 → 상세기획서 → 기능명세서 → 스토리보드 → 검사기준서를 먼저 동결(freeze)한 뒤 그 스펙대로 구현했다. 마일스톤은 엔진 골격(M1) → 던전(M2) → 전투(M3) → 루프 완성(M4=MVP) → 폴리시(M5) → 마감(M6) 순으로 잘랐다.
그리고 이 글의 스크린샷이 바로 그 하네스의 산출물이다. QA 에이전트가 쓰는 캡처 스크립트(capture.ps1, qa-walk.ps1, qa-death.ps1)가 실제 빌드를 실행해 창을 캡처한다 — 이동 시퀀스를 찍는 walk, 사망까지 가는 death, 임의 장면을 잡는 capture. 위 이미지들은 전부 그렇게 자동 생성된 플레이테스트 캡처다. 사람이 일일이 스크린샷을 누른 게 아니라, QA 파이프라인이 게임을 돌리며 찍어 둔 것이다.
마무리 — 제약이 끌고 가는 게임
아직 만드는 중이지만, 여기까지 와서 한 줄로 줄이면 “1.44MB라는 제약이 게임의 모든 결정을 끌고 간다”는 이야기다.
- 용량 제약 → 에셋 0개 → 전부 코드 절차 생성(그래픽·사운드·맵·폰트)
- 에셋 0개 → 네온 벡터 룩 → 도형 + 가산 글로우라는 일관된 비주얼 언어
- 핸드메이드 최소화 → 4 적 × 어픽스 / 스택 모듈 / 보스 2종 / 절차 던전이라는 시스템형 콘텐츠
- no-CRT C + 정적 풀 → 수십 KB 실행파일과 프레임 단위 통제감
제약은 선택지를 줄이지만, 줄어든 선택지가 오히려 설계를 또렷하게 만든다. 그리고 그 좁은 길을 멀티에이전트 하네스가 빠르게 밟아 나가는 중이다 — 기획을 동결하고, 구현하고, 빌드 크기를 감시하고, 플레이테스트 스크린샷까지 자동으로 남기면서. 마감(2026-09-04)까지 다듬고 채울 게 아직 많지만, 방향은 이미 이 제약이 정해 줬다. 완성되면 후속 회고로 다시 정리할 생각이다.
에셋 한 장 없이 1.44MB 안에서 — 제약은 짐이 아니라 설계의 출발점이다. (개발 진행 중)
댓글