A short first-person descent through the fluorescent-lit sublevels of a
building that closed hours ago,
where every empty room you walk into looks a
little more like the last one you tried to leave.
▶ Play it in your browser — the raylib / C build, compiled to WebAssembly. No install.
▶ Watch the full walkthrough (MP4) · Built twice — once in Godot, once in raylib 6 / C · Engine comparison →
Vacancy is a PSX/VHS-styled liminal-space horror walker. It is small, quiet, and deliberately ugly in the way old hardware was ugly. There is no combat, no inventory, no enemy, and no chase. You move, look, crouch, interact — and you take the elevator down. The dread is built entirely out of repetition, wrongness, and restraint. A full playthrough runs 10–20 minutes.
Each descent opens onto another sublevel that is almost the floor you just left — the same corridors, the same doors, the same hum — but something is off. A light that was steady now stutters. A chair has turned to face the wrong way. A door that was shut stands ajar. None of it is loud. Each individual wrongness is small enough to talk yourself out of. They accumulate.
The most frightening thing the game does is, occasionally, on arrival at a new floor, cut every sound to total silence for two seconds.
The whole 3D world is rendered the way a 1997 console would have, and the jank is the art direction. Everything renders into a 320×240 buffer with PSX vertex snapping (geometry jitters to a coarse grid) and affine texture mapping (perspective-incorrect UVs, so textures swim). Then a two-stage post chain — an ordered-dither / bit-crunch pass, then a VHS/CRT pass (scanlines, barrel distortion, chromatic aberration, tracking noise, roll) — degrades the picture further the deeper you go. The picture decaying is the building decaying.
This repo contains the same game built twice, from the same design, with the same layout, systems, audio, anomalies, and ending — on two completely different foundations:
godot-port/ |
raylib-port/ |
|
|---|---|---|
| Engine | Godot 4.6 (Forward+) | raylib 6.0 (built from source) |
| Language | GDScript | C (C11) |
| World | scenes (.tscn) + nodes |
hand-built meshes + colliders in code |
| Lighting | engine omni lights + shadows | per-room point lights in a shader |
| Audio | engine buses + reverb | hand-rolled mixer + Schroeder reverb |
| Ships as | 68 MB self-contained binary | 1.7 MB binary, system libs only |
| Build | Godot editor + export templates | any C compiler + make |
Both were built with Claude. A detailed, measured writeup — line counts, scc
complexity, build cost, performance, binary size, and where each engine helps or
fights you — lives in GODOT_VS_RAYLIB.md.
The one-number version: by
sccthe two builds are nearly the same size (1,971 vs 2,141 code lines) but the C port carries ~2.4× the cyclomatic complexity (185 → 435) — because in raylib you write the collision, lighting, audio mixing, and state machines the engine otherwise hands you for free.
All figures measured on one machine (Apple Silicon, macOS); full method and caveats are in GODOT_VS_RAYLIB.md.
| Metric | Godot | raylib | |
|---|---|---|---|
| Shipping binary | 68 MB | 1.7 MB | ~40× smaller |
| Cold start → first frame | ~0.6 s | ~0.4 s | ~1.6× faster |
| Peak memory (max RSS) | ~235 MB | ~90 MB | ~2.6× less |
| CPU per rendered frame | ~2.3 ms | ~0.35 ms | ~6–7× lighter |
| Frame rate (this scene) | 60 fps locked | 60 fps locked | tie |
| CPU % at locked 60 fps | ~11–12% | ~11–12% | tie |
The honest read: at a desktop 60 fps lock the two are a wash on frame rate and CPU% — both hold 60 trivially. raylib's wins are all in the floor, not the ceiling: a ~40× smaller binary, ~⅓ the memory, a faster start, and ~6–7× less CPU work per frame. That per-frame headroom is latent — invisible at 60 fps on a desktop, but decisive on weak/embedded hardware, at high refresh rates, or with far more on-screen geometry. (The eye-catching "~1,600 vs ~78 fps uncapped" is a macOS compositor artifact — raylib bypasses the display-refresh cap, Godot honors it — not a throughput result; see the comparison doc.)
| Input | Action |
|---|---|
| W A S D | move |
| Mouse | look |
| Shift (hold) | walk slowly |
| Ctrl (hold) | crouch |
| E | interact — open/close doors, press elevator buttons, read notes |
| Esc | release the mouse cursor; dismiss a note |
It's a walking sim. Movement is deliberately slow (~3 m/s) — this is a game you are meant to move through carefully, listening. A subtle distance-keyed headbob keeps footsteps in sync at any speed; footstep timbre changes with the floor surface.
raylib / C (tiny, self-bootstrapping — fetches & builds raylib on first run):
cd raylib-port
./build.sh # clones + builds raylib 6.0, then the game
./build/vacancy # run from this directoryWeb (WebAssembly) — the same C port, built with Emscripten and live at vacancy.ximg.app/play:
cd raylib-port
./build-web.sh # builds raylib for WebGL2 + the game into web/
# then serve web/ over HTTP and open it; see raylib-port/README.mdGodot: open godot-port/ in Godot 4.6 and press Play, or:
cd godot-port && ./run.shThis spoils the ending — which is the point of the game. Click to expand.
After enough descents (seven, by default) the doors open not onto another sublevel but onto the original lobby — the one you started in. Except it's empty, dimmed, and lit a sickly green; one tube is dead and another flickers. The desk note has changed. The front doors — locked all night — are now unlocked.
You walk to them, you push them open, and you are standing back at the elevator. The exit leads inward. There is no way out. You read the last note, the screen fades, a title holds for a moment in the dark, and that's it. No reveal, no creature, no explanation. The horror is that there's no exit.
.
├── README.md this file
├── SPEC.md the original design brief both builds were made from
├── GODOT_VS_RAYLIB.md measured engine comparison
├── media/ screenshots + walkthrough capture
├── godot-port/ the Godot 4.6 / GDScript implementation
└── raylib-port/ the raylib 6.0 / C implementation
Each implementation has its own README with build instructions, a file-by-file map, and its dev tooling (telemetry, dev flags, self-tests).
Restraint over spectacle. Empty, quiet, fluorescent, slightly wrong. The guiding rule throughout: if a feature makes the game louder, it's probably wrong for Vacancy. The fear is supposed to come from an ordinary place made subtly uninhabitable, and from the slow certainty that the way out isn't where you left it.





