Implementation: Simulating Dual-Lane Evacuation Traffic


One thing we heard again and again while exploring wildfire evacuation stories was how challenging evacuations become, especially when residents are trying to take two cars to save more belongings or bring along trailers, pets, or equipment.

To bring this experience into the game, we needed a traffic system where cars could move along dual-direction roads and make meaningful choices under pressure. That’s why we developed a custom car navigation system. Why not NavMesh? For our small, grid-based map, NavMesh can be overkill and doesn’t handle our layout very well — we created a lightweight, flexible alternative.

How It Works

Every road tile prefab contains a set of manually placed Marker objects—think of these as nodes that define where cars can go.

public class Marker : MonoBehaviour
{
    public Vector3 Position => transform.position;
    public List<Marker> adjacentMarkers; // manually assigned
    [SerializeField] private bool openForConnections; // neighbor auto-link flag
    public bool OpenForconnections => openForConnections;
}

These are positioned and connected directly in the Unity Inspector, giving us clear control over:

  • Which direction cars can turn,
  • Which lane they’re in (inner or outer),
  • And how traffic flows through corners, intersections, or straightaways.

Markers on the same tile are manually linked using the adjacentMarkers list. For connections between neighboring tiles, we use a simple flag: if a marker has openForConnections enabled, the system will automatically connect it to the closest marker on the adjacent tile during pathfinding.

Under the Hood: Graph & AStar Pathfinding

When a car spawns, the ATC_AIDirector builds a custom graph from the tile path between the car’s start and destination.

  • Each Marker becomes a vertex.
  • Each adjacentMarker creates an edge.
  • If openForConnections is true, the system links to a nearby marker on the next tile.
foreach (var marker in markersList)
{
    graph.AddVertex(marker.Position);

    foreach (var neighbor in marker.adjacentMarkers)
    {
        graph.AddEdge(marker.Position, neighbor.Position);
    }

    if (marker.OpenForconnections && i + 1 < path.Count)
    {
        var nextTile = placementManager.GetStructureAt(path[i + 1]);
        var targetMarker = nextTile.GetNearestCarMarkerTo(marker.Position);
        graph.AddEdge(marker.Position, targetMarker);
    }
}

This graph is passed into a lightweight AStar pathfinding algorithm, which calculates the best route across connected markers. It works great for short routes and lets us simulate subtle but impactful traffic behavior.

The A* search is run like this:

public static List<Vector3> AStarSearch(AdjacencyGraph graph, Vector3 start, Vector3 end)
{
    var startVertex = graph.GetVertexAt(start);
    var endVertex = graph.GetVertexAt(end);
    // pathfinding logic ...
    return GeneratePath(parentsDictionary, endVertex);
}

It uses Manhattan distance and constant edge cost (1f) to simplify runtime cost:

float priority = newCost + ManhattanDiscance(end, neighbour);

RoadHelper & Marker Selection

Each tile uses a RoadHelper-derived class to expose marker logic. For example:

public virtual Marker GetPositioForCarToSpawn(Vector3 nextPathPosition)
{
    return useInner ? innerOutgoing : outerOutgoing;
}

Or, for tiles that support multiple lanes (e.g. RoadHelperMultipleCarMarkers):

public override Marker GetPositioForCarToSpawn(Vector3 nextPathPosition)
{
    return useInner
        ? GetClosestMarkeTo(nextPathPosition, innerOutgoingMarkers)
        : GetClosestMarkeTo(nextPathPosition, outerOutgoingMarkers);
}

This allows cars to spawn from the correct lane given the upcoming direction.

Car Behavior During Simulation

Cars are assigned paths at runtime:

car.GetComponent<CarAI>().SetPath(carPath);

And internally follow those points:

Vector3 rel = transform.InverseTransformPoint(currentTargetPosition);
float angle = Mathf.Atan2(rel.x, rel.z) * Mathf.Rad2Deg;
int steer = angle > turningAngleOffset ? 1 : angle < -turningAngleOffset ? -1 : 0;

Basicall, the car can:

  • Stop temporarily at defined “stop points” (SetStops)
  • Check for fire on collision:
if (other.CompareTag("Fire") && !sawFire)
    HandleFireDetection();
  • May respawn and re-route:
ATC_AIDirector.Instance.RespawnACar(car.start, car.ends, car.carSpeed);

What This System Lets Us Do

  • Split cars across lanes using inner or outer markers to simulate realistic dual-direction traffic.

  • Support multiple stops, like picking up kids at school or loading a trailer.

  • React dynamically to hazards—cars can reroute if they spot fire, or respawn if they get stuck.

  • Create realistic consequences when too many vehicles use the same exit routes, mirroring real-world evacuation challenges.

Leave a comment

Log in with itch.io to leave a comment.