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, and PathData instances 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.unity

  • Script: 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

  1. Initialization & Pooling Warmup: On Start(), the sample initializes the RuntimeGenerationPool and calls RuntimeGenerationUtils.Warmup() to pre-allocate internal collection capacities, eliminating garbage collector allocations during road generation.

  2. 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.

  3. Configuring TrafficNodeData Instances: For each boundary node, a pooled TrafficNodeData instance 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.

  4. 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().

  5. Registering with Simulation World: Once segments are generated, they are registered synchronously with the DOTS world using runtimeRoadManagerCustom.AddSegmentsSync(newSegments).

  6. 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.Nodes contains fully initialized TrafficNodeData objects with accurate positions and properties before passing it to GenerateAutoCrossroad().

  • Ensure Correct Node Orientation & Alignment: Always place boundary TrafficNodeData nodes 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() and PopTrafficNode() 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.