MassDriver
A launcher that fires pooled bodies on a timer. In Space Golf it is the rock chute.
What it does
Every interval seconds the driver takes a body from its pool, puts it muzzleOffset ahead of itself and sends it along direction at speed. It never creates a body: world.acquire(pool, { x, y, z }) hands back a parked slot already placed at the muzzle, and the driver only sets that body's GravityBody velocity. A full pool recycles its oldest body. A pool name with nothing registered under it fires nothing and emits massdriver.jammed.
Each shot is tilted inside a cone of half-angle spread, so the stream fans out a little and a perfect line through a gap is not a perfect answer. The tilt comes from a small seeded generator whose state lives with the driver and restarts from seed each time the driver enters play. A level fires the same stream every time it loads, and two drivers with one seed fire the same shots.
The clock expects a player to change it. Change interval mid-wait and the wait already running is scaled by the same ratio, so "slower" lands on the next rock, not one whole old interval later. A long frame fires once rather than a backlog of missed shots. Switching active back on restarts the clock at firstDelay.
Nothing in it is golf. A mortar or a waterfall of crates is the same component with other numbers.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add MassDriver
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
The rock blueprint needs a GravityBody for the shot to move. In Space Golf the chute is a pool blueprint too, with active: false and firstDelay: 0. The director places it for each level and switches it on at GO, so the first rock leaves on that frame.
Props
pool(string, default"rock") — the pool bodies are taken from.interval(number, default2.4) — seconds between automatic shots.speed(number, default10) — launch speed in units per second.direction([x, y, z], default[1, 0, 0]) — launch direction, any length. Zero or NaN falls back to +X.muzzleOffset(number, default1.6) — how far ahead of the driver, alongdirection, a body appears.spread(number, default0.015) — half-angle of the launch cone in radians. 0 fires dead straight.active(boolean, defaulttrue) — fire on the timer.firstDelay(number, default0.6) — seconds before the first shot after the driver enters play or is switched on.seed(number, default0x5eed) — seeds the spread.
Events
Emits:
massdriver.launched— a body left the muzzle:{ driverId, entityId, x, y, z, vx, vy, vz }.massdriver.jammed— there was no pool to take from:{ driverId, pool }.
Listens:
massdriver.fire— fire once now, timer or not:{ driverId? }. Leave out the id to fire every driver.massdriver.active.set— start or stop the timer:{ driverId?, active }. A bare payload means on.
Dependencies
MeshPrimitive— the driver's position, and the barrel it turns.GravityBody— on the pooled bodies. The driver sets its velocity.
Notes
- The driver owns its mesh's rotation. It turns local +Y, the long axis of a cylinder or capsule, onto
direction, and replaces anyrotationyou author. - It runs before GravityBody, so a rock fired this frame also moves this frame.
- An inactive driver still answers
massdriver.fire. A parked driver, or a pool blueprint, answers nothing. - A body with no GravityBody still appears at the muzzle, and
massdriver.launchedstill carries the velocity, so another mover can pick it up.
































