Skip to content

Moving Platforms

When followPlatform is enabled (the default), a character standing on a moving or kinematic body inherits that body’s motion — you ride elevators, rafts, and rotating discs without sliding off, and you jump with the platform’s velocity.

Each tick, if the ground body under the character is kinematic or dynamic, the controller:

  1. Computes the platform’s velocity at the contact point (linear + angular).
  2. Expresses the character’s velocity relative to that surface — this is what relativeVelocity / moveSpeed report.
  3. Drives the relative planar velocity toward zero using platformGripFactor.
  4. On a rotating platform, folds the platform’s spin into the character’s facing (exposed as turnOnYQuat) so you turn with the disc.

controller.isOnPlatform (and snapshot().isOnPlatform) is true whenever this coupling is active.

Two separate knobs control friction because static and moving ground want different feels:

Option Applies to Default Effect
slideGripFactor Static ground 0.5 Soft slide-to-stop when you release the stick.
platformGripFactor Moving / kinematic ground 1 How strongly planar velocity matches the surface each tick. 1 = fully match (no slide); lower = slippery.

Because platformGripFactor couples you to the surface velocity, it’s also what carries you off the edge of a rolling platform — stronger grip means both “no sliding on the mover” and “a stronger roll-off from a spinning cylinder”.

Kinematic bodies are the simplest movers: you drive them, physics doesn’t. Create one on a layer the character can stand on, and move it toward a target each tick with Body.moveKinematic(...) (see the jolt-ts docs for the exact signature):

const platform = world.createBody({
type: "kinematic",
shape: Shape.box({ halfExtents: [4, 0.2, 4] }),
position: [0, 0.2, 0],
layer: "moving",
});
function frame(t: number, dt: number) {
const target = [Math.sin(t) * 6, 0.2, 0];
platform.moveKinematic(target, [0, 0, 0, 1], dt); // velocity-based motion
controller.setMovement(input);
controller.step(dt);
world.step(dt);
}

On dynamic ground — a physics raft, a see-saw, a stack of crates — the character should push back, or it would appear weightless. The controller applies Newton’s-third-law reactions to the ground body, each individually toggleable:

Option Default Reaction applied to dynamic ground
applyCounterMass true The character’s weight presses down (a raft sinks under you).
applyCounterJumpImp true Jumping off pushes the ground down.
counterJumpImpFactor 1 Scales the counter-jump impulse.
applyCounterMoveImp true Walking pushes the ground backward.
counterMoveImpFactor 1 Scales the counter-move impulse.

How hard the character pushes back is scaled by the mass ratio between the ground body and the character, remapped through massRatioFallOffCurveData — a curve. This keeps behavior believable across scales: a light plank reacts strongly to your weight, while a massive barge barely notices you.

const controller = new CharacterController({
world,
massRatioFallOffCurveData: {
points: [
{ x: 0, y: 0, r_out: 0 },
{ x: 0.5, y: 0, r_in: 0, r_out: 0 },
{ x: 1, y: 1, r_in: 0 },
],
},
});

If you replace the curve at runtime, call controller.refreshMassRatioFallOffCurve() to rebake it.