roadPieces
Road and track pieces built as real geometry, in three styles that one setting switches:
roadPieces
Road and track pieces built as real geometry, in three styles that one setting switches:
| style | deck | markings | roadside |
|---|---|---|---|
country | 6.7 m tarmac, two lanes | faded dashed white centre line, edge lines | 2.5 m mown verges, white marker posts every 25 m |
highway | 17.2 m tarmac, two lanes each way, shoulders | double yellow centre, dashed lane lines, edge lines | concrete (New Jersey) barriers on the shoulders, 3 m verges |
dirt | 4.4 m track | none | dirt wheel ruts, gravel shoulders, a grass crown down the middle, verges |
Pieces: straight, curve (radius, angle; positive turns right), tJunction, xJunction (filleted kerb corners, stop lines on the side roads), roundabout (kerbed grass island, 1 to 4 arms clipped exactly to the ring, give-way lines), ramp (slope, length), kicker (lip height and angle, gap, table-top landing with an 18° run-out), bankedTurn (radius, angle, bank eased in and out, red/white kerb on the inside, concrete wall on the outside), guardrail (W-beam on posts), wall (concrete barrier), tyreWall (two staggered rows of tyre stacks), and spline (a continuous road through a list of points that follows their heights).
The asphalt and its markings are one procedural shader (roadSurface, on driveSurfaces' pavement library): aggregate, repair patches, cracks, polished wheel tracks per lane and worn paint, placed by world position so consecutive pieces join without a seam. Every piece's deck, verges, kerbs and walls collide (static exact mesh colliders) and carry a driveSurfaces Surface: deck tarmac (dirt on the dirt style, gravel shoulders, grass crown), verges grass, kerbs rumble, walls and ramp sides concrete.
First example
Paste this into the console (execute) of a fresh world that has roadPieces (with driveSurfaces and groundMaterials) installed. It builds a country road with a bend and a roundabout, then switches the bend to a highway with one setting:
local Roads = require("roadPieces.roads")
-- every piece starts at its origin and runs along its local -Z
local straight = Roads.spawn("straight", { style = "country", length = 30, position = { 0, 0, 0 }, synced = true })
local bend = Roads.spawn("curve", { style = "country", radius = 25, angle = 90, position = { 0, 0, -30 }, synced = true })
local rb = Roads.spawn("roundabout", { style = "country", radius = 8, length = 12, position = { 60, 0, -40 }, synced = true })
-- one setting restyles a piece in place
bend.component.get("roadPieces.RoadPiece").style = "highway"
-- the centreline in world space, for an AI driver (aiDrivers: AiDriver:setPath(points, loop))
local points, loop = Roads.pathOf(rb)
print(#points, loop) --> 49 true (the ring, counter-clockwise from above)
A spline road through your own points (world positions with heights):
local hill = Roads.spline({
vector.create(0, 0, 0), vector.create(40, 3, -30), vector.create(10, 6, -80), vector.create(-30, 2, -110),
}, { style = "dirt", synced = true })
To see everything, load the gallery (edit mode): zero scene load roadPieces.roadGallery. Rows are the three styles (country at z 0, highway at z 100, dirt at z 200), columns the twelve kinds, each labelled.
API (require("roadPieces.roads"))
| Call | What it does |
|---|---|
Roads.spawn(kind, opts?) | spawns an entity with a RoadPiece; opts takes any RoadPiece field plus position, yaw (degrees), parent, name, synced |
Roads.spline(points, opts?) | a spline piece through points (vectors or {x,y,z}, in the entity's local space = world space at the origin); opts.loop closes it |
Roads.pathOf(road, offset?) | (points, loop): the centreline on the deck surface in world space; offset metres shifts it to the right of travel (lane centres). Junctions answer their through road, a roundabout its ring. Accepts the piece or any of its children |
Roads.build(spec) / Roads.realize(root, built, style) | the pure geometry builder and the step that stands it up as temporary children |
Roads.specOf(component), Roads.encodePoints, Roads.decodePoints | field <-> spec helpers |
Roads.gallery(parent?) | every piece in every style, labelled |
Roads.STYLES, Roads.KINDS, Roads.DECK (deck height, 5 cm), Roads.BASE_DEPTH | the tables behind it |
The RoadPiece component
Fields (all synced): kind, style, length (20), radius (25), angle (90), bank (25), slope (12), height (1.2), gap (8), width (0 = the style's), arms (4), verge (true), rails (auto | none | posts | guardrail | concrete | tyres), points (spline points as "x,y,z;x,y,z"), loop. Changing any field rebuilds the piece. Methods: :path(offset?), :rebuild().
Geometry is built locally on every peer from those fields as temporary, internal child entities, so a scene stores only the RoadPiece record and nothing has to replicate but the fields. Spline points live in one string field rather than a synced list (synced lists with gaps, #21242) and nothing holds a synced entity reference (#21241).
Placing pieces
- Every piece lies in its own local space: it starts at the origin and runs along local -Z, right is +X, up +Y. The deck top is 5 cm above the piece's ground level (local y = 0); verges and base plates lie at y = 0 and reach 0.7 m down, so pieces sit into terrain without gaps.
- A curve of
radiusR andangleA ends at(R(1-cos A), 0, -R sin A)(mirrored for negative angles) heading along the turned direction: place the next piece there withyaw = -A. - Junction arms and roundabout arms end flush with straights of the same style, so a network is pieces end to end.
- With
verge = true, ramps, kickers, banked turns and barriers stand on a grass base plate; set it false to drop them on your own ground.
Known limits
- #1404: the engine's built-in roads are in progress, which is why this package builds its own road meshes. When they land, the styles and the Surface wiring here can sit on top of them.
- The engine's z-fighting diagnostic flags ground laid near other ground from a distance (it uses a tolerance that grows with distance: about 10 cm at 20 m). Within a piece no asphalt lies over grass (junction and roundabout plates wrap the decks instead of sitting under them), but a deck 5 cm above its verge, or a piece on terrain, still flags from far off. The gallery stands each piece on a 0.6 m plinth above its meadow for that reason.
- Pieces do not bend to terrain: lay them on flat ground or use a
splinewhose points follow the terrain.
Files
roads.module, RoadPiece.component, shaders/roadSurface.shader, materials/ (roadCountry, roadHighway, barrierConcrete, kerb, kerbConcrete, steel, rubber, postWhite), roadGallery.scene.
Scoped to this part · feeds back into the world's score.