logo

Babylon.js Market

BoatMode

vehicular · vehicle-combat

BoatMode

1
Unlock

Install with the CLI:

bjs download BoatMode

BoatMode

The road becomes a river: the vehicle swaps handling, buoyancy and silhouette, and swaps them all back on the far bank.

What it does

Two components and one edge. BoatWaterComponent marks a stretch of water — a box for a river, a disc for a lake — and carries the surface height. BoatModeComponent goes on the vehicle and holds what that vehicle becomes over water: a boat's handling profile, a draft and a swell, and a hull. BoatModeSystem asks each frame whether any water is under the vehicle, and on the frame the answer changes it snapshots the land handling, writes the boat profile in, builds a hull that sinks to float depth, and hides the road silhouette behind it. Beach the vehicle and all three come back.

The hull is its own entity carrying its own MeshPrimitive. That is what keeps this System off the mesh handle CarDriveSystem writes every frame — two systems moving one mesh in one frame is where flicker comes from — and it makes the silhouette swap literal rather than a squashed box.

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 BoatMode

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

    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": {
        "maxSpeed": 26,
        "grip": 0.85
      },
      "BoatMode": {
        "waterMaxSpeed": 18,
        "waterGrip": 0.25,
        "draft": 0.25,
        "bobAmplitude": 0.12,
        "bobFrequency": 0.7,
        "settleRate": 4
      }
    }
  },
  "River": {
    "components": {
      "MeshPrimitive": {
        "primitive": "box",
        "width": 16,
        "height": 0.1,
        "depth": 120,
        "position": [0, -0.05, 160],
        "color": [0.09, 0.22, 0.32]
      },
      "BoatWater": {
        "halfWidth": 8,
        "halfDepth": 60,
        "surfaceY": 0
      }
    }
  }
}

The water's mesh is what you see and where it is; halfWidth / halfDepth / radius are the extent that actually launches the vehicle. Keep the two in step, and keep surfaceY near the height the road sits at — the swap happens in one frame, and a metre of daylight between road and river reads as a pop.

The float is two things, kept apart

depth is where the hull is settling to. It only ever eases toward surfaceY - draft:

TypeScript
const depth = settleToward(state.depth, surfaceY - params.draft, params.settleRate, dt);

The swell is a sine, added on top afterwards:

TypeScript
y: depth + bobOffset(time, params.bobAmplitude, params.bobFrequency)

Fold the two together — ease toward "float depth plus this frame's wave" — and a low settleRate quietly damps the swell, so bobAmplitude: 0.3 draws a wave half that tall and the knob lies. Kept apart, the settle handles exactly one thing (the transient) and the amplitude means what it says.

The settle starts from the vehicle's own height, which is what makes a launch off the bank sink over a beat instead of appearing at float depth. settleRate: 0 places it immediately; 4 takes about a second.

TypeScript
let hull = createBoatFloatState({ depth: 0.5 });          // launched off a bank at Y 0.5
hull = stepBoatFloat(hull, DEFAULT_BOAT_FLOAT_PARAMS, 0, 1 / 60);
// → { time: 0.0167, depth: 0.466, y: 0.501 } — sinking toward -0.25, riding the swell

Why a boat's numbers look like that

road carboat
grip0.850.25
maxSpeed2618
acceleration147
braking283
steerRate2.62
drag0.81.4

grip at a quarter is the whole feel: a hull carries its momentum sideways through a turn instead of biting, so the bow comes round long before the boat does. braking is almost gone, because you cannot stop a boat — you can only stop pushing it — and drag goes up to take that job, so lifting off the throttle is what slows you. Everything else is a slower vehicle.

Handling is written on the edge, once

The System writes those six numbers onto CarDriveComponent the frame the vehicle floats, and not again. Two consequences, and both are deliberate.

A component that scales the profile while the vehicle is out there — a SurfacePatch slick on the river, a power-up — keeps its effect, instead of being stamped over sixty times a second by a System that thinks it owns the fields.

And the six water-handling sliders in the panel are marked live: false, so the panel dims them rather than letting you conclude the values do nothing. Edit them ashore, or beach and relaunch. The buoyancy sliders are stepped every frame and move under you as you drag them.

Props

On the vehicle (BoatMode):

  • waterGrip (number, default 0.25), waterMaxSpeed (18), waterAcceleration (7), waterBraking (3), waterSteerRate (2), waterDrag (1.4) — the profile written onto CarDrive while afloat.
  • draft (number, default 0.25) — how far the hull's centre rides below the surface.
  • bobAmplitude (number, default 0.12) — half-height of the swell. 0 is a millpond.
  • bobFrequency (number, default 0.7) — swell rate in cycles per second.
  • settleRate (number, default 4) — how fast the hull settles to float depth, as 1 - exp(-rate·dt). 0 places it immediately.
  • hullWidth / hullHeight / hullLength (number, defaults 2.4 / 0.6 / 4.6) and hullColor ([r,g,b]) — the hull box. Build-time: the mesh is created with these once.
  • hideVehicleMesh (boolean, default true) — hide the road silhouette while afloat. False shows both at once, which is useful while you are sizing the hull.

On the water (BoatWater):

  • shape ("box" | "disc", default "box") — a box is a river or a channel, a disc is a lake.
  • halfWidth / halfDepth (number, defaults 10 / 40) — box half-extents.
  • radius (number, default 20) — disc radius.
  • surfaceY (number, default 0) — height of the water surface. Hulls float draft below it.

Events

  • emits boatMode.entered{ entityId, waterEntityId, surfaceY } — the frame the vehicle floats. The moment for a splash, a wake emitter, or an engine-note change. waterEntityId is empty when the mode was forced from the bus.
  • emits boatMode.exited{ entityId, waterEntityId } — the frame it beaches, whether it drove out or the water was deleted.
  • listens for boatMode.set{ entityId?, afloat, surfaceY? } — force the mode from a director or a scripted jump. true floats it (at surfaceY, or wherever it already is), false beaches it, null hands the decision back to the water bodies. Omit entityId to switch every amphibious vehicle in the world.

Dependencies

  • MeshPrimitive — on the vehicle (its position, and the silhouette that hides), on the water body (its centre), and on the hull the System builds. MeshPrimitiveSystem must be in the world or the hull exists without a mesh.
  • CarDrive — the handling profile being swapped. It is optional: a vehicle without one still floats, bobs and swaps silhouette, it just has no handling to exchange.

Notes

  • Positions come off the renderer (getMeshWorldPosition), never MeshPrimitiveComponent.position. Nothing writes that array back from physics, so a vehicle dropped into the river by a respawn would otherwise still be reported on dry land.
  • The frame a hull is created it stays where MeshPrimitiveSystem put it, and the System takes it over from the next frame. Two systems writing one mesh handle in one frame is the flicker bug.
  • Crossing back and forth reuses the same hull — it is hidden, not disposed. The hull entity is destroyed with the vehicle, with the component, or with the System.
  • The water body's own footprint is read every frame, so a river scrolled past a parked car by RoadTreadmill arrives on schedule without anything special.
  • Y is the only axis this component owns, and only for the hull. Where the vehicle goes on the XZ plane is still CarDrive's business, with the boat's numbers.
  • 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

CarDrive
MeshPrimitive
BoatWater
ContactDamage
RoadSpawner
RoadTreadmill
Skybox
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