logo

Babylon.js Market

SmokeScreen

vehicular · vehicle-combat

SmokeScreen

Listens toKeyboardInput
1
Unlock

Install with the CLI:

bjs download SmokeScreen

SmokeScreen

Press the button and leave a wall of smoke down the road behind you.

What it does

A screen isn't one cloud. Press the button and the car vents for ventSeconds, dropping a puff every dropInterval — a trail of overlapping clouds laid down the road behind you, each blinding for puffLifetime before it thins out. That's why the screen still works while you're turning: the puffs mark where you were. SmokeScreenComponent is the rack and the recipe; SmokeScreenSystem runs the state machine, drops the puffs as real particle emitters, and reports which pursuers are sitting in one.

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 SmokeScreen

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

    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
{
  "Input": {
    "components": {
      "KeyboardInput": {}
    }
  },
  "Car": {
    "tags": ["player"],
    "components": {
      "MeshPrimitive": { "primitive": "box", "width": 1.8, "height": 1, "depth": 4, "position": [0, 0.5, 0] },
      "CarDrive": {},
      "SmokeScreen": {
        "maxCharges": 2,
        "rechargeSeconds": 8,
        "ventSeconds": 1.6,
        "dropInterval": 0.2,
        "puffLifetime": 4,
        "blindRadius": 5,
        "pursuerTag": "enemy"
      }
    }
  }
}

KeyboardInput is what publishes keyboard.keydown. Without it in the scene the car has a rack of smoke and no button.

Twelve emitters, not six hundred

Every puff is a live particle emitter, and a two-minute chase is hundreds of drops. The System never creates more than maxPuffs of them:

TypeScript
if (rt.puffs.length >= cap) {
  const recycled = rt.puffs.shift()!;
  recycled.x = at.x;
  recycled.y = y;
  recycled.z = at.z;
  recycled.life = comp.puffLifetime;
  if (recycled.handle && renderer) {
    renderer.setParticleEmitterPosition(recycled.handle, at.x, y, at.z);
  }
  rt.puffs.push(recycled);
  return;
}

The oldest cloud is moved to the new drop point and its clock restarts. createParticleEmitter runs at most maxPuffs times per car for the life of the game; disposeParticleEmitter runs when a puff outlives its lifetime, when maxPuffs is dragged down, or when the car leaves the world.

The trade is the reason the Look knobs are dead in the panel — emitRate, emitRadius, minSize, maxSize, rise and capacity are baked into the emitter's spec when it's created, and a recycled emitter keeps the spec it was born with. Set those in scene JSON.

Blinding is a place, not a flag

Nothing writes "blinded" onto a pursuer. Each frame every entity tagged pursuerTag is measured against every live puff, and the events fire on the edges:

TypeScript
const was = rt.blinded.has(pursuer.id);
if (inside === was) continue;

So a chaser that drives through the smoke gets one smokeScreen.blinded going in and one smokeScreen.cleared coming out — not sixty a second — and the same cleared arrives on its own when the puff it was sitting in thins out. An AI listens for those two and drops its target.

blindRadius has nothing to do with how big the cloud looks. Set it to roughly match the visible smoke: too small and pursuers drive through a wall of grey unaffected, too large and they lose you in clear air, which reads as a bug either way.

Props

  • maxCharges (number, default 2) — screens the car carries at once.
  • rechargeSeconds (number, default 8) — seconds to earn one charge back. 0 refills instantly.
  • ventSeconds (number, default 1.6) — how long one press keeps venting.
  • dropInterval (number, default 0.2) — seconds between puffs. 0 or less drops one every frame.
  • puffLifetime (number, default 4) — seconds a puff keeps blinding.
  • maxPuffs (number, default 12) — concurrent puffs, and therefore the emitter budget. Reaching the cap recycles the oldest.
  • dropBack (number, default 3.5) — world units behind the nose a puff lands.
  • puffY (number, default 0.4) — height above the car the puff sits at.
  • blindRadius (number, default 5) — XZ radius each puff blinds within.
  • pursuerTag (string, default "enemy") — what the screen is meant to shake off.
  • keyboardControlled (boolean, default true) — set false on AI cars so the player's key doesn't fire the whole grid.
  • deployKeys (string[], default ["Space"]) — KeyboardEvent.code list. Edge-triggered on keydown; holding it does nothing.
  • capacity / emitRate / emitRadius / minSize / maxSize / minLifeTime / maxLifeTime / rise — the emitter recipe, read once per puff when it's created.

Events

  • emits smokeScreen.deployed{ entityId, charges } — a charge was spent. charges is what's left, for a HUD.
  • emits smokeScreen.depleted{ entityId } — the button was pressed on an empty rack. The cue for an empty-click sound.
  • emits smokeScreen.recharged{ entityId, charges } — a charge came back.
  • emits smokeScreen.blinded / smokeScreen.cleared{ entityId, screenEntityId } — a pursuer entered or left the screen. entityId is the pursuer; screenEntityId is whose smoke it is.
  • listens for keyboard.keydown (published by KeyboardInput) — deploys on a fresh press of any deployKeys code.
  • listens for smokeScreen.deploy{ entityId? } — how an AI or a director fires a screen. Omit entityId and every screen in the world deploys.

Dependencies

  • MeshPrimitive — the car the puffs drop behind. The System reads its live world position and its yaw.
  • KeyboardInput — the bridge that publishes the key events. Only needed for cars that read the keyboard.

Notes

  • Puffs land along the car's heading, not its travel direction, so a car sliding sideways still lays its screen out of the exhaust rather than out of the door.
  • A press made mid-vent is ignored — not refused. It neither extends the screen nor spends a second charge, so leaning on the button can't drain the rack. Releasing and pressing again after the vent ends does deploy again.
  • dropTimer carries its remainder across frames, so puff spacing is identical at 30fps and 144. A frame hitch drops several puffs at once rather than skipping them, capped at maxPuffs so a stall can't mint a hundred clouds.
  • The car's position is read live through getMeshWorldPosition. Nothing writes MeshPrimitiveComponent.position back from physics, so a screen that trusted it would drop every puff at the spawn point.
  • The puff sprite is rasterised on the CPU into an RGBA buffer and uploaded once, deduped by key — every car in the scene shares one texture. It's white, so the emitter's own colours decide whether the screen is diesel black or white.
  • No hotkey on the tuning panel: the driving controls own the letters and the spare digits are taken. Open it from the panel index.

More like this

KeyboardInput
MeshPrimitive
AiPilot
Asteroid
BoatMode
BoatWater
Bullet
CarDrive
ContactDamage
Enemy
Health
MachineGun
Missile
MissileLauncher
OilBlob
Parts
RoadSpawner
RoadTreadmill
ShipLoadout
SpaceDust
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