Custom Runtime Segment Generation
Overview
The DOTS Traffic City framework provides a flexible pipeline for generating road network topology completely at runtime. Instead of relying purely on pre-baked authoring assets in the Unity Editor, you can programmatically construct straight road segments, build complex crossroads, and link pedestrian paths on the fly.
This approach is ideal for procedural city generation, dynamic map chunk loading, or custom runtime road creation tools.
Key Components & Utilities
RuntimeGenerationUtils: A static helper class containing low-level path calculations, Bezier curve evaluations, straight segment math, and automated crossroad topology generation.
TrafficNodeData: A lightweight runtime data structure representing an entry/exit boundary node of a road segment. It defines lane counts, widths, crosswalk settings, and position/rotation transforms for connecting adjacent roads.
RuntimeGenerationPool: A zero-allocation object pool used to pop and recycle
RuntimeSegmentCustom,TrafficNodeData, andPathDatainstances during runtime operations.RuntimeRoadManagerCustom: The runtime manager responsible for registering generated road segments into the live DOTS simulation world.
Segment Connection Principles
Adjacent road segments seamlessly snap together at their boundary TrafficNodeData nodes.
When connecting two segments (e.g., a straight road segment and an intersection):
Position: The boundary nodes of both connecting segments must share the exact same world position.
Rotation: The boundary nodes must have opposite (inverted) rotations—facing 180 degrees away from each other.
This inverse rotation aligns incoming lanes from one segment directly with outgoing lanes of the adjacent segment.
Generating Straight Roads
Straight road segments connect two boundary traffic nodes (Start & End) and can consist of straight lanes, crosswalks, and optional pedestrian side paths along the segment.
To generate a straight road segment, create two boundary nodes, define guide path waypoints, and call RuntimeGenerationUtils.GenerateStraightSegment():
// 1. Pop or instantiate a new custom segment
var newSegment = runtimeGenerationPool.PopSegment();
// 2. Instantiate and configure Start (index 0) and End (index 1) boundary nodes
for (int i = 0; i < 2; i++)
{
var node = runtimeGenerationPool.PopTrafficNode();
node.Owner = newSegment;
node.LocalNodeIndex = i;
node.Position = nodePositions[i];
node.Rotation = nodeRotations[i]; // Facing opposite to entrance vector
node.LaneCount = 2;
node.LaneWidth = 3.75f;
newSegment.Nodes.Add(node);
}
// 3. Define path waypoints (e.g. start position, intermediate spline points, end position)
List<Vector3> waypoints = new List<Vector3> { startPos, endPos };
// 4. Configure straight segment parameters
var straightSettings = new RoadSceneUtils.StraightSegmentSettings()
{
LaneCount = 2,
LaneWidth = 3.75f,
IsOneWay = false,
IsEndOfOneWay = false,
AddCrosswalk = true,
AddPedestrianNodes = true,
AddPedestrianAlongLine = true,
PedestrianNodeSpacing = 5f
};
float speedLimit = 60f; // Speed limit in km/h
// 5. Generate parallel lane paths, crosswalks, and pedestrian routes
RuntimeGenerationUtils.GenerateStraightSegment(newSegment, waypoints, straightSettings, speedLimit);
Note
Straight road segments always require exactly two boundary nodes in newSegment.Nodes (index 0 for Start, index 1 for End).
Generating Auto Crossroads
The framework provides automated crossroad generation via RuntimeGenerationUtils.GenerateAutoCrossroad(). There are two primary workflows for generating an intersection segment:
1. Standard Crossroad Setup
Populate a RuntimeSegmentCustom instance with pre-configured boundary nodes (TrafficNodeData) explicitly defining the intersection’s outer entry/exit points, then pass the segment to the generator:
// 1. Pop or instantiate a new custom segment
var newSegment = runtimeGenerationPool.PopSegment();
// 2. Instantiate and configure boundary nodes with correct positions, rotations, and lane settings
for (int i = 0; i < nodeCount; i++)
{
var node = runtimeGenerationPool.PopTrafficNode();
node.Owner = newSegment;
node.LocalNodeIndex = i;
// Assign correct world position and facing direction (opposite to entrance)
node.Position = nodePositions[i];
node.Rotation = nodeRotations[i]; // Facing opposite to entrance vector
node.LaneCount = 2;
node.LaneWidth = 3.75f;
// Add boundary node to segment
newSegment.Nodes.Add(node);
}
// 3. Configure shared light state container and enable traffic light signal control for intersection nodes
newSegment.LightStateContainer = baseLightContainer;
newSegment.AddTrafficLights();
// 4. Generate internal paths, lane connections, U-turns, and crosswalks
RuntimeGenerationUtils.GenerateAutoCrossroad(newSegment, generationSettings);
Note
Calling newSegment.AddTrafficLights() marks the intersection’s traffic nodes to operate under traffic light control modes and assigns light indices to connected lane paths.
Important
Before calling GenerateAutoCrossroad(newSegment, ...), the newSegment.Nodes list must already be populated with valid TrafficNodeData instances containing precise world positions and orientations.
2. Stitching Existing Connected Straight Roads
You can also stitch together an intersection from existing straight road segments whose endpoint nodes already meet at the junction center:
// Collect boundary nodes from straight roads pointing to the junction
List<TrafficNodeData> connectedStraightRoadNodes = GetConnectedEndpointNodes();
// Generate the crossroad by connecting existing boundary nodes
RuntimeSegmentCustom newIntersection = RuntimeGenerationUtils.GenerateAutoCrossroad(
connectedStraightRoadNodes,
baseLightContainer,
generationSettings,
areExistsNodes: true
);
// Add the newly created intersection segment to your segment list
newSegments.Add(newIntersection);
Note
Setting areExistsNodes: true tells the utility to reuse the boundary transformations and configuration of existing nodes rather than instantiating new ones. This ensures seamless lane stitching between pre-existing straight roads and the new crossroad.
Sample Scene: Runtime Generation
The framework includes a dedicated sample scene demonstrating dynamic road segment generation from raw data and intersection creation at runtime:
Location:
Spirit604/DotsCity/Samples/RuntimeGenerationSample/RuntimeGenerationSample.unityScript:
RuntimeGenerationSample.cs
Note
Demonstration Notice: The road creators (RoadSegmentCreator) and scene objects present in this sample scene are used strictly as data containers to extract raw parameters (such as waypoint positions, lane widths, and connection rules) for API demonstration purposes.
You are not limited to using these editor components. The runtime generation API is entirely decoupled from scene MonoBehaviours, allowing you to feed your own custom data sources (e.g., procedural grid generators, tilemap databases, external JSON files, or network streams) directly into RuntimeGenerationUtils.
How the Sample Works
Initialization & Pooling Warmup: On
Start(), the sample initializes theRuntimeGenerationPooland callsRuntimeGenerationUtils.Warmup()to pre-allocate internal collection capacities, eliminating garbage collector allocations during road generation.Parsing Raw Road Data: The sample iterates over a list of reference editor road objects purely to read their underlying structural data (lane count, width, speed limits, pedestrian spacing) as an input source.
Configuring TrafficNodeData Instances: For each boundary node, a pooled
TrafficNodeDatainstance is populated with geometric parameters (lane counts, divider widths, crosswalk offsets) and assigned transform data. Node rotations are assigned facing opposite to the entrance direction of the node.Processing Straight Roads vs. Crossroads:
Straight Roads: Evaluates middle waypoint positions, pops nodes from the pool, and calls
RuntimeGenerationUtils.GenerateStraightSegment()to build multi-lane geometry and lateral pedestrian paths.Crossroads: Assigns shared traffic light containers and automatically computes turns, Bezier curves, and pedestrian links via
RuntimeGenerationUtils.GenerateAutoCrossroad().
Registering with Simulation World: Once segments are generated, they are registered synchronously with the DOTS world using
runtimeRoadManagerCustom.AddSegmentsSync(newSegments).Traffic Light & Frame Binding: Upon completion of segment addition, physical traffic light MonoBehaviours in the scene are bound to their respective crossroad IDs via
RegisterTrafficLights().
Best Practices
Populate Segment Nodes Before Generation: Ensure
newSegment.Nodescontains fully initializedTrafficNodeDataobjects with accurate positions and properties before passing it toGenerateAutoCrossroad().Ensure Correct Node Orientation & Alignment: Always place boundary
TrafficNodeDatanodes at the exact same position with inverted (opposite) rotations so adjacent road segments align and stitch seamlessly.Use Custom Data Sources: Feel free to replace the sample’s scene road references with your own custom runtime data structures or procedural algorithms.
Use Object Pooling: Always use
RuntimeGenerationPool.Instance.PopSegment()andPopTrafficNode()instead of instantiating new objects directly to prevent GC spikes during runtime generation.Recycle Removed Segments: When tearing down chunks or removing road segments, return them to the pool using
runtimeGenerationPool.RecycleSegment(segment).Pre-allocate with Warmup: Call
RuntimeGenerationUtils.Warmup()during game startup or scene loading to ensure zero-allocation performance when constructing road networks dynamically.