logo

Babylon.js Market

TrafficConvoy

vehicular · vehicle-combat

TrafficConvoy

1
Unlock

Install with the CLI:

bjs download TrafficConvoy

TrafficConvoy

A car that drives through the same piece of road the truck in front of it drove through, a moment later.

What it does

Point a follower at the vehicle in front and steer toward it and you get a line that cuts every corner: the follower aims at where the leader is, not where it went, so each car takes a slightly tighter line than the one ahead and by the fourth truck the convoy is driving through the verge.

TrafficConvoy follows the path instead. The System records the lead's pose into a ring buffer every frame, and a follower is a lookup into that buffer at its own age — car 0 is where the lead was spacingSeconds ago, car 1 twice that. Every car drives through the same road the lead did, so the line bends round a curve as one shape and a car pulled out to laneOffset stays exactly that far out the whole way round.

Only the follower carries a component. The lead is named by id and needs nothing, so a convoy is three trucks pointing at a fourth that has no idea it's being followed — and that fourth can be a player, a RoadSpawner car, or anything else with a mesh.

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 TrafficConvoy

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 TrafficConvoy
    bjs download TrafficConvoy

    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
{
  "Truck": {
    "tags": ["convoy"],
    "components": {
      "MeshPrimitive": {
        "primitive": "box",
        "width": 2.2,
        "height": 2.4,
        "depth": 6,
        "position": [0, 1.2, 60],
        "color": [0.45, 0.4, 0.35]
      },
      "CarDrive": { "maxSpeed": 16, "keyboardControlled": false }
    }
  },
  "Trailer1": {
    "tags": ["convoy"],
    "components": {
      "MeshPrimitive": {
        "primitive": "box",
        "width": 2.2,
        "height": 2.2,
        "depth": 6,
        "position": [0, 1.2, 52],
        "color": [0.5, 0.45, 0.4]
      },
      "TrafficConvoy": { "lead": "Truck", "index": 0, "spacingSeconds": 0.9 }
    }
  },
  "Trailer2": {
    "tags": ["convoy"],
    "components": {
      "MeshPrimitive": {
        "primitive": "box",
        "width": 2.2,
        "height": 2.2,
        "depth": 6,
        "position": [0, 1.2, 44],
        "color": [0.5, 0.45, 0.4]
      },
      "TrafficConvoy": { "lead": "Truck", "index": 1, "spacingSeconds": 0.9 }
    }
  }
}

The trailers' authored positions only matter for the frame or two before the trail has anything in it. After that the System owns their X, Z and yaw; Y stays theirs.

The delay is the spacing

There is no distance figure anywhere in this component. A car's place in the line is (index + 1) * spacingSeconds of time, and the gap in metres falls out of how fast the lead is going:

TypeScript
export function convoyDelay(index: number, spacingSeconds: number): number {
  return (Math.max(0, index) + 1) * spacingSeconds;
}

That's the right way round for traffic. A convoy that slows for a bend closes up and opens out again on the straight, exactly as one does on a real road, and none of it needs a following distance controller. It also means one number retimes the whole line: halve spacingSeconds on every car and the trucks bunch nose to tail at any speed.

Where the history runs out

A convoy that has existed for a quarter of a second has a quarter of a second of path. Car 2 at 0.9 spacing wants 2.7 seconds of it. Clamping to the oldest sample would stack every car on that one spot until the lead has driven far enough to feed them — very visible when a RoadSpawner drops a convoy in ahead of the player.

So a follower that outruns the history is walked back down the lead's heading by the distance the lead would have covered in the missing time:

TypeScript
if (kind === 'oldest' && p.stringOut) {
  const missing = out.time - targetTime;
  advanceAlong(out, -missing * convoyTrailSpeed(trail), out);
}

The line is strung out correctly from the first frame, and as real history accumulates the guess is replaced by recorded samples with no visible seam. Set stringOut: false and you get the bunch-then-unfold instead.

Props

  • lead (string, default "") — entity id of the vehicle this one follows. Empty means it follows nobody, which is a legitimate state rather than a broken convoy.
  • index (number, default 0) — place in the line. 0 trails the lead by one spacing.
  • spacingSeconds (number, default 0.9) — seconds between neighbours.
  • laneOffset (number, default 0) — sideways offset from the lead's line, positive to its right.
  • capacity (number, default 360) — samples of path a lead keeps. At 60fps that's six seconds. Read once, when the lead's trail is allocated.
  • stringOut (boolean, default true) — walk a car back along the heading when the history is shorter than its delay.

Events

  • emits trafficConvoy.broken{ entityId, lead, index } — the frame the lead stops existing, whether it was destroyed or parked back in its pool. Edge-triggered: one event, not one a frame. The car is left where the last good sample put it.
  • emits trafficConvoy.rejoined — same payload — if that lead comes back.
  • listens for trafficConvoy.lead.set{ entityId, lead, index? } — promoting the second truck when the first blows up, or handing a straggler to a different convoy. An empty lead detaches the car and lets whatever else is on it take over.

Dependencies

  • MeshPrimitive — on the follower, which the System places, and on the lead, whose live world position it records.

Notes

  • The lead's heading comes from the step it took, not from MeshPrimitiveComponent.rotation. That array is only right when something happens to be writing it every frame, whereas a step is there whatever moves the vehicle. The cost is that a lead in reverse implies a heading 180° out, and its convoy will face the way it's travelling rather than the way it's pointing.
  • Positions are read off the renderer every frame. A lead driven by CarDrive, a velocity or a physics body never writes back to MeshPrimitiveComponent.position, so recording that array would record the truck standing still at its spawn point forever.
  • One trail per lead, no matter how many cars follow it, and it's dropped the frame the last follower lets go. Two followers that disagree about capacity get the first one's figure — the ring is already allocated by the time the second asks.
  • Pooled leads keep their entity ids between lives. A convoy pointed at traffic-3 will silently re-attach when that slot is reused by a completely different car, so hand followers a new lead explicitly rather than relying on the id.
  • Only X, Z and yaw are written. Whatever owns the car's height keeps owning it.
  • No hotkey on the tuning panel, and it binds to the first follower it finds — dragging spacingSeconds retimes that one car, not the whole line. Open it from the panel index.

More like this

MeshPrimitive
AIGoalSeek
AIKick
AIZone
AiPilot
BallPossession
BallPursuit
BoatMode
BoatWater
CarDrive
ContactDamage
Enemy
EnemySpawner
FreighterChain
Jump
KeyboardMover
LineOfSight
Movement
PokerAI
PokerAIBill
PokerAICali
PokerAIRandy
PokerAIShark
PokerAISurge
PokerAITilt
RoadSpawner
RoadTreadmill
ShipFlight
SmokeScreen
SurfacePatch
Transform6DOF
Velocity6DOF
WaveDirector
WeaponsVan
WorldOriginAnchor

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search