logo
RoadCamera

vehicle · vehicle-combat

RoadCamera

1
Unlock

Install with the CLI:

bjs download RoadCamera

RoadCamera

The overhead camera of a top-down road game: north up, the car low on the screen, the road ahead in view.

What it does

Pick a spot on the screen for the car, and the camera works out where it has to be to keep it there. As the car speeds up, it drops lower on the screen and the camera climbs, so you see more of what is coming.

It makes one perspective camera and announces it on camera.active.changed.

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:

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

    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
{
  "Camera": {
    "components": {
      "RoadCamera": {
        "target": "Player",
        "height": 42,
        "pitch": 1.1,
        "carScreenY": 0.3,
        "speedCarScreenY": 0.2
      }
    }
  }
}

pitch is how far the camera looks down from the horizon, in radians (1.57 is straight down).

Pinning the car

A pinhole camera puts a point that is y from the middle of the screen at atan(y · tan(fov/2)) off its axis. Add the pitch and you have the angle down to the car. The car is then height / tan(angle) ahead of the camera along the road:

TypeScript
  const y = clamp(screenY, 0.02, 0.98) * 2 - 1;
  const below = Math.atan(-y * Math.tan(fov / 2));
  const carAngle = clamp(pitch + below, 0.05, Math.PI / 2);
  return {
    toCar: height / Math.tan(carAngle),
    toCenter: height / Math.tan(clamp(pitch, 0.05, Math.PI / 2)),
  };

With the defaults at rest, the camera sits 13.1 m behind the car. The middle of the screen lands 8.2 m ahead of the car. At topSpeed the camera rises to 52 m and that look-ahead grows to 14.7 m. The camera moves to that spot every frame, so the car stays where you put it.

Following the road

Sideways the camera follows a blend of the car and the road's centre line (roadFollow). Locked to the car, every lane change swings the world. Locked to the road, a car on the shoulder slides off the screen. With no HighwayRoad in the world it follows the car.

The blend is smoothed with a time constant, not a fixed share per frame:

TypeScript
export function catchUp(rate: number, dt: number): number {
  return 1 - Math.exp(-Math.max(0, rate) * Math.max(0, dt));
}

At the default followRate, one frame at 60 fps closes 6.4% of the gap. One frame at 30 fps closes 12.5%. That is the same as two frames at 60, so the camera moves the same at any frame rate. Along the road it never lags.

Shake

Send roadCamera.shake with a trauma from 0 to 1. Knocks add up, and time drains them at shakeDecay. The offset grows with trauma squared:

TypeScript
  const s = state.trauma * state.trauma * p.shakeMax;

So trauma 0.5 moves the camera by a quarter of shakeMax. A scrape is a nudge and a crash is a jolt. Give x and z too and a far-off bang shakes less, down to nothing at shakeRadius.

Settings

FieldDefaultWhat it does
target"Player"Entity to follow.
height42Metres above the road at rest.
speedHeight10Extra metres up at topSpeed.
pitch1.1Look-down angle from the horizon, radians. π/2 is straight down.
carScreenY0.3Car's place on screen at rest. 0 is the bottom edge, 0.5 the middle.
speedCarScreenY0.2Car's place on screen at topSpeed.
topSpeed52The speed that counts as full, m/s.
roadFollow0.45Sideways blend: 0 is the car, 1 is the road's centre line.
followRate4Catch-up rate, per second. Higher is stiffer.
fov0.8Up-and-down field of view, radians.
shakeDecay1.6Trauma drained per second.
shakeMax1.4Shake offset at trauma 1, metres.
shakeRadius60A located shake fades to nothing this far from the car.
near0.5Near clip plane.
far700Far clip plane.

Events

EventPayloadDirection
camera.active.changed{ entityId, handle }emits once, when it makes its camera
roadCamera.shake{ trauma, x?, z? }listens

Needs

  • MeshPrimitive: where the target car is right now.
  • CarDrive: the target's speed, which sets the climb and the look-ahead.
  • HighwayRoad: the road's centre line ahead of the car.

More like this

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search