Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

pathNetwork

Path networks built at runtime from Luau: roads and streets, race tracks, railway and tram lines, footpaths, river courses, patrol, delivery and tower-defence routes. A network is nodes (places, junctions, ends) joined by edges (paths through control points). Each edge carries directed lanes. build turns the network into drivable surfaces with static colliders and a material, conforms the terrain under them (cut and fill, not floating or buried), and joins the lanes into a graph that answers the nearest lane to a point, a route between two places, and the point a set distance ahead: what AI drivers, traffic, trains and followers steer by.

Editing a node or an edge and building again rebuilds the surfaces and the ground that edit reaches (the edges at the node, their junctions, the ground under them) and leaves the rest standing, so an edit costs what it reaches; the lane graph is joined again in one pass over its lanes. The same calls build it on native and in the browser.

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 })
local d = net:addNode({ 180, 0, -220 })
net:addEdge(a, b, { points = { { 100, 0, 40 } } })
net:addEdge(b, c)
net:addEdge(b, d, { lanes = { forward = 2, backward = 2 } })   -- b is now a junction
net:addEdge(c, d, { bank = 6 })
print(net:build())             -- { edges, junctions, texels, triangles, lanes, ms }

-- Live edit: only the edges at `c` and the junctions at their ends rebuild.
net:moveNode(c, { 400, 0, -40 })
net:build()

Settings

paths.new(opts) states network-wide settings; addEdge(a, b, fields) and setEdge(id, fields) state them per edge. An edge reads its own, then the network's, then paths.DEFAULTS.

SettingDefault
lanes{ forward = 1, backward = 1 }Lanes from the first node to the last (forward) and back. backward = 0 is one-way.
laneWidth3.5Metres per lane.
shoulder0.5Metres of surface beyond the outer lanes, each side.
widthfrom the lanesThe whole surface's width; when stated, the lanes share it.
speedLimit13.9Metres per second; route { by = "time" } reads it.
bank0Degrees the surface tips about its heading (positive lowers the right edge): one number, or one per control point, ends included.
elevation"terrain""terrain" lays the surface on the ground; "points" takes the control points' heights (a bridge, a ramp, a raised track).
smoothing16Metres the ground-following profile is averaged over.
lift0Metres the surface stands above the ground it follows.
conformtrueCut and fill the ground to meet the surface. A bridge sets false.
bed0.06Metres the ground sits below the surface.
falloff8Metres over which the ground blends back to its own height.
skirt0.8Metres the side walls reach down.
resolution2Metres between cross-sections.
uvScalethe widthMetres of surface one texture repeat covers along the path.
material@builtin::materials.defaultWhat the surface is drawn with.
tintdark grey{ r, g, b } over the material; nil keeps its own colour.
collider{ friction = 1.0 }Fields for each surface's MeshCollider (friction, restitution, and any other field it takes); false for none.

Network-only: name (the parent entity), drive ("right" or "left": which side forward lanes keep to), terrain (a Terrain entity or a list, false for none; unset uses every Terrain in the scene), parent (an entity the surfaces go under).

Authoring

Call
net:addNode(pos, { id?, elevation?, junctionRadius? })A node; answers its id.
net:addEdge(a, b, { points?, id?, ...settings })An edge through points from a to b; answers its id.
net:moveNode(id, pos) / net:setNode(id, fields)Move or change a node.
net:setEdge(id, { points?, ...settings, clear? })Change an edge; clear lists settings to drop.
net:removeEdge(id) / net:removeNode(id)Remove; the ground comes back on the next build.
net:addPlan(plan, edgeFields?) / paths.fromPlan(plan, opts?)Add a laid-out plan (below).
net:spec() / net:apply(spec)The network as plain data, and bringing it to a spec (by id, changing only what differs).
net:build({ all? })Build what changed; answers { edges, junctions, texels, triangles, lanes, ms }.
net:destroy()Take the surfaces down and give the ground back; the nodes and edges stay.

A node where three or more edges meet becomes a junction: the edges stop short of it and a junction surface joins their ends, sharing their vertices. On the ground the junction lies on a plane through the node that keeps the grade of its through way (the two edges that run most nearly straight on into each other) and is level across it; the other edges tip onto that plane where they arrive and curve onto its grade along the way they arrive, so a road through a junction on a slope keeps its grade and a side road climbing to it eases onto it the way it meets a real one. Across a node of two edges the edges share one tangent and, on the ground, one height and grade, so the surface runs on without a step or a crease. Where an edge eases from its own grade to the one it meets, its grade changes by at most 1.5 % per metre over any edge long enough to hold the curve. Where an edge tips onto a junction's plane, its cross slope (the rise from one edge of the surface to the other, per metre across) turns by at most 1 % per metre over an edge long enough to hold the turn, so a car's four wheels keep the ground. A stretch whose surface twists gets cross-sections closer than resolution, so the flat triangles drawn and collided with stand no more than 5 mm off the surface they stand for.

Procedural layout

paths.procedural answers plans ({ nodes, edges }) that paths.fromPlan or net:addPlan builds. The same seed lays out the same network.

Rule
connect(places, { loops?, meander?, spacing?, seed? })Join a set of places: a spanning tree plus loops extra links that cross nothing.
grid({ cols?, rows?, spacing?, origin?, jitter?, seed? })A grid of streets.
circuit({ origin?, radius?, radiusZ?, count?, wander?, pointsPerEdge?, seed? })A closed circuit round an ellipse; wander is the share of the radius it wanders by (0 to 0.9).

Queries

Call
net:nearest(pos, { heading?, maxDistance?, lanes? })The nearest edge, distance along it (s), lateral offset, onSurface, and the lane there with laneDistance, lanePosition, direction. lanes (a route's lanes) looks only at those, so a driver keeps to its route through a junction.
net:route(from, to, { by? })Lanes in order, length, cost, and points to follow. from/to are points or { lane, distance }; by = "time" takes speed limits into account.
net:lookAhead(from, metres, { route?, choose? })The point that far ahead along the lanes, through the nodes.
net:lanePath(ids, { closed? })The polyline of a run of lanes, for a loop or a racing line.
net:lanePoint(lane, d)Position and direction a distance along a lane.
net:sample(edge, s)The built surface's position, forward, right, up and bank at s.
net:lane(id) / net:laneIds()A lane: edge, direction, index, offset, width, speed limit, length, next, prev, stopLine.
net:node(id) / net:edge(id) / net:nodeIds() / net:edgeIds()The network as data.
net:groundHeight(x, z)The ground's height now, cut and fill included.
net:entity()The entity the surfaces stand under.

Lane ids read <edge>:+<n> (running from the edge's first node) and <edge>:-<n> (toward it); n counts out from the centreline. A lane arriving at a junction carries a stopLine. next lists the lanes it leads on to: at a junction, one per other edge; at the end of a single edge, the lanes back.

Followers and drivers

The PathFollower component moves an entity along route.points or a lane loop at a constant speed. A physics vehicle (VehicleController with controlSource = "script") drives a network by steering toward net:lookAhead({ lane, distance }, metres) from net:nearest(position).

Notes

  • The surfaces are spawned as temporary entities and the ground edits live in the loaded terrain: a network is built from Luau when the scene runs (a scene script, a component's awake), and net:spec() is what to store.
  • A built network keeps to its ground: when a Terrain under it loads after the build, or is read again from its asset (a re-bake, reload), the network builds itself again on it.
  • Surfaces are in world space; the parent entity stands at the origin.
  • The ground is the ground each Terrain draws, wherever its entity stands it: on a moved, turned, tilted or scaled Terrain a path lies on the drawn ground, and the cut and fill under it is made along the landscape's own up axis.
  • Sub-modules: paths.geometry (centrelines, cross-sections, meshes), paths.lanes (the lane graph), paths.procedural (layout rules).