Traffic Simulator
games · v1.0
Description
Traffic simulator game with resource management
Updated 18 October 2026
README
Autonomous Traffic Simulation
Top-down traffic simulation for a browser game. Simulation logic is plain ES
modules (no p5/DOM) under src/sim/; rendering is p5.js under src/render/.
Run
npm start # serves the folder on http://localhost:8080 (any static server works)
npm test # unit tests, Node's built-in runner (Node 20+)
Layout
| Path | Purpose |
|---|---|
src/sim/geometry.js |
Bezier sampling, lane offsetting, arc-length Polyline |
src/sim/network/ |
Road graph: Node, LaneEdge, Connector, LogicalRoad, RoadNetwork |
src/sim/network/editing.js |
Topology-aware edits (split road, merge nodes, update/delete) that keep lanes, junction sizes and turns consistent |
src/sim/network/bounds.js, gateways.js |
The map's border and the rule that roads stay inside it; entry/exit points with their sources and sinks outside |
src/sim/network/roundabouts.js |
Roundabout prefab: generated ring, merge/diverge nodes, arms; place, rebuild, delete |
src/sim/network/places.js |
Places vehicles call at: buildings with a dock (a spur off the road), bus stops in the lane or in a layby; bus routes |
src/editor/ |
Editor state machine and hit testing, in world coordinates (no DOM/p5) |
src/sim/FixedStepClock.js, Simulation.js |
Fixed 30 Hz timestep, independent of frame rate |
src/sim/routing/astar.js |
A* over lane edges via enabled connectors and lane changes (travel-time cost); returns a Route |
src/sim/vehicles/ |
Vehicle and Traffic: spawning at sources, driving routes, despawning at sinks; ordered vehicle list per lane/connector |
src/sim/vehicles/vehicleTypes.js |
Vehicle types as data (car, van, bus, lorry): size, sprite, spawn weight, driver parameters, loaded state |
src/sim/vehicles/idm.js |
Intelligent Driver Model: acceleration from speed, gap and closing speed; default driver parameters |
src/sim/vehicles/VirtualObstacles.js |
Stopped virtual vehicles any system can place on a lane or connector |
src/sim/vehicles/speedProfile.js |
Bend radius along a path and the speed a driver takes it at |
src/sim/vehicles/JunctionControl.js |
Default right of way at junctions: first come, first served, with a deadlock tie-break |
src/sim/vehicles/RoundaboutControl.js |
Giving way to the ring at roundabout entries: time-based gap check, deadlock tie-break |
src/sim/vehicles/Itineraries.js |
Calling at places on the way: stopping, dwelling, going into buildings and back out, loading and delivering |
src/sim/vehicles/GiveWayControl.js |
Giving way to the road when pulling out of a building's spur: time-based gap check, deadlock tie-break |
src/sim/vehicles/mobil.js |
MOBIL lane-change decision, route urgency, smoothstep lateral offset (pure functions) |
src/sim/vehicles/LaneChanging.js |
Lane changes each step: MOBIL choices, route-aware urgency, the change itself, zip merging |
src/sim/network/conflicts.js |
Which turn paths at a junction cross or merge |
src/sim/signals/ |
Traffic signals: plan.js (phase data, validation, default plan) and SignalSystem.js (a controller per junction) |
src/ui/signalPanel.js |
Editor panel for a junction's signal phases |
src/scenarios/ |
Maps: the default townMap.js (generated from a seed) and hard-coded test maps (crossroadsTestMap.js, demoNetwork.js, laneDropTestMap.js, roundaboutTestMap.js, deliveriesTestMap.js), registered in index.js |
src/render/roadGeometry.js |
Pure drawing geometry: tarmac bands, lane markings, junction outlines (no p5) |
src/render/roadAnnotations.js |
Where speed roundels and road names go (no p5) |
src/render/ |
p5 camera and network renderer (read-only view of sim state) |
src/ui/ |
Menus, dialogs, collapsible panels, key combos (plain DOM helpers) |
src/main.js, index.html |
Wiring: menu bar, side panel, status bar, dialogs, keyboard shortcuts |
UI
- Menu bar: File (New, Save, Load…), Edit (Undo, Redo), Help (Keyboard shortcuts), the tool buttons, and the Edit / Run switch (Space).
- Side panel: collapsible sections. Selection shows only when something is selected, New road only while the Create tool is active, then Network and Under cursor. Collapsed sections are remembered.
- Status bar: on the left, a Debug overlays button (showing how many are on) that opens the list of overlays, then the frame rate (red below 30 FPS; the simulation itself always steps 30 times a second; hover it for how much of the map was drawn); on the right, hints for the current tool, or refused actions in red for a few seconds.
- Drawing vs simulating: the whole map is simulated every step, but only
what is on screen (plus a 40-pixel margin) is drawn: roads, junctions,
vehicles, markings, signals and every overlay are skipped when off screen
(
NetworkRenderer, usingCamera.visibleRectand bounding boxes cached with each road's and lane's geometry). - View: scroll to zoom, drag empty ground to pan. The view stays on the map: it can't zoom out further than the whole map in view, or pan beyond its border (plus a 40-pixel margin). The roads and sources outside the border are glimpsed at the edges only.
- Load… opens a window with a Library tab (example maps; saved maps to come) and an Upload JSON tab (choose or drop a file).
- ? (or Help → Keyboard shortcuts) lists every shortcut; the list is built from the same table the key handler uses. Close with ×, Esc or a click outside.
Default map
The app starts on a generated town (buildTownMap({ seed }) in
src/scenarios/townMap.js), 600 × 480 m and laid out afresh each time it is
loaded (Town in Load…; the same seed always gives the same town):
- a loose 3 × 3 grid of junctions, nudged off the grid, with some local roads gently curved and a two-lane-each-way High Street across the middle;
- up to two grid roads left out (never leaving a dead end or cutting the town in two) and often a diagonal cut-through, so the loops vary;
- entry/exit points at both ends of the High Street and the middle north-south road, plus two to four more at random places on the border, each joined to the nearest junction.
Anything that would break the map's rules (a corner too sharp, a road over the border) is left out. The hard-coded test maps are still in Load….
Map size, border and entry/exit points
Every map has a size (network.bounds: top-left x, y, width,
height in metres; 1000 × 800 m for a new map), saved with it and edited in
the Map panel. It is drawn as a dashed yellow border, with everything
outside it darkened. Roads the player builds must stay inside it with room
for their width: their tarmac, plus 1 m, can't reach the border
(src/sim/network/bounds.js). Roads drawn over it are refused (the preview
turns red), dragged nodes and bend handles stop at it, and widening a road
that would then cross it is refused. Shrinking the map is refused if a road
would end up outside. Maps saved before maps had a size get a border fitted
round what is on them.
Entry/exit points (src/sim/network/gateways.js) are where roads cross
the border. Each is a node on the border (role: 'gateway') with a source
and sink 100 m from it (role: 'external'), joined by a link road. Link
roads vary: each leaves the border square to it, then curves off to a source
and sink up to 25° off straight out. The shape follows from the entry/exit
point's id, so it stays the same when the point is moved or the map is
saved. Link roads never cross or touch each other: if a shape would, milder
or mirrored versions are tried, and straight out if nothing else fits. The outside world can't be modified (outside the map editor's
entry/exit tools): its node and link road can't be selected, moved, split,
joined or deleted. Clicking the link road selects the entry/exit point.
Vehicles spawn at those external sources and finish at the external sinks,
so there is always somewhere to spawn, and the player always has points to
connect roads to.
- Create: the Entry/exit tool (E) adds one at the border point nearest the click (at least 20 m from a corner and from another entry/exit point), using the lanes in / out and speed limit in its panel.
- Edit: select it to change its lanes in and out, speed limit, lane width and name. Its lanes in and out also set the lanes of the road joined to it inside the map, so no lane ends at the border. Roads drawn to it, or joined or dropped onto it, take its lanes too. A change the joined road can't take (too wide for its bend or for the border) is refused. Drag it (Select tool) to slide it along the border: its source and sink follow, and moves that would make a corner too sharp or take a joined road over the border are refused.
- Delete: removes it with its source, sink and link road, and any road joined to it.
- Draw roads to it like any node. Joining a road end onto it keeps it where it is, on the border. Resizing the map keeps each one on its side of the border, at the same place along it where it can.
Editor
Ctrl+Z undoes the last action and Ctrl+Y / Ctrl+Shift+Z redoes it (100 steps kept, cleared when a new map is loaded). Each click, drag or panel change is one step.
| Tool | Key | Use |
|---|---|---|
| Select | V | Click a road, node or connector. Drag a node to move it; drop it on another node to join them. Drag a selected road's midpoint handle to bend it. Click a connector to toggle that turn. Edit lanes forward/back, speed limit, lane width and name in the panel (lanes regenerate immediately; refused values snap back when you leave the field). Delete removes the selection. |
| Create | D | Click start, click end. Shift+click before the end to add a bend the road passes through. Clicks snap to nodes; clicking on an existing road splits it into a junction. Keeps drawing from the last end until Esc / right-click. |
| Join | J | Click an end point, then a node (merge) or the middle of a road (T-junction). |
| Entry/exit | E | Click near the map border to add an entry/exit point there (see above). |
| Roundabout | U | Click to place a roundabout (radius and arms in the New roundabout panel); click a junction or road end to replace it with one, its roads joined on (see Roundabouts below). |
| Stops | T | Click on (or just beside) a road to add what the New stop panel says: a building (plain, depot or drop-off) beside it on the side clicked, or a bus stop serving the lanes on that side, in the lane or in a layby (see Itineraries below). Select one to name it, change how long vehicles stay, and a building's kind and number of bays. |
| Bus route | L | Click bus stops in the order buses call at them, then Save route. Select a route (Edit in the Buses & deliveries panel) to rename it, set how many buses run on it, change its stops or delete it. |
Bends can't be tighter than the road's half-width + 1 m (centreline radius), so the inside edge never folds over itself. Bend handles and Shift+click bends stop at that limit (the drawing preview turns orange), dragging a node eases any attached bend it would over-tighten, and adding lanes to a bend that's already too tight for the new width is refused with a message.
Roads meeting at a node must be at least 40° apart (MIN_ROAD_ANGLE in
src/sim/network/corners.js). Drawing a road that would break this is refused
(the preview turns red), dragged nodes and bend handles stop at the last valid
position, and joins that would make a sharper corner are refused. At any
allowed angle the junction grows until neighbouring roads no longer overlap
outside it.
Junction areas are drawn like the roads (same tarmac, edge lines) with
rounded kerb returns joining each road's edge to the next. Junctions are sized
so each return can reach KERB_RETURN_RADIUS (8 m, in corners.js); a
corner without room uses the largest arc that fits. Where exactly two roads
meet (a bend, or one road continuing into another) the centre line and lane
dividers also carry on through the junction; at real junctions (three or more
roads) they stop at the mouth. Dead ends are rounded off with a half-circle as
wide as the road, edged the same way, and their U-turn paths are half-circles
inside it.
Junctions resize to fit their roads, and default turns are rebuilt whenever roads meet or change (UK rules: straight on from every lane, left from the leftmost lane, right from the rightmost; U-turns only at dead ends). Turns you switch off stay off while their lanes exist. Where a road simply carries on with fewer lanes (two roads meeting end to end), the extra kerb-side lanes end there: a lane drop, whose traffic has to merge before the node.
Speed-limit roundels (white number in a white ring, as road paint) are painted in every lane at the start of a road section whose limit differs from a section leading into it.
Traffic
Stops on the way. In the app (maxStops: 3; Traffic's default is 0)
each randomly spawned vehicle also has 0–3 stops: random points on roads
inside the map, on lanes between two of its junctions (not those leading in
from or out to an entry/exit point). It drives from its source to each stop
in turn, then to its sink. A stop is made by getting onto that road going
that way, in any lane. Its route only ever runs to the next stop (or the
sink), and every re-plan (lane changes, giving up waiting, rejoining after an
edit) aims there (Traffic.planFrom). A stop whose road has been removed, or
that can't be reached from where the vehicle is, is dropped, and it carries
on to the next. With at least one stop, a trip may start and end at the same
entry/exit point. The vehicle's trip is on vehicle.trip ({ to, stops }).
Auto-spawning. While the simulation runs, vehicles keep appearing at
random sources: Vehicles per second (default 1) up to Max vehicles on
the map (default 200), set in the Map panel with Auto-spawn to switch it
off. It runs in simulation time (Simulation.autoSpawn), so nothing appears
while paused; if every source is full, at most one vehicle is kept waiting,
so there is no burst when room comes. These are game settings for now, not
saved with the map.
Every entry/exit point has a source and a sink 100 m outside the map (see
above). Any other node can also be tagged: select it and tick Source
(vehicles appear on the lanes leaving it) and/or Sink (vehicles finish at
the end of a lane arriving there). The example maps have entry/exit points at
their road ends. Press S (or Spawn vehicle in
the Traffic panel) to spawn one vehicle at a random source, provided the first
lane of its route has SPAWN_CLEARANCE (12 m) free, heading for a random
sink it can reach. Its route comes from A* over lane edges and enabled
connectors, minimising travel time at the speed limits. Each vehicle tracks its
current leg (lane or connector) and its progress along it (0-1, shown under
the cursor), and despawns on reaching the end of its last lane. Vehicles move
only in Run mode.
Editing with traffic on the map. Edits don't remove the traffic. While
editing, vehicles stay where they were. When the simulation runs again,
sim.networkChanged() (Traffic.networkChanged) rebuilds what was worked
out from the map's shape, clears the obstacles, and puts each vehicle back
on the nearest lane running within 60° of the way it was going (any way, if
none does). It is slid along that lane to the nearest spot with room, and its
trip is planned again from there: to its destination if that is still
reachable, otherwise to the nearest sink it can reach. It keeps its speed,
unless that would run it into what is now ahead. So a vehicle on a road that
was deleted moves to the nearest suitable road. Changed lanes or turns are
picked up by the new plan. Vehicles on roads that didn't change carry on as
before. A vehicle with no lane with room or no way to any sink is removed,
and the status bar says how many.
Vehicle types
Vehicles come in four types, defined as data in vehicleTypes.js
(VEHICLE_TYPES): a length and width, a sprite, a weight (how often it is
picked at random), and the IDM/MOBIL parameters its driver uses over the
defaults below.
| Type | Size | Weight | Driver, compared with a car |
|---|---|---|---|
| Car | 4.5 × 1.8 m | 70 | The defaults |
| Van | 5.6 × 2.0 m | 15 | A little gentler to speed up and brake, slightly longer headway and merge gaps |
| Bus | 12 × 2.55 m | 5 | 50 mph at most, gentle acceleration (1.0 m/s²) and braking (1.5 m/s²), 2 s headway, wants 3 m spare to merge and 1 s extra to pull out |
| Lorry | 16.5 × 2.55 m | 10 | 56 mph at most, accelerates at 0.8 m/s², brakes at 1.3 m/s² (so starts braking further back), 2.2 s headway, 3.5 m stopped gap, slow lane changes (3 s) that need 4 m spare, 1.5 s extra to pull out onto a roundabout |
Vans, buses and lorries can be loaded (a bus "full"), by chance on spawning
(50–60 %). Loaded, a vehicle speeds up and brakes more gently still: a loaded
lorry has 0.7 × the acceleration and 0.8 × the comfortable braking of an
empty one (driverFor(type, loaded)). The lorry is drawn as a cab and a
trailer hinged behind it that follows the path round corners; a loaded
trailer shows its load. Vans and buses have their own sprites too.
Spawning a type. S spawns a vehicle whose type is picked at random by
the weights (pickType); 1–4 (or the type buttons in the Traffic
panel) spawn a car, van, bus or lorry. Auto-spawning uses the weights. In
code: traffic.spawnRandom({ type: 'lorry', loaded: true }),
spawnOnRoute(route, { type, loaded }); Traffic/Simulation with
vehicleMix: true picks types at random (otherwise everything is a car), or
vehicleMix: { car: 1, lorry: 1 } for weights of your own. Hovering a
vehicle shows its type, and the stats show the mix on the map.
Itineraries: buildings, bus stops, routes and jobs
A vehicle's trip (vehicle.trip) is an ordered list of stops, then a sink.
Besides points on roads (above), a stop can be a call at a place
(network.places, places.js), with how long it stays (dwell, s) and what
it does there (action: load or deliver). Each leg is planned with A*
right to the lane the place is on (planRoute(…, { edge })), and on from
there once the call is made (Itineraries.js):
- Buildings with a dock. Placing one splits the road into a T-junction,
with a 32 m two-way spur (20 mph) to a dock node at the building. There
are no turns at the dock node, so nothing routes through it. A vehicle
calling there drives to the end of the spur's lane in, and once one of the
building's bays (
capacity) is free it is taken off the road: hidden, taking no road space, but still counted. With every bay taken, callers wait at the end of the spur and queue behind (back onto the road, if it comes to it). After its stay, once the spur's lane out has room, it comes back out from a standstill and gives way to the road (GiveWayControl.js). As at roundabouts, a time-based gap check looks 120 m back along every lane into the junction for traffic whose way crosses or joins its turn. It goes only if they would get there after it has cleared its turn, plus 0.5 s and its owngiveWayMargin(more for a lorry); and those joining the same lane, after their following headway too. Until let go it doesn't ask the junction for its turn, so first come, first served can't hand it the road. There is a deadlock tie-break as at roundabouts. A building is a plain building (cars and vans call there), a depot (lorries load) or a drop-off (deliveries; cars and vans call too). - Bus stops, in the lane. A point on the kerb lane of the side clicked. A bus brakes to stop with its front at the spot and stands there for the stop's dwell time, as a stopped vehicle: traffic behind changes lanes round it (MOBIL) where there is another lane, and queues where there isn't.
- Bus stops, in a layby. A 90 m stretch of the road is split off and given
an extra kerb lane, the bay (
LaneEdge.layby, fromLogicalRoad.layby). It has no turns into or out of it, and A* never uses it except as the target, so only a bus calling there changes lanes into it. It stops 16 m short of the bay's end, stands, then has to change back out. Pulling out it has priority: it indicates at once, and drivers in the lane make room braking up to their comfortable rate (others merging only get gentle braking). Its stretch of road can't be split, moved or deleted on its own. - Bus routes (
network.busRoutes): a fixed loop of bus stops. A bus on a route (trip.loop) calls at each in turn, and after the last starts again from the first, for ever. The simulation keeps each route's number of buses (buses, default 2) going round it, one setting off every 20 s from the source nearest its first stop while it is short (Simulation.busService). If a route has more than it wants, the extras finish their lap and leave. Random buses go once round a random route (75 %). - Lorry jobs (
traffic.spawnLorryJob(), X): empty to a random depot to load, then to 1 to 3 random drop-offs. A loaded vehicle speeds up and brakes more gently (setLoadedinvehicleTypes.js); it is unloaded after its last delivery, then heads for a sink. With a depot and drop-offs on the map, most random lorries (75 %) run a job. Cars and vans with random stops call at buildings (other than depots) for some of them (35 %). - Missed stops. A vehicle that finds itself past its stop on that lane (a late lane change, an edit) goes round the block to call there, or gives the stop up if there is no way round.
- Editing keeps vehicles in buildings inside. If their building is deleted they come back out onto the nearest road. Calls at places that have gone are dropped. Deleting a bus stop takes it off any route.
In code: traffic.spawnItinerary({ type, loaded, stops: [placeId | { placeId, dwell, action }], loop, source, sink }), traffic.spawnBus(routeId),
traffic.spawnLorryJob(); places.addDock, addBusStop, addBusRoute
(see deliveriesTestMap.js). Deliveries and buses in Load… has all of
these: a depot, three drop-offs and a shop, in-lane and layby bus stops, and
two routes. It is a small map, so set auto-spawn to about 0.3 vehicles a
second there; at 1 a second it jams. The Buses & deliveries panel lists the routes
with their buses (+ Bus sends another) and has a Lorry job button.
Hovering a vehicle shows its next call and how long it has left at a stop;
hovering a building shows who is inside.
Car following (IDM)
Speeds come from the Intelligent Driver Model (src/sim/vehicles/idm.js).
Each vehicle carries its own driver parameters (defaults in
DEFAULT_DRIVER; pass driver: {…} to Traffic or Simulation to override):
| Parameter | Default | Meaning |
|---|---|---|
desiredSpeed |
31.3 m/s (70 mph) | Speed on an empty road; capped by the lane's limit (a connector uses the lower of its two lanes' limits) |
timeHeadway |
1.5 s | Time gap kept to the vehicle ahead |
minGap |
2 m | Bumper-to-bumper gap when stopped |
maxAccel |
1.5 m/s² | Strongest acceleration |
comfortDecel |
2 m/s² | Braking the driver plans for |
maxLateralAccel |
2.5 m/s² | Sideways acceleration accepted in a bend: sets the speed through curves and turns |
speedFactor |
1 | Multiplies the speed limit when picking a cruising speed |
politeness |
0.3 | MOBIL: how much others' gain or loss counts against the driver's own (0 selfish, 1 altruistic) |
safeDecel |
2.5 m/s² | MOBIL: hardest braking a lane change may force on the vehicle it moves in front of (or on itself) |
changeThreshold |
0.2 m/s² | MOBIL: advantage a lane change must bring before the driver bothers |
laneChangeTime |
1.5 s | How long a lane change takes at a steady speed |
mergeSpace |
1 m | Spare room, beyond the vehicles' own gaps, wanted to change lanes into |
giveWayMargin |
0 s | Extra time wanted over GAP_MARGIN before pulling out onto a roundabout |
length, width (on the vehicle) |
from its type |
In the app, traffic is made with variety: true: each spawned driver gets
their own speedFactor (0.9–1.1), and their type's politeness and
laneChangeTime varied (± 0.25, and × 0.75–1.25). traffic.spawnOnRoute(route, { driver }) sets one
vehicle's parameters.
Every step, Traffic.nearestAhead(vehicle) finds what the driver has to react
to ahead along the vehicle's route, across lane and connector boundaries, up
to LOOKAHEAD (150 m, or further for a slow-braking vehicle at speed:
1.3 × the gap IDM wants at its speed): the vehicle or virtual obstacle that calls for the
most braking. That is nearly always the nearest, but a red light just beyond a
moving car wins. A vehicle turning off a lane another way still counts until
its tail has cleared the lane. All drivers choose an acceleration from the
same snapshot, then all move. What each is reacting to is kept on
vehicle.ahead ({ gap, speed, accel, vehicle | obstacle }) and its
acceleration on vehicle.accel. New vehicles appear already moving, at the
speed limit or as fast as the road and traffic ahead comfortably allow.
For a stop that appears close ahead (a light changing, or a plan changing
after a lane change), the model would brake harder than stopping needs. A
driver who would brake harder than comfortDecel for an obstacle brakes
only 1.3 × as hard as stopping there takes.
Drivers also slow down in advance for bends and lower speed limits. Each
lane and turn path is split into zones by bend radius (speedProfile.js); a
zone's speed is sqrt(maxLateralAccel × radius), capped by the limit. A
driver brakes at comfortDecel so as to be down to each slower zone's speed
as they reach it, so a tight left turn at the crossroads is taken at about
10 mph and a 60 → 20 mph change starts braking about 150 m out.
Lane changing (MOBIL)
Every step, each driver on a multi-lane road weighs moving one lane left or
right with MOBIL (mobil.js, run by traffic.laneChanging, LaneChanging.js):
- Incentive: own gain in acceleration, plus
politeness× the gain or loss for the vehicle that would end up behind it and the one it leaves behind, plus a small preference for the kerb lane, must beatchangeThreshold. - Safety: neither the new follower nor the driver may have to brake harder
than
safeDecel, and there must be at least 1 m clear in front and behind. - By choice only: not in the last 25 m before a junction, not within 4 s of the last change, and never into a lane the driver would soon have to leave.
Route-aware. A* plans lane changes as well as turns (at a 3 s penalty
each). A route that needs one stops at the lane where it starts and lists the
lanes to move into (route.laneChanges); after each change the trip is
planned again from the new lane (Route.join). Traffic also starts in random
lanes: most drivers pick one that suits their first turn, a quarter don't.
A driver who still needs a change gets an urgency that grows from 0 to 1
over the last 120 m per change needed (urgency()). Urgency adds up to
3 m/s² of incentive and makes the driver less polite and less fussy. A driver
who must change also brakes to wait 10 m short of the end of the lane
(END_MARGIN; "needs another lane" / "lane ends" under the cursor) rather
than drive on, and indicates. If they wait there for 20 s (2 s if the car
alongside wants to swap lanes with them and arrived first), they give up and
find another way on from the lane they are in.
The change itself. The vehicle joins the new lane's ordered list at once,
the same fraction of the way along (so it neither jumps ahead nor drops back),
and is drawn easing across with a smoothstep sideways offset over
speed × laneChangeTime metres (at least 10 m). The offset follows distance
along the route, and front and rear are each offset by where the vehicle was
at that distance, so the body turns into the change and back. Until its tail
is across, the vehicle still counts in the lane it left: drivers there stop
behind it, and it keeps clear of what is ahead there. Junction requests stay
in driving order across the change.
Lane drops: zip merging. A lane that ends is just a lane every route has to leave. Drivers in it brake towards its end. Once their urgency passes one half, drivers in the next lane make room for them, braking up to 2 m/s² × (0.5 + politeness) for one that is ahead of them. The two streams then take turns. Lane drop test map (Load…) shows a three-lane road losing its kerb lane, then a T-junction where traffic for the side road needs the offside lane.
Turn indicators flash on vehicles that are changing or want to change lanes. Planned vehicle routes (P) shows a planned lane change as the line moving across, and Under cursor shows a vehicle's lane state and politeness.
Junctions
Turn paths (connectors) at a junction conflict when they cross, come
within 2 m of each other, or join the same lane (network/conflicts.js).
Turns out of the same lane never conflict. A bus or lorry (over 7 m) cuts
corners: its body is a straight line from front to rear, both on its path.
So for those, a turn also conflicts with any other path its body sweeps
across on the way through (sweptConflict). Two vehicles clash if either's
turn conflicts with the other's (junctions.clash), and a turn needs right
of way if it conflicts with anything, for any vehicle (junctions.asks).
By default a junction is first come, first served
(traffic.junctions, JunctionControl.js). A vehicle asks for its turn path
when it gets to where it would have to start braking for the stop line. It is
given the path once no conflicting path is held or was asked for earlier, and
keeps it until its tail has left the junction; until then it waits at the end
of its lane (behind a virtual obstacle only it can see). Vehicles on paths
that don't conflict cross together. A vehicle is not given the junction while
it is stuck close behind one that hasn't been given its own turn (in its lane,
changing into it, or, while its own tail is still across, in the lane it is
leaving). Such a request doesn't count as "earlier" for anyone else either,
so a lane change can't tie the junction in a knot. Deadlock tie-break: if
nobody at a junction has moved for 8 s (DEADLOCK_WAIT) while vehicles wait
at its stop lines, the longest-waiting one whose turn path is physically
clear (nothing on it or on any path crossing it, and room beyond) goes,
taking the junction from anyone holding it who hasn't moved into it. A vehicle that is close
to a junction but not yet on the lane leading to it (a short road after
another junction, or a fast arrival) asks already, once everyone between it
and the junction has, so a refusal still leaves room to stop. It ranks
behind everyone already on that lane. There is no road hierarchy yet: a side
road and a main road take turns. Another system can take a junction over with
traffic.junctions.setControlled(node, false), as the signals do.
A vehicle on a turn follows one on another turn into the same lane that is nearer where they join. Before its own turn, only traffic that won't be stopped first looks out for those (traffic round a roundabout's ring, for vehicles about to join it).
Don't block the box (traffic.exitHasRoom(vehicle, connector)): at any
junction with conflicting paths, signalled or not, a vehicle won't enter
unless there is room for it on the lane it is heading for. Traffic moving
off down that lane (2 m/s or more) counts as room; a stopped or crawling
queue, or an obstacle, must leave a car length plus the minimum gap, less the
space already claimed by vehicles crossing to the same lane.
A vehicle that is too close to the end of its lane to stop comfortably, with
nothing holding it, is committed (vehicle.committed): it ignores a
light that then changes or an exit that then fills, rather than braking hard.
Roundabouts
The Roundabout tool (U) places a roundabout prefab with the radius and number of arms set in its panel (defaults 25 m and 4). Clicked on a junction or road end, it replaces it: centred there, with an arm lined up with each road (more in the widest gaps, up to the number asked for) and the roads joined on; the radius grows if the roads come in close together. Select one (click its ring) to change its radius or arms (it is rebuilt and the roads join back on to the arm nearest where they were) or delete it (its roads then meet at a plain junction). Roads drawn with the Create tool snap onto an arm when clicked on or near the roundabout, and must come in from outside it. Roundabout test map in Load… is a four-arm one.
It is generated (src/sim/network/roundabouts.js) from a record in
network.roundabouts (saved with the map): a ring of one-way lanes, true
circular arcs, going clockwise when driving on the left (UK),
anticlockwise on the right. For each arm, traffic leaves the ring at a
diverge node and, a little further round, joins it at a merge node.
An arm node 20 m out is where a road joins; an entry lane runs in from it to
the merge node, curving to meet the ring going its way, and an exit lane
comes back out from the diverge node, side by side like a two-way road. The
ordinary turn generator links them all up, so A* routes through
roundabouts with nothing special: from the west, north (left) is the first
exit, east the second and south the third. A roundabout's nodes and lanes
can't be moved or deleted on their own, only with it.
Giving way (RoundaboutControl.js): traffic on the ring has priority.
Each entry lane has a give-way line (a virtual obstacle at its end, drawn
as a dashed double line) holding the first vehicle there until a
time-based gap check lets it go:
- walk back along the ring from the merge point for 80 m
(
SIGHT_DISTANCE), finding the vehicles that will come past it (not those turning off first), and when each will reach it (from its speed, speeding up at half its strongest acceleration to the limit); - work out how long this vehicle needs to accelerate and clear the merge
point, tail and all (
timeToCover); - go only if the ring traffic arrives later than that by its own following
headway (so it isn't left right behind) plus 0.5 s (
GAP_MARGIN) plus its owngiveWayMargin(so a lorry wants a bigger gap), the merge point itself is clear, and there is room on the ring beyond.
Once on its way it keeps going with half that headway to spare, rather than
stop and start for every small change, and once it is too close to stop
comfortably it carries on. Deadlock tie-break: after waiting 8 s
(DEADLOCK_WAIT) behind ring traffic that is standing still (a jammed ring),
a vehicle goes anyway, provided the merge point is clear and there is room.
Traffic signals
Select a junction in Edit mode and press Add signals in the Selection panel. That creates a starting plan in which every turn gets a green and no phase has conflicting turns; then, per phase (tabs 1 2 3 … +):
- set the green, amber and all-red times in seconds;
- tick the turns that are green. Turns that cross or merge with one already ticked are greyed out (hover for which). Pointing at a turn in the list highlights it on the map, where the phase's green turns are drawn green and the rest red;
- Remove phase / Remove signals. Every change is one undo step.
Phases run in order, round and round: green, amber, all red, next phase. A turn that is green in two consecutive phases stays green through the change. A junction whose signals are running has a solid white stop line across each arriving lane; any other junction of three or more roads has a dashed give-way line there.
On red and amber the controller puts a virtual obstacle at the stop line of each lane with a turn that isn't green (and lifts it when all are); it only stops vehicles whose own turn is not green. On amber, vehicles that can stop comfortably do, and the rest carry on (they are committed, above). On the map each signalled turn shows its light as a coloured stub at its start.
Plans are saved with the map (signals in the JSON, RoadNetwork.signals /
setSignalPlan(nodeId, plan)), and refer to turns by the lanes they join
("<fromEdgeId>><toEdgeId>"), so they survive most edits; turns that
disappear drop out of the plan.
net.setSignalPlan('centre', { phases: [
{ green: ['majorW/b0>majorE/f0', …], greenTime: 20, amberTime: 3, allRedTime: 2 },
…
] });
validateSignalPlan(net, 'centre', plan); // { errors: [...], warnings: [...] }
validateSignalPlan reports conflicting turns green in the same phase and bad
times as errors, and turns that are never green as warnings. A plan with
errors is shown in red in the panel and is not run: that junction stays
first come, first served until it is fixed. The signals are their own system
(sim.signals, a SignalSystem with a SignalController per junction),
stepped just before the traffic.
Virtual obstacles
traffic.obstacles (VirtualObstacles) lets any system put a stopped virtual
vehicle at a point on a lane or connector; drivers treat it exactly like the
rear of a stationary car and queue behind it.
const stop = traffic.obstacles.add({ leg: edge, s: edge.length, owner: 'signals' }); // e.g. a red light
traffic.obstacles.remove(stop); // or removeOwnedBy('signals')
appliesTo: (vehicle) => boolean limits an obstacle to some vehicles (e.g. a
give-way that only concerns one turn). To try it: point at a lane and press
B to block it there (a red bar), B again on the bar to remove it, or
Clear obstacles in the Traffic panel.
Click a vehicle (in Run mode, or with the Select tool) or point at it and press F to select it and have the camera follow it; zooming then stays centred on it. Esc, F, dragging the view or the vehicle arriving stops following. The selected vehicle always shows its speed and gap, its route, its numbered stops still to make and where it leaves the map, whether or not the Vehicle routes and Vehicle speed & gap overlays are on (they show everyone's).
Debug overlay
Toggle from the Debug overlays list (bottom left) or with keys: C junction connectors (hidden at simple bends, where there is no turn choice), A lane direction arrows, N node IDs, O lane offsets (metres, + = right of travel), R road names (written where each named section begins), P planned vehicle routes, G each vehicle's speed and gap, K sources and sinks (rings and tags; off by default), I entry/exit points (the IN / OUT bar and name at each; off by default, but always shown while the Entry/exit tool is in use or one is selected). Click a connector to enable/disable that turn.
Which overlays are on is kept in a cookie (trafficSimOverlays, for a
year), so they come back as you left them next time. Without the cookie
(first visit, or cookies blocked) they start as the page sets them.
With Vehicle speed & gap on, every vehicle is labelled with its speed and, when it is reacting to something, the gap to it in metres, with a line along its route from its front bumper to that vehicle, obstacle or stop line. Labels are green while accelerating, red while braking. Pointing at a vehicle also shows its target speed, acceleration, gap and what it is behind in Under cursor (a vehicle, or "red light", "give way", "exit blocked", "obstacle").
Conventions
- Units: metres, seconds, m/s. World y points down (screen-aligned), so a positive heading change is a right turn.
lateralOffsetis measured from the road centreline, positive = right of the edge's own direction of travel.- Lane index 0 is the offside (overtaking) lane; the highest index is the kerb lane.
- Road-generated edge ids are deterministic (
<roadId>/f0,<roadId>/b1), so editing a road keeps surviving lanes' connectors and enabled flags. Node.radiusis the junction area: lanes are trimmed back by it and connectors (turn paths) cross it.