Skip to content

TrackMover

TrackMover rides an entity along a Track in track coordinates: a distance along the centerline, an offset across it, and a height above it. It has no physics body. Gravity along the slope adds speed, and drag and friction take it away, so every number that decides the ride is a prop. A sled, a racer, a rail shooter and a water slide ride this way.

new Track(points) builds a track from two or more points. The curve passes through every point. Each point may name a width in metres, blended to the next point, and 8 when left out. It may also name a zone, such as "ice", which a TrackSurface draws in its own material. The track answers the following lookups:

  • length: metres from the start to the end.
  • frameAt(distance): the position, the direction of travel (tangent), the level right, the surface’s up and the halfWidth at that distance.
  • pointAt(distance, lateral): the world point at those track coordinates, lateral positive to the right of travel.
  • locate(point): the distance and lateral of the track point nearest a world point.
  • zoneWeight(distance, zone): how much of zone the track is at that distance, from 0 to 1, blended between points.

A Track is data built once, so build it at module level or in a useMemo.

new Track(points, { friction }) gives the track a surface that slows a grounded mover by where it rides. friction({ track, distance, lateral }) returns a loss in 1/s at those track coordinates, and track.frictionAt(distance, lateral) reads it, 0 when the track has none. Each step adds it to the mover’s own friction prop, so every mover on the track rides the same surface. A kart game returns 1 on the grass past its racing line and 0 on the asphalt. Sled returns the depth of the snow under the rider times 1.14, which costs about 0.04/s on a snow lane, 0.4/s at a shoulder’s outer edge, and nothing on ice.

Unity, Unreal, Godot, Roblox and PlayCanvas read friction off the material of the collider under a body. A mover has no contact, so its track answers in track coordinates instead.

Its props are in TrackMoverProps.

The step reads the player’s steer and jump into every mover on an entity with authority={RunContext.Client}. A steer shifts it across, and releasing it holds the line it is on through a bend. At the track’s edge the outward shift stops. A jump leaves only from the ground. For any other mover, such as a rival racer, the game writes steer and jump on its TrackMoverTrait. The trait also holds the ride’s state, which a game reads for a speedometer or a landing: distance, lateral, height, speed and airSeconds.

launchTrackMover(entity, speed) sets a mover going at speed metres a second from where it stands, and enables it: a sling’s release, a starting gate or a boost pad. The speed replaces the one it had, as Unreal’s LaunchCharacter replaces a character’s velocity. A mover mounted with enabled={false} holds at its start until the launch.

Client. The mover runs in the step’s move block and stands the entity at its track coordinates, facing down the track.

import { Entity, RunContext, Track, TrackMover } from "@daniel-zarinski/engine";
const run = new Track([
{ x: 0, y: 0, z: 0, width: 6 },
{ x: 8, y: -4, z: -20 },
{ x: -8, y: -8, z: -45 },
]);
export function Sled() {
return (
<Entity model="sled" authority={RunContext.Client}>
<TrackMover track={run} distance={6} gravity={15.7} />
</Entity>
);
}