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.
The track
Section titled “The track”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 levelright, the surface’supand thehalfWidthat that distance.pointAt(distance, lateral): the world point at those track coordinates,lateralpositive to the right of travel.locate(point): thedistanceandlateralof the track point nearest a world point.zoneWeight(distance, zone): how much ofzonethe 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.
Surface friction
Section titled “Surface friction”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.
Launch
Section titled “Launch”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.
Where it runs
Section titled “Where it runs”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> );}