1,474,560바이트의 제약: 1.44MB 콘테스트와 스택 선정The 1,474,560-Byte Ceiling: Contest Rules and Tech Stack

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

OUTBOUND (subtitle: *Beyond the Galaxy, One-Way*) is a solo project built for the 1.44MB GAME_DEV CONTEST. The contest rules are strict: the entire submission—executable and any runtime dependencies combined—must not exceed 1,474,560 bytes (the capacity of a standard 3.5-inch floppy disk), web wrappers (Electron, browser bundles) are prohibited, and the entry must run as a standalone native Windows binary.

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

General-purpose engines like Unity or Unreal easily exceed tens of megabytes even on an empty scene, ruling them out immediately. I chose C11 + raylib 5.5 (statically linked from source against the OpenGL 3.3 backend). Builds cross-compile from Linux to Windows x64 using zig cc 0.13.0, with a setup.sh script that bootstraps the entire toolchain and verification environment cleanly.

좌우로 밀어서 표 전체 내용을 확인하세요
항목스펙 및 제약 조건
참가 대회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% 여유)
Swipe horizontally to view the full table
ItemSpecification & Constraints
Competition1.44MB GAME_DEV CONTEST
Size Ceiling1,474,560 bytes (standalone native .exe; no external DLLs or web wrappers)
Tech StackC11 + raylib 5.5 (static link) · zig cc 0.13.0 (Linux → Win x64 cross-compile)
Asset PolicyZero external asset files (all graphics, fonts, and audio synthesized or embedded in C)
Measured Binary Size497,152 bytes (33.7% of limit used; 66.3% margin remaining)

에셋 파일 0개 정책: 코드로부터 생성하는 세계Zero External Assets: Generating Everything from Code

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

To stay well inside the 1.44 MB ceiling, I enforced a zero-asset-file policy. Neither the repository nor the build output directory (./build/) contains a single .png, .wav, or .ttf file. Every visual and audio element is either synthesized mathematically at runtime or baked into C byte arrays at build time.

한글 341자 서브셋 폰트 파이프라인341-Glyph Korean Subset Font Pipeline

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

All narrative logs and cockpit UI text are displayed in Korean. Embedding a full 2,350-syllable Korean font would consume a large fraction of the floppy budget. Instead, I wrote tools/genfont.py to parse every string literal in the C source files and extract only the exact 341 Hangul glyphs used in the game.

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

The script pulls the source font via fontsource, rasterizes each glyph with fonttools and PIL, and crops against the ink bounding box rather than the typographic ascender so glyphs never clip vertically. Because adding a new string in C without re-running genfont.py would produce missing-glyph boxes (□), a pre-commit Python checker verifies that every codepoint in src/ exists in the generated glyph table (FKR_CP).

절차적 사운드와 그래픽 생성Procedural Audio and Vector Instrument Rendering

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

Sound effects are synthesized directly in C by evaluating oscillators and amplitude envelopes in memory with zero audio files. To verify audio on a headless Linux build environment without speakers, tools/audiodump.c dumps the synthesized PCM buffers to temporary .wav files for inspection. Graphics likewise use zero textures, drawing the starfield, orbital trajectories, celestial bodies, and cockpit instruments entirely through raylib geometric primitives and procedural shaders.

OUTBOUND 스타필드 탈출 화면
실제 게임 렌더러(src/viewport.h)와 디버그 뷰 생성기로 추출한 스타필드 탈출 화면Starfield escape view rendered by the in-game viewport (`src/viewport.h`) and exported via the headless debug view generator.

5구간의 코어 루프와 조작 가시화 원칙Five-Stage Core Loop and Control Visibility Rules

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

The game loop progresses through five clear phases: Title → 4-Scene Intro → Orbital Planning (Incremental Button Aiming) → Ignition & Trajectory Playback → Celestial Capture → Real-Time Powered Descent (Thrust-Hold Toggle) → Final Transmission Log.

인트로는 게임 시작 전에 전제를 제시하되, 감상적인 서사 대신 "돌아올 몫은 싣지 않았다"와 같이 편도 비행의 제약을 숫자로만 전달하여 건조한 SF 톤을 유지했다.

The intro establishes the premise before launch, avoiding melodrama and conveying the reality of a one-way mission strictly through dry fuel and mass numbers ("No return propellant was loaded").

OUTBOUND 발사대 점화 화면
지표면 발사대 점화 및 탈출 궤도 계산 장면Surface launchpad ignition and escape trajectory calculation view.

하강 및 착륙: 물리와 오토파일럿 설계Powered Descent and Landing: Physics and Autopilot Design

프로젝트에서 가장 많은 시행착오를 겪은 지점은 천체 표면으로의 실시간 하강 및 착륙(Landing) 시뮬레이션이었다. 천체의 중력과 추력의 밸런스를 맞추는 과정에서 여러 물리적 한계와 버그가 드러났다.

The subsystem that required the most iteration was real-time powered descent and surface landing. Balancing celestial surface gravity against discrete thruster control exposed several numerical and physical traps.

물리와 반응 시간의 딜레마 (슬로모션)Physics vs. Human Reaction Time (0.45× Time Dilation)

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

In the initial physical model, planetary surface gravity was 1.7× the maximum safe touchdown speed per second. Running powered descent at 1.0× real time meant an unassisted lander exceeded structural touchdown limits in 0.33 seconds—faster than human reaction time. Rather than fudging gravity constants, entering the descent gate applies a 0.45× time scale so the underlying physics remain consistent while giving the player time to read instruments.

#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
#define DESC_START_R    2.2     // Descent entry altitude as a multiple of planet radius
#define DESC_THRUST_G   1.8     // Thruster acceleration relative to surface gravity
#define DESC_LAND_FRAC  0.18    // Max safe landing speed as a fraction of escape velocity
#define DESC_TIME_SCALE 0.45    // Slow-motion factor during powered descent
#define HOVER_TARGET       0.45
#define HOVER_BRAKE_SAFETY 0.55

호버 듀티비 수렴 버그와 히스테리시스 밴드Hover Duty-Cycle Convergence Bug and Hysteresis Band

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

The lander's main thruster is binary (On/Off) with thrust fixed at 1.8× surface gravity. When I initially wrote the descent assist as a standard proportional (P) controller, the on/off duty cycle naturally converged to 1 / 1.8—hovering stationary at 8 meters altitude for 115 seconds until propellant ran dry, then dropping at −40 m/s.

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

To prevent mid-air hovering, I replaced proportional switching with a hysteresis band between turn-on and turn-off velocity thresholds, and prioritized vertical gravity cancellation before allocating any remaining thrust budget (budget) to horizontal braking:

// 추력은 켜짐/꺼짐뿐이고 세기가 표면중력의 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;
// Thrust is binary On/Off at 1.8x surface gravity. A naive P-controller
// converges its duty cycle to stationary hover at altitude 8m until fuel exhaustion.
int engage = *thrust;
if (vv < -vdes)             engage = 1;
else if (vv > -vdes * 0.5)  engage = 0;

// Vertical support comes first; only surplus thrust budget goes to horizontal braking.
double budget = T * T - aUp * aUp;
budget = (budget > 0.0) ? sqrt(budget) : 0.0;

고도 비례 목표 하강률과 각운동량 보존Sqrt-Altitude Descent Rate and Angular Momentum Conservation

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

Using a constant target descent rate causes the autopilot to brake far too early at high altitude, wasting propellant. Instead, the target descent rate (vdes) scales with the square root of altitude (sqrt(2 · aNet · alt)) along the kinematic stopping curve:

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가 급격히 증가했다. 결국 하강 인계 고도는 난이도 다이얼이 될 수 없으며, 진입 시점의 초기 접선 속도야말로 착륙 난이도를 좌우하는 핵심 변수임을 수식으로 확인하고 수치를 확정했다.

Another important physical finding came from conservation of angular momentum (L = r · v_h). As the lander descends and radial distance r shrinks, horizontal tangential speed v_h climbs inversely. That proved mathematically that entry altitude cannot be used as a difficulty dial—initial tangential velocity at the descent gate is the true variable governing landing difficulty.

OUTBOUND 천체 접근 화면
목표 천체 접근 및 궤도 진입 렌더링 화면Target body approach and orbital insertion rendering.

실제로 터진 11가지 함정과 트러블슈팅11 Bugs Caught and Fixed During Development

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

Writing orbital physics, custom font rasterization, and immediate-mode UI in plain C11 without a commercial engine surfaced 11 notable bugs:

좌우로 밀어서 표 전체 내용을 확인하세요
#증상원인 및 해결
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 재실행 누락 방지를 위해 리터럴 코드포인트 검사 스크립트 강제.
Swipe horizontally to view the full table
#SymptomRoot Cause & Fix
1Game unwinnable in early main.cSafe touchdown speed threshold was set lower than what thruster deceleration could achieve; recalibrated physical constants.
2Orbits diverged and flung spacecraft away at large dtSymplectic Euler lost stability on coarse time steps; subdivided integration via physSubCount().
3High-speed spacecraft tunneled through planetsCollision detection was running outside the physics substep loop; moved inside the substep loop.
4Viewport jittered in inertial world coordinatesShifted coordinate origin to Leg.frame relative coordinates per mission leg to preserve floating-point precision.
5Aiming felt like guesswork when trajectory showed 'No Intercept'Added pericenter (closest approach) readout, target orbit markers, and fine auto-trim.
6Aiming parameters reset to zero upon entering WAIT stateFixed state-transition ordering bug where legReset fired after player input.
7Top and bottom strokes of Korean glyphs clippedPIL default anchor used typographic ascender; switched genfont.py to crop by ink bounding box.
8Sound effects triggered dozens of times per frameAudio trigger calls were sitting inside the draw loop; moved to discrete state-transition edges.
9Launches at t ≠ 0 falsely registered immediate TERRA collisionCollision check read planet positions before advancing ephemeris time t.
10AUTO aim solver got stuck in local minimaFlat cost plateaus trapped 4-neighbor local search; replaced with coarse-to-fine global grid search.
11Newly added dialogue strings rendered as □ boxesEnforced pre-commit codepoint verifier script so genfont.py is never skipped.

화면 없는 환경에서의 무결성 검증Headless Verification Without a Display

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

Because the build environment was a headless Linux VM with no GUI display, I could not playtest visually on the build machine. Instead, I built five automated verification tools:

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

The main engineering takeaway was simple: never trust paper math without running it through the actual discrete-time integrator. In a simulation stepping at finite dt, analytical orbital formulas accumulate integration drift. Running ./solve.sh headlessly after every constant change ensured every mission leg remained physically solvable.

실측 용량 보고현재 OUTBOUND의 최종 빌드 용량은 497,152 바이트 / 1,474,560 바이트 (33.7%)로, 플로피 디스크 용량 한도 대비 66.3%의 안전 마진을 확보한 상태다. build.sh가 매 빌드마다 바이트 단위 잔량을 체크하고 초과 시 빌드를 실패시키도록 안전장치가 작동하고 있다.
Final Binary Size ReportThe final Windows x64 executable of OUTBOUND measures 497,152 bytes out of 1,474,560 bytes (33.7%), leaving a 66.3% safety margin under the 3.5-inch floppy limit. build.sh prints the exact byte count on every build and fails the build automatically if the limit is exceeded.