Horde
A crowd that never stops arriving, a gun that aims itself, and five minutes
you are not going to get — the same apps/horde that builds a
native binary, running here.
controls
Move, and pick. The shooting is not your problem.
- Walk W A S D, the arrows, or a thumb anywhere on the field
- Fire automatic — the nearest enemy in range, always
- Take an upgrade 1 2 3, or press one
- Start a run, or begin again after one ends R, Space, or the button on screen
- Pause — and from it, fullscreen and the debug panel the PAUSE button, top right
- Pause and resume Esc
- Fullscreen F11
- Debug overlay F3
- Debug console `
- Pause, fullscreen and the debug overlay keys — unless a control for them is listed above
- Debug console, and its on-screen keyboard the CONSOLE button, once you have touched the canvas
why a fourth game
To find out what one tick costs per live body.
Breakout spawns its world once. Flappy runs a treadmill. Asteroids churns hard and holds fifty things. This one holds a thousand and wants ten thousand, and every one of them steers, asks the broadphase what it is standing next to, and is drawn. It is the sample that exists to be measured rather than played: a benchmark in a game costume.
The measurement is written down in
the sample's plan, with the conditions each number was taken under. The short version, on a
Radeon RX 7900 XTX: the render side carries ten thousand with room
to spare — a frame costs about a tenth of a millisecond of CPU whether the
field holds one enemy or ten thousand — and the simulation is what
breaks, because separation is one broadphase query per body per tick. It is
spread over a job pool now — steering goes through par_for, and
web/run-horde-threads-e2e.sh asserts a chunk really ran on a
Web Worker — but not on this page: GitHub Pages cannot send the COOP/COEP
headers a shared memory needs, so the published build has no workers to
announce and every chunk runs inline. That degradation is itself one of the
gate's red checks.
what is actually running
The native sample, with four differences the browser forces.
Start-up is polled across several frames, because
requestDevice is a promise and the main thread cannot block on
it. The frame loop is driven by requestAnimationFrame out here
rather than by a loop inside the engine. The clock is the
browser's, because wasm32-unknown-unknown has no
Instant. And your longest run lives in the Origin Private File
System instead of ~/.config.
Everything else — the ECS, the fixed-tick server, the physics, the render graph, the spatial audio — is the same code the native build runs. The whole crowd is one draw call: the player, all three enemy kinds and the experience gems share a single sprite sheet at a single frame size, so the number of batches a frame costs does not move when the field grows. Press F3 and the panel will tell you how many of the field survived the view cull and how many draws it took.
console.log, so log lines are queued
inside wasm and drained by the page each frame. crcbl.audio(),
crcbl.saves() and crcbl.logLevel(4) are there too.