Log inGet started

Paths: splines, roads, tracks, rails, lanes and routes

Updated October 26, 2026
PieceWhat it is
@builtin::modules.splineCurve math: linear, Catmull-Rom and Bezier splines, eased sampling, placement along a curve (fence posts, rows of props), and measuring a curve by arc length.
@builtin::modules.pathNetworkNetworks of paths built at runtime: roads, streets, race tracks, railway and tram lines, footpaths, river courses, patrol and delivery routes. Drivable surfaces with colliders and a material, junctions, terrain cut and fill, directed lanes, and route queries.
PathFollower componentMoves an entity along a path at a constant speed: a train, a tram, a patrol, a camera dolly, a ghost car.

A single curve

local spline = require("@builtin::modules.spline")
local path = spline.new({ { 0, 0, 0 }, { 20, 2, 10 }, { 40, 0, 0 } }, { mode = "catmullRom" })
local mid = spline.evaluate(path, 0.5)          -- by parameter
local m = spline.measure(path)                   -- by arc length
local p, dir = spline.atDistance(m, 12)          -- 12 metres along, constant speed
local posts = spline.fence(path, 2.5)            -- placements every 2.5 m

A spline's parameter does not advance at a constant rate along the curve, so anything that moves in metres per second reads the measured form.

Moving something along a path

local e = entity.spawn("tram")
e.component.add("Model", { model = "@builtin::meshes.cube" })
e.component.add("PathFollower", { speed = 8, mode = "loop" })
e.component.get("PathFollower"):follow(spline.new(points, { mode = "catmullRom", closed = true }))

PathFollower takes a list of points, a spline, or anything carrying points (a network route, a lane loop), and loops, ping-pongs or stops at the end. It sets the entity's transform, so it drives entities with no dynamic body. A physics vehicle follows a path by steering instead (below).

A network of paths

local paths = require("@builtin::modules.pathNetwork")
local net = paths.new({ lanes = { forward = 1, backward = 1 }, speedLimit = 15 })
local a = net:addNode({ 0, 0, 0 })
local b = net:addNode({ 200, 0, 40 })
local c = net:addNode({ 380, 0, -60 })
net:addEdge(a, b, { points = { { 100, 0, 40 } } })
net:addEdge(b, c, { bank = 5 })
net:build()
  • Edges run through control points; settings per edge or per network: lanes each way, lane width, shoulder, speed limit, bank, material, tint and the collider's fields (friction, ...).
  • Junctions form where three or more edges meet; the edges stop short and a junction surface joins their ends.
  • Elevation: "terrain" lays the surface on the ground (smoothed); "points" takes the control points' heights. A bridge is an edge with elevation = "points" and conform = false.
  • Terrain: with a Terrain in the scene, build cuts and fills the ground to meet each surface and blends it back over falloff metres, through Terrain:setHeights, which rebuilds the drawn ground and its collision where the change reaches. Moving or removing a path gives the ground back.
  • Live editing: moveNode, setEdge, removeEdge, then build() again rebuilds only what the edit reaches; build answers what it rebuilt and the milliseconds it took.
  • Procedural layout: paths.procedural.connect(places), .grid({...}), .circuit({...}) answer plans for paths.fromPlan(plan).
  • A railway is a one-lane, one-way edge (lanes = { forward = 1, backward = 0 }, width = 3); a footpath a narrow edge with no collider; a river course an edge whose surface is a water material, lowered with lift.

Lanes, routes and AI drivers

Every edge carries directed lanes (<edge>:+1 from its first node, <edge>:-1 back), joined through the nodes into a graph:

local route = net:route(car.position, goal, { by = "time" })     -- lanes, length, points
-- each tick: where the car is on its route, and the point it steers toward
local here = net:nearest(car.position, { heading = forward, lanes = route.lanes })
local target = net:lookAhead({ lane = here.lane, distance = here.laneDistance }, 15, { route = route.lanes })

A car built from WheelColliders with VehicleController { controlSource = "script" } drives a network by steering toward target.position each tick (setIntent { throttle, steer }); traffic reads lane.stopLine and lane.speedLimit from net:lane(id).

man pathNetwork and the module's README list every call.