logo

Babylon.js Market

RoadSpawner

vehicular · vehicle-combat

RoadSpawner

1
Unlock

Install with the CLI:

bjs download RoadSpawner

RoadSpawner

Traffic that arrives on the road ahead of you and goes back in the box once it's behind you.

What it does

A spawner bolted to a pad can pick a spawn point once and use it forever, because the pad never moves. A road spawner can't: the only stretch of road worth putting a car on is the one the player is about to reach, and that stretch moves at whatever speed they're doing. So every distance here is measured off the player's live Z. Cars go in at playerZ + spawnAhead and come back out below playerZ - releaseBehind, and the whole window slides along with the car.

RoadSpawnerComponent is the shape of that window plus the lane rules; RoadSpawnerSystem runs the frame. It creates nothing. A new car is a pool.spawn naming trafficPool or enemyPool, and a retired one is a pool.release — the generic Pool blueprint owns the entities either way.

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.

Fastest path

bjs download scene RoadSpawner

Pulls this exact scene into your project and runs it — no copy-paste. Add --all to grab the components and assets it needs too.

Or build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add RoadSpawner
    bjs download RoadSpawner

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "Car": {
    "tags": ["player"],
    "components": {
      "MeshPrimitive": {
        "primitive": "box",
        "width": 1.8,
        "height": 1,
        "depth": 4,
        "position": [0, 0.5, 0]
      },
      "CarDrive": { "maxSpeed": 34 }
    }
  },
  "TrafficBlueprint": {
    "components": {
      "Pool": { "name": "traffic", "size": 12 },
      "MeshPrimitive": {
        "primitive": "box",
        "width": 1.8,
        "height": 1.2,
        "depth": 4.2,
        "color": [0.2, 0.45, 0.7]
      }
    }
  },
  "EnemyBlueprint": {
    "components": {
      "Pool": { "name": "enemy", "size": 6 },
      "MeshPrimitive": {
        "primitive": "box",
        "width": 1.9,
        "height": 1.1,
        "depth": 4.4,
        "color": [0.7, 0.15, 0.15]
      }
    }
  },
  "Traffic": {
    "components": {
      "RoadSpawner": {
        "player": "Car",
        "interval": 1.1,
        "maxAlive": 8,
        "spawnAhead": 90,
        "releaseBehind": 40,
        "laneCount": 3,
        "laneSpacing": 3.2,
        "enemyChance": 0.3
      }
    }
  }
}

The spawner goes on its own marker entity — it has no mesh and needs none. The two blueprint entities are what make "traffic" and "enemy" real names; without a Pool of that name in the scene the request is dropped and the road stays empty.

The window rides with the player

Two numbers and a live position read are the whole of it:

TypeScript
export function releaseReason(carZ, playerZ, p) {
  if (carZ < releaseBehindZ(playerZ, p)) return 'behind';
  if (carZ > releaseAheadZ(playerZ, p)) return 'ahead';
  return null;
}

behind is the one that fires all day — a car the player has driven past. ahead is the safety net: traffic faster than the player, or a player who turned round, would otherwise drift away forever holding a pool slot that never comes back. Set releaseAhead to 0 and that net is off (releaseAheadZ returns Infinity), which is only safe if nothing on the road can outrun you.

The player's Z comes off the renderer every frame, never from MeshPrimitiveComponent.position. A car driven by CarDrive, a physics body or a respawn moves without anything writing back to that array, so reading it would pin the window to wherever the car was authored and retire nothing, ever.

Lanes are what stop a pile-up, not the timer

interval decides how often the spawner tries. Whether it succeeds is a lane question: a lane is offered a car only when its frontmost live one is already minLaneGap clear of the new spawn point. With every lane blocked nothing goes out, the timer stays hot, and the road fills as fast as it can and no faster.

The scan starts on a random lane and wraps, which matters more than it sounds. Scanning from lane 0 every time gives the inside lane nearly every car; starting anywhere spreads them.

Lane centres are laid out symmetrically about laneOffset — three lanes at 3.2 apart put the middle one on the centreline and the outer two at ±3.2, which fits inside RoadTreadmill's default ten-wide road. If that road bends, copy its curveAmplitude and curveWavelength onto the spawner as well: both use the same sine of world Z, so the cars land on the same asphalt the slabs did. Leave them at 0 and the spawner assumes a straight road.

Props

  • spawning (boolean, default true) — false parks the stream. Live cars still retire.
  • interval (number, default 1.1) — seconds between attempts.
  • maxAlive (number, default 8) — live cap for this spawner. The pool's size is the harder ceiling.
  • spawnAhead (number, default 90) — how far up the road a car goes in.
  • spawnAheadJitter (number, default 40) — extra random depth on top, so cars don't arrive in ranks.
  • releaseBehind (number, default 40) — distance behind the player at which a car goes back to the pool.
  • releaseAhead (number, default 200) — same from the front. 0 disables it.
  • laneCount (number, default 3) — lanes across the carriageway.
  • laneSpacing (number, default 3.2) — distance between two lane centres.
  • laneOffset (number, default 0) — sideways shift of the whole carriageway.
  • minLaneGap (number, default 24) — clear road a lane needs in front of its frontmost car.
  • spawnY (number, default 0.5) — height a car is placed at. Whatever drives it owns Y from then on.
  • enemyChance (number, default 0.3) — share of spawns that come from enemyPool, 0..1.
  • trafficSpeed / enemySpeed (number, default 18 / 30) — speed handed over on the spawn payload.
  • speedJitter (number, default 0.15) — random spread on that speed, as a fraction.
  • curveAmplitude / curveWavelength (number, default 0 / 120) — the road's bend, mirrored.
  • seed (number, default 1) — same seed, same traffic, every run.
  • player (string, default null) — entity the window rides with. Null anchors it on world Z 0.
  • trafficPool / enemyPool (string, default "traffic" / "enemy") — Pool blueprint names.

Events

  • emits pool.spawn{ pool, x, y, z, spawnerId, speed, lane, enemy }. The extra keys ride through the pool untouched and arrive on pool.acquired, which is where a game applies them to the car it got back.
  • emits pool.release{ entityId } — when a car leaves the window.
  • emits roadSpawner.spawned{ entityId, spawnerId, pool, lane, x, z, enemy, speed } — once the pool has answered and the car has an id.
  • emits roadSpawner.despawned{ entityId, spawnerId, z, reason }, where reason is behind, ahead or cleared. behind is the one worth scoring on: it means the player let that car go.
  • listens for pool.spawned / pool.released to keep the live count true. A release covers both a kill and the pool recycling its oldest under pressure.
  • listens for roadSpawner.spawning.set{ entityId?, spawning } — a director stopping the stream for a boss or a finish line. Omit entityId to gate every spawner.
  • listens for roadSpawner.clear{ entityId? } — retires everything this spawner has out, on a death or a level reset.

Dependencies

  • Pool — the blueprint the cars actually come from. One named pool per kind.
  • MeshPrimitive — carried by the pooled cars, not by the spawner. It's how the System reads where each car has got to.

Notes

  • The cadence is time, not distance. A per-kilometre cadence keeps traffic density honest at any speed, but it dies in a treadmill scene: park the player, scroll the road, and the player's own odometer never moves, so nothing would ever spawn. Time works under both wirings.
  • The spawner places cars and reclaims them. It does not move them — that belongs to whatever the blueprint carries: a CarDrive with keyboardControlled: false, a velocity, an AI. The rolled speed is handed over for exactly that purpose. Put a car in a pool with nothing to drive it and it will sit at its spawn point until the player passes it.
  • Retiring happens before spawning inside one frame, so a car that leaves frees both its slot and its lane in time for its replacement.
  • A car that something else destroyed outright drops out of the ledger silently on the next sweep — no second release, no leak.
  • The same seed replays the same lanes, speeds and traffic mix. Drag the seed slider and the pattern re-rolls on the spot; leave it alone and a bug you saw once is a bug you can see again.
  • The gate is called spawning, not enabled, because every Component already carries an enabled flag of the framework's own. Setting that one switches the component off wholesale; spawning: false keeps the spawner working and only stops new cars.
  • No hotkey on the tuning panel: WASD is the driving control and the spare digits are taken. Open it from the panel index.

More like this

MeshPrimitive
BoatMode
BoatWater
CarDrive
ContactDamage
DeckDealer
EnemySpawner
ObstacleField
RoadTreadmill
SmokeScreen
SurfacePatch
TrafficConvoy
WeaponsVan

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search