Gameplay loop: the lobby, down the corridor, into the elevator

Vacancy

A first-person descent through the fluorescent-lit sublevels of a building that closed hours ago

Every empty room you walk into looks a little more like the last one you tried to leave. No combat. No inventory. No chase. You move, you look, you take the elevator down.

PSX / VHS aesthetic Godot 4.6 raylib 6 / C walking sim

10–20 min playthrough  ·  built twice from one design

▶ Play in your browser No install · runs on WebAssembly · the raylib / C build

The building closed hours ago

Vacancy is a PSX/VHS-styled liminal-space horror walker. It is small, quiet, and deliberately ugly in the way old hardware was ugly. The dread is built entirely out of repetition, wrongness, and restraint — there is no enemy and nothing to fight.

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 look is the engine

Left: the raw 320x240 PSX render. Right: the same frame after dither + VHS/CRT post.
Left: the raw 320×240 PSX render. Right: the same frame after the dither + VHS/CRT post chain.

320×240 buffer

The whole 3D world renders into a tiny internal buffer, then stretches up with nearest-neighbor. Native UI sits on top.

Vertex snapping

Geometry jitters to a coarse grid — the classic PlayStation wobble as you move and turn.

Affine texturing

Perspective-incorrect UVs, so textures swim and warp across surfaces exactly like 1997 hardware.

Dither / bit-crunch

An ordered-dither pass quantizes the color depth, banding the gradients into something sickly.

VHS / CRT pass

Scanlines, barrel distortion, chromatic aberration, tracking noise, and roll degrade the picture further.

Decay scales with depth

The deeper you descend, the worse the signal gets. The picture decaying is the building decaying.

Built twice, one game

The same game — same layout, systems, audio, anomalies, and ending — built from one design on two completely different foundations. A full measured writeup lives in GODOT_VS_RAYLIB.md.

 Godot portraylib port
EngineGodot 4.6 (Forward+)raylib 6.0 (from source)
LanguageGDScriptC (C11)
Worldscenes (.tscn) + nodeshand-built meshes + colliders in code
Lightingengine omni lights + shadowsper-room point lights in a shader
Audioengine buses + reverbhand-rolled mixer + Schroeder reverb
Ships as68 MB binary1.7 MB binary
scc code lines1,9712,141
Cyclomatic complexity185435

The one-number version: the two builds are nearly the same size by line count, 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.

Performance (one machine, Apple Silicon)

MetricGodotraylib 
Shipping binary68 MB1.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 fps60 fpstie (both locked)

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 work per frame. That headroom is latent on a desktop but decisive on weak hardware, at high refresh, or with far more on-screen geometry.

How it plays

MoveW A S D
LookMouse
Walk slowly (hold)Shift
Crouch (hold)Ctrl
Interact — doors, buttons, notesE
Release cursor / dismiss noteEsc

Movement is deliberately slow (~3 m/s) — this is a game you move through carefully, listening. A distance-keyed headbob keeps footsteps in sync at any speed; footstep timbre changes with the floor surface.

Under the hood

Render target320×240 SubViewport, nearest-neighbor upscale
Post chainPSX snap + affine → dither → VHS/CRT
raylib build1.7 MB binary, system libs only, any C compiler + make
Godot build68 MB self-contained, Godot 4.6 export templates
Verbsmove · look · crouch · interact — no combat, no inventory

How it ends

This 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, 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.

Take the elevator down

Play it right now in your browser — no install, no download. Or build the native raylib port, which is self-bootstrapping: it fetches and builds raylib 6.0 on first run, then the game.

▶ Play in your browser Source on GitHub ▶ Watch the walkthrough