1,474,560바이트의 제약: 1.44MB 콘테스트와 스택 선정

‘OUTBOUND’(부제: 은하 밖으로, 편도)는 1.44MB GAME_DEV CONTEST 출품을 목표로 시작한 1인 개발 프로젝트다. 콘테스트의 규칙은 명확하고 엄격하다. 실행파일과 런타임을 모두 포함하여 정확히 1,474,560바이트(고전 3.5인치 플로피 디스크 1장 용량) 이하여야 하며, 웹 기술(Electron, 브라우저 번들 등)은 전면 금지되고 윈도우 환경에서 단독 실행되는 네이티브 바이너리 제출이 필수 조건이다.

유니티나 언리얼 같은 상용 엔진은 빈 씬만 빌드해도 수십 메가바이트를 가볍게 넘기기 때문에 후보군에 둘 수 없었다. 결국 모든 레이어를 직접 제어할 수 있는 C11 + raylib 5.5(OpenGL 3.3 백엔드, 소스 정적 링크)를 최종 스택으로 선정했다. 빌드는 리눅스 환경에서 zig cc 0.13.0을 이용해 Windows x64 크로스컴파일을 수행하도록 구성했으며, 작업 세션마다 setup.sh를 통해 약 10분 만에 환경을 새로 부트스트랩하는 파이프라인을 갖췄다.

항목스펙 및 제약 조건
참가 대회1.44MB GAME_DEV CONTEST
용량 상한1,474,560 바이트 (독립 실행파일 필수, 외부 DLL/웹 기술 금지)
개발 스택C11 + raylib 5.5 (정적 링크) · zig cc 0.13.0 (Linux → Win x64 크로스컴파일)
리소스 정책외부 에셋 파일 0개 (그래픽·폰트·오디오 전부 코드로 생성/임베드)
실측 용량497,152 바이트 (용량 한도 대비 33.7% 사용, 66.3% 여유)

에셋 파일 0개 정책: 코드로부터 생성하는 세계

용량 한도 1.44MB를 지키기 위해 세운 절대 원칙은 '에셋 파일 0개'였다. 프로젝트 저장소와 빌드 산출물 폴더(./build/) 안에는 PNG, WAV, TTF 같은 외부 리소스 파일이 단 하나도 존재하지 않는다. 모든 시각·청각 요소를 런타임에 수학적으로 합성하거나 빌드 타임에 C 바이트 배열로 구워 넣었다.

한글 341자 서브셋 폰트 파이프라인

게임의 모든 스토리와 UI는 한글로 출력된다. 하지만 한글 완성형 2,350자 전체 폰트 데이터는 콘테스트 용량의 상당 부분을 잠식한다. 이를 해결하기 위해 소스코드 내 문자열 리터럴을 파싱하여 실제 게임에 등장하는 341자만 정확히 추출하는 파이프라인(tools/genfont.py)을 구축했다.

npm fontsource에서 원본 폰트를 받아 fonttools와 PIL을 거치며, 기준점을 어센더가 아닌 잉크 경계(Ink Bounding Box) 기준으로 크롭하여 글자가 위아래로 잘리는 현상을 막았다. 만약 코드에 새로운 대사가 추가되었는데 폰트 스크립트를 재실행하지 않으면 즉시 물음표(?)나 네모(□)로 깨지기 때문에, 커밋 전 소스 리터럴의 코드포인트가 생성된 글리프 집합(FKR_CP)에 포함되는지 파이썬 검사기로 자동 검증하도록 강제했다.

절차적 사운드와 그래픽 생성

효과음 역시 오디오 파일 없이 C 코드로 오실레이터와 엔벨로프를 직접 계산해 합성한다. GUI 창을 띄울 수 없는 빌드 머신 환경에서는 tools/audiodump.c를 통해 메모리 상의 파형을 임시 WAV 파일로 덤프하여 귀로 직접 확인하는 청음 검증 과정을 거쳤다. 그래픽 또한 텍스처 파일 없이 raylib의 기본 도형 함수와 절차적 알고리즘으로 우주 배경과 계기판, 천체를 실시간 렌더링한다.

OUTBOUND 스타필드 탈출 화면
실제 게임 렌더러(src/viewport.h)와 디버그 뷰 생성기로 추출한 스타필드 탈출 화면

5구간의 코어 루프와 조작 가시화 원칙

게임의 루프는 5단계로 명료하게 구성된다: 타이틀 → 인트로 4장면 → 궤도 계획(버튼 조준) → 점화 및 궤적 재생 → 천체 포획 → 실시간 하강 착륙(추진 고정 토글) → 최종 통신 로그.

인트로는 언더테일 스타일로 게임 시작 전에 전제를 제시하되, 감상적인 서사 대신 "돌아올 몫은 싣지 않았다"와 같이 편도 비행의 제약을 철저히 숫자로만 전달하여 건조하고 묵직한 SF 톤을 완성했다.

OUTBOUND 발사대 점화 화면
지표면 발사대 점화 및 탈출 궤도 계산 장면

하강 및 착륙: 가장 오래 헤맸던 물리와 오토파일럿

프로젝트에서 가장 많은 난관에 부딪혔던 지점은 천체 표면으로의 실시간 하강 및 착륙(Landing) 시뮬레이션이었다. 천체의 중력과 추력의 밸런스를 맞추는 과정에서 여러 물리적 한계와 버그가 터져 나왔다.

물리와 반응 시간의 딜레마 (슬로모션)

계산 결과, 천체의 표면중력이 안전 착륙 허용 속도의 1.7배에 달했다. 1.0배속 실시간으로 돌리면 플레이어가 아무 키도 누르지 않고 가만히 있어도 0.33초 만에 착륙 속도 한계를 초과해 박살난다. 물리 법칙을 깨뜨리지 않고 인간의 인지 및 반응 시간을 벌어주기 위해, 하강 구간에 진입하면 물리 시뮬레이션에 0.45배 슬로모션을 적용했다.

#define DESC_START_R    2.2     // 하강 시작 고도 = 반지름의 배수
#define DESC_THRUST_G   1.8     // 표면중력 대비 추력
#define DESC_LAND_FRAC  0.18    // 착륙 허용 속도 = 탈출속도의 이 비율
#define DESC_TIME_SCALE 0.45    // 하강 구간만 슬로모션
#define HOVER_TARGET       0.45
#define HOVER_BRAKE_SAFETY 0.55

호버 듀티비 수렴 버그와 히스테리시스 밴드

착륙선의 추력은 On/Off 2단계뿐이며, 세기는 표면중력의 1.8배로 고정되어 있었다. 오토파일럿을 일반적인 비례제어(P제어)로 작성하자 치명적인 문제가 발생했다. On/Off 듀티비가 표면중력과 딱 맞아떨어지는 호버링 상태에 저절로 수렴하면서, 고도 8m 상공에 영원히 멈춰 떠 있다가 연료가 고갈되어(실측 115초 공중 체류 후 초속 -40m로 추락) 폭발하는 현상이었다.

이 문제를 해결하기 위해 켜짐/꺼짐 임계값 사이에 간격을 두는 히스테리시스 밴드(Hysteresis Band) 제어를 도입했다. 또한 수직 낙하를 막는 수직 지지를 최우선으로 두고, 남는 잉여 추력 예산(budget)만 수평 제동에 투입하도록 분리했다.

// 추력은 켜짐/꺼짐뿐이고 세기가 표면중력의 1.8배다. 비례제어로 짜면
// 듀티비가 저절로 호버에 수렴해 고도 8에서 영원히 떠 있는다(실측).
int engage = *thrust;
if (vv < -vdes)             engage = 1;
else if (vv > -vdes * 0.5)  engage = 0;

// 수직 지지가 우선. 남는 추력만 수평 제동에 쓴다.
double budget = T * T - aUp * aUp;
budget = (budget > 0.0) ? sqrt(budget) : 0.0;

고도 비례 목표 하강률과 각운동량 보존

목표 하강률을 일정한 상수로 설정하면 높은 고도에서부터 조기 호버링을 시작해 연료가 고갈된다. 따라서 고도의 제곱근에 비례하여 안전 정지 거리를 계산하는 방식으로 목표 하강률(vdes)을 동적 연산했다.

double vdes = sqrt(2.0 * aNet * alt) * HOVER_BRAKE_SAFETY;
if (vdes < lv * HOVER_TARGET) vdes = lv * HOVER_TARGET;

또 하나의 중요한 물리적 발견은 각운동량 보존(L = r · v_h)이었다. 착륙선이 고도를 낮출수록 반지름 r이 줄어들어 수평 속도 v_h가 기하급수적으로 폭증했다. 결국 하강 인계 고도는 난이도 다이얼이 될 수 없으며, 진입 시점의 초기 접선 속도야말로 착륙 난이도를 좌우하는 진짜 다이얼임을 수학적으로 증명하고 수치를 확정했다.

OUTBOUND 천체 접근 화면
목표 천체 접근 및 궤도 진입 렌더링 화면

실제로 터진 11가지 함정과 트러블슈팅

상용 엔진의 보호막 없이 순수 C 언어로 물리 엔진, 렌더링, 폰트 파이프라인을 직접 다루다 보니 수많은 엣지 케이스와 버그가 발생했다. 실제 개발 과정에서 마주쳐 해결했던 11가지 기록이다.

#증상원인 및 해결
1초기 main.c에서 게임 클리어 불가착륙 허용 속도가 탈출 속도보다 낮게 설정되어 있었음. 물리 상수 현실화.
2큰 dt에서 궤도가 튕겨 나가 발산심플렉틱 오일러가 큰 시간 스텝을 버티지 못함. physSubCount() 서브스텝 세분화.
3고속 이동 시 천체 충돌 터널링 발생충돌 검사를 서브스텝 루프 외부에서 실행하고 있었음. 루프 내부로 이동.
4관성계에서 화면 렌더링 깨짐비행 구간(Leg)마다 Leg.frame 기준 상대좌표계로 원점 전환하여 정밀도 확보.
5궤도 '교점 없음' 상태에서 조준이 운에 의존천체와의 최근접 거리(Pericenter), 궤도 마커, 미세 자동 보정 기능 추가.
6대기(WAIT) 구간 진입 시 조준값 초기화legReset 호출 타이밍이 상태 전이와 꼬여 있던 버그 수정.
7한글 폰트 렌더링 시 글자 상하단 잘림PIL 기본 앵커가 어센더 기준이었음. 잉크 경계(Ink Bounding Box) 크롭으로 수정.
8효과음이 1프레임에 수십 번 중복 재생그리기 루틴 내부에서 오디오를 호출하고 있었음. 상태 전이 이벤트 시점으로 분리.
9t≠0 시점 발사가 전부 'TERRA 충돌'로 오판충돌 판정 코드가 시간 전진 전의 이전 천체 좌표를 참조하고 있었음.
10AUTO 자동 조준 알고리즘이 갇혀서 탈출 불가비용 지형이 평탄해 4방향 국소 탐색이 고립됨. 전역 그리드 탐색으로 교체.
11새 문자열 추가 시 글자가 네모(□)로 출력genfont.py 재실행 누락 방지를 위해 리터럴 코드포인트 검사 스크립트 강제.

화면 없는 환경에서의 무결성 검증

빌드 머신이 GUI 디스플레이를 지원하지 않는 헤드리스 Linux VM이었기 때문에, 개발자가 화면을 보며 눈대중으로 테스트하는 방식은 불가능했다. 대신 철저한 자동화 검증 스크립트로 눈과 귀를 대신했다.

이 과정에서 얻은 핵심 엔지니어링 교훈은 "문서상으로 계산한 이론적 수치를 시뮬레이션 없이 맹신하지 않는다"였다. 이산 시간 스텝(dt)을 거치는 물리 시뮬레이션 환경에서는 아무리 정교하게 계산된 기획 수치라도 수치 적분 과정에서 오차가 누적될 수밖에 없다. 따라서 모든 물리 파라미터는 코드에 반영하기 전 반드시 헤드리스 수치 적분(solve.sh)을 통해 실제 플레이 가능 여부를 사전에 검증하고 보정해야 한다.

실측 용량 보고현재 OUTBOUND의 최종 빌드 용량은 497,152 바이트 / 1,474,560 바이트 (33.7%)로, 플로피 디스크 용량 한도 대비 66.3%의 안전 마진을 확보한 상태다. build.sh가 매 빌드마다 바이트 단위 잔량을 체크하고 초과 시 빌드를 실패시키도록 안전장치가 작동하고 있다.