Routing component of RoadTrafficGraph. Implements Dijkstra-based driving distance and route search over TrafficLane objects, snapping endpoints to nearby lanes and building waypoint or lane sequences for vehicles to follow.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool;
/// <summary>
/// Routing queries over the lane graph — how far it actually is to drive from one place to another.
///
/// "Actually" is the point: straight-line distance is useless for anything that has to obey roads. A delivery
/// two blocks away as the crow flies can be a mile of one-way streets, and a job that times you on crow-flight
/// distance is unwinnable in exactly the places that are most interesting to drive.
/// </summary>
public sealed partial class RoadTrafficGraph
{
/// <summary>
/// How far either end of a route may be from a lane and still count as being on it. A road carries a lane
/// per direction, so a single point sits near several — see <see cref="FindNearbyLanes"/> for why taking
/// them all matters.
/// </summary>
public const float DefaultRouteSnapRadius = 1000.0f;
/// <summary>
/// Driving distance from <paramref name="_From"/> to <paramref name="_To"/>, following lanes in their legal
/// direction. False when neither end is anywhere near a road, or when no route exists at all — a one-way
/// system can genuinely have no way round.
///
/// Dijkstra over whole lanes rather than individual waypoints: lanes are the unit the graph is linked in,
/// and a city's worth of them is a few thousand nodes. Costs are measured to the START of each lane, with
/// the partial lengths at both ends added on, so the answer is measured between the actual points.
/// </summary>
public bool TryGetDrivingDistance(Vector3 _From, Vector3 _To, out float _Distance)
{
_Distance = 0.0f;
List<(TrafficLane Lane, int Index)> starts = FindNearbyLanes(_From);
List<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);
if (starts.Count == 0 || goals.Count == 0)
return false;
var goalIndices = new Dictionary<TrafficLane, int>();
foreach ((TrafficLane lane, int index) in goals)
goalIndices[lane] = index;
var best = new Dictionary<TrafficLane, float>();
var queue = new PriorityQueue<TrafficLane, float>();
float bestTotal = float.MaxValue;
foreach ((TrafficLane lane, int index) in starts)
{
// Goal on the same lane and ahead of us: straight down the road, no junction involved. Behind us
// doesn't count — a lane is one-way, so that really does mean driving round and coming back, which
// the search below works out properly.
if (goalIndices.TryGetValue(lane, out int goalIndex) && goalIndex >= index)
bestTotal = Math.Min(bestTotal, lane.DistanceFromStart(goalIndex) - lane.DistanceFromStart(index));
// We start partway along, so what's reachable is the successors, at the cost of finishing this lane.
float toEnd = lane.DistanceToEnd(index);
foreach (TrafficLane next in lane.Successors)
Relax(next, toEnd, best, queue);
}
while (queue.TryDequeue(out TrafficLane lane, out float cost))
{
// Min-ordered, so once the cheapest thing left already costs more than an answer we have, nothing
// better can come out of it.
if (cost >= bestTotal)
break;
if (best.TryGetValue(lane, out float known) && cost > known)
continue;
// Reaching a goal lane doesn't end the search: another route might arrive at a different goal
// candidate — the other side of the same street, say — for less.
if (goalIndices.TryGetValue(lane, out int goalIndex))
bestTotal = Math.Min(bestTotal, cost + lane.DistanceFromStart(goalIndex));
float exit = cost + lane.Length;
foreach (TrafficLane next in lane.Successors)
Relax(next, exit, best, queue);
}
if (bestTotal >= float.MaxValue)
return false;
_Distance = Math.Max(0.0f, bestTotal);
return true;
}
/// <summary>
/// The actual waypoints to DRIVE from one point to another, following lanes in their legal direction. False
/// when there's no route, same as <see cref="TryGetDrivingDistance"/>.
///
/// This is the vehicle counterpart of a navmesh path, and it has to be a separate structure rather than the
/// navmesh itself. A navmesh is baked around a person — it runs over pavements, through doorways and up
/// stairs, and its corridors are person-wide. A route down one is a perfectly valid walk and an impossible
/// drive, and unlike a pedestrian scraping a wall, a car routed somewhere it doesn't fit is stuck for good.
/// Lanes are the drivable surface by construction, and they carry direction, which a navmesh has no concept
/// of at all.
///
/// The list is reused rather than returned, because anything chasing anything re-routes constantly.
/// </summary>
public bool TryGetDrivingRoute(Vector3 _From, Vector3 _To, List<Vector3> _Route)
{
_Route.Clear();
if (!TrySearchRoute(_From, _To, out RouteSearch search))
return false;
BuildRoute(search.Starts, search.GoalIndices, search.CameFrom, search.Goal, search.Start, search.StartIndex, _Route);
return _Route.Count > 0;
}
/// <summary>
/// The same route as <see cref="TryGetDrivingRoute"/>, but as the LANES to drive rather than the waypoints
/// along them — including the one we're starting on.
///
/// This is the form anything that already knows how to drive a lane wants. <see cref="TrafficVehicle"/>
/// follows lanes and picks a successor at every junction; handing it a lane list turns "wander" into "go
/// here" without touching a single line of how it actually steers, brakes or corners.
/// </summary>
public bool TryGetDrivingLaneRoute(Vector3 _From, Vector3 _To, List<TrafficLane> _Route)
{
return TryGetDrivingLaneRoute(FindNearbyLanes(_From), _To, _Route);
}
/// <summary>
/// The same, but starting from the lane a vehicle is ALREADY DRIVING rather than from its position.
///
/// This is the overload anything mid-journey wants, and the difference is not subtle. Asking by position
/// seeds the search with every lane in range — including the one going the other way down the same road —
/// and Dijkstra will happily return the shortest route from whichever of those is cheapest. That route
/// starts on a lane the vehicle is not on, so it never matches, and a driver that can't find itself in its
/// own route falls back to picking turns at random. The symptom is a car that mostly goes the right way and
/// occasionally sets off round the block for no visible reason.
/// </summary>
public bool TryGetDrivingLaneRoute(TrafficLane _FromLane, Vector3 _FromPosition, Vector3 _To, List<TrafficLane> _Route)
{
_Route.Clear();
if (_FromLane is null || _FromLane.Waypoints.Count == 0)
return false;
return TryGetDrivingLaneRoute([(_FromLane, NearestWaypointIndex(_FromLane, _FromPosition))], _To, _Route);
}
/// <summary>Which waypoint of a lane is closest to a point.</summary>
private static int NearestWaypointIndex(TrafficLane _Lane, Vector3 _Point)
{
int best = 0;
float bestDistance = float.MaxValue;
for (int i = 0; i < _Lane.Waypoints.Count; i++)
{
float distance = _Lane.Waypoints[i].DistanceSquared(_Point);
if (distance >= bestDistance)
continue;
bestDistance = distance;
best = i;
}
return best;
}
private bool TryGetDrivingLaneRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, List<TrafficLane> _Route)
{
_Route.Clear();
if (!TrySearchRoute(_Starts, _To, out RouteSearch search))
return false;
// Never left the start lane — the route is just that one.
if (search.Goal is null)
{
if (search.Start is null)
return false;
_Route.Add(search.Start);
return true;
}
List<TrafficLane> chain = BuildLaneChain(search.CameFrom, search.Goal);
if (chain.Count == 0)
return false;
// The lane we're ON isn't in the chain (the chain begins at one of its successors), and a driver already
// travelling it needs to see it in the list or its very first junction is an unplanned one.
foreach ((TrafficLane lane, int _) in search.Starts)
{
if (!lane.Successors.Contains(chain[0]))
continue;
_Route.Add(lane);
break;
}
_Route.AddRange(chain);
return true;
}
/// <summary>What a completed search found: the winning route's ends, and the map to walk it back with.</summary>
private struct RouteSearch
{
public List<(TrafficLane Lane, int Index)> Starts;
public Dictionary<TrafficLane, int> GoalIndices;
public Dictionary<TrafficLane, TrafficLane> CameFrom;
/// <summary>The lane the route ends on, or null when it never left the lane it started on.</summary>
public TrafficLane Goal;
public TrafficLane Start;
public int StartIndex;
}
/// <summary>
/// The Dijkstra itself, shared by both route shapes so there's one search to be correct rather than two to
/// keep in step.
/// </summary>
private bool TrySearchRoute(Vector3 _From, Vector3 _To, out RouteSearch _Result)
{
return TrySearchRoute(FindNearbyLanes(_From), _To, out _Result);
}
/// <inheritdoc cref="TrySearchRoute(Vector3, Vector3, out RouteSearch)"/>
private bool TrySearchRoute(List<(TrafficLane Lane, int Index)> _Starts, Vector3 _To, out RouteSearch _Result)
{
_Result = default;
List<(TrafficLane Lane, int Index)> starts = _Starts;
List<(TrafficLane Lane, int Index)> goals = FindNearbyLanes(_To);
if (starts is null || starts.Count == 0 || goals.Count == 0)
return false;
var goalIndices = new Dictionary<TrafficLane, int>();
foreach ((TrafficLane lane, int index) in goals)
goalIndices[lane] = index;
var best = new Dictionary<TrafficLane, float>();
var cameFrom = new Dictionary<TrafficLane, TrafficLane>();
var queue = new PriorityQueue<TrafficLane, float>();
// The winning route so far: where it ends, and which of the several starts it began at.
float bestTotal = float.MaxValue;
TrafficLane bestGoal = null;
TrafficLane bestStart = null;
int bestStartIndex = 0;
foreach ((TrafficLane lane, int index) in starts)
{
// Goal on the same lane and ahead of us — no junction involved, so the route is just this stretch.
if (goalIndices.TryGetValue(lane, out int sameLaneGoal) && sameLaneGoal >= index)
{
float direct = lane.DistanceFromStart(sameLaneGoal) - lane.DistanceFromStart(index);
if (direct < bestTotal)
{
bestTotal = direct;
bestGoal = null; // null goal marks "never left the start lane"
bestStart = lane;
bestStartIndex = index;
}
}
float toEnd = lane.DistanceToEnd(index);
foreach (TrafficLane next in lane.Successors)
{
if (Relax(next, toEnd, best, queue))
cameFrom[next] = lane;
}
}
while (queue.TryDequeue(out TrafficLane lane, out float cost))
{
if (cost >= bestTotal)
break;
if (best.TryGetValue(lane, out float known) && cost > known)
continue;
if (goalIndices.TryGetValue(lane, out int goalIndex))
{
float total = cost + lane.DistanceFromStart(goalIndex);
if (total < bestTotal)
{
bestTotal = total;
bestGoal = lane;
}
}
float exit = cost + lane.Length;
foreach (TrafficLane next in lane.Successors)
{
if (Relax(next, exit, best, queue))
cameFrom[next] = lane;
}
}
if (bestTotal >= float.MaxValue)
return false;
_Result = new RouteSearch
{
Starts = starts,
GoalIndices = goalIndices,
CameFrom = cameFrom,
Goal = bestGoal,
Start = bestStart,
StartIndex = bestStartIndex
};
return true;
}
/// <summary>
/// Walks the predecessor chain back from the winning goal lane to whichever start it came from, then lays
/// the waypoints down in travel order.
///
/// The two ends are partial lanes — we join partway along the first and stop partway along the last — which
/// is why they're handled separately from the whole lanes in between.
/// </summary>
private void BuildRoute(List<(TrafficLane Lane, int Index)> _Starts, Dictionary<TrafficLane, int> _GoalIndices,
Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal,
TrafficLane _Start, int _StartIndex, List<Vector3> _Route)
{
// Never left the start lane: one straight run down it.
if (_Goal is null)
{
if (_Start is null || !_GoalIndices.TryGetValue(_Start, out int stop))
return;
for (int i = _StartIndex; i <= stop; i++)
_Route.Add(_Start.Waypoints[i]);
return;
}
List<TrafficLane> chain = BuildLaneChain(_CameFrom, _Goal);
if (chain.Count == 0)
return;
// The first lane in the chain is a successor of the start lane, so the start lane itself isn't in it —
// find which of the candidates fed it and lay down the tail of that one first.
TrafficLane head = chain[0];
foreach ((TrafficLane lane, int index) in _Starts)
{
if (!lane.Successors.Contains(head))
continue;
for (int i = index; i < lane.Waypoints.Count; i++)
_Route.Add(lane.Waypoints[i]);
break;
}
for (int c = 0; c < chain.Count; c++)
{
TrafficLane lane = chain[c];
// The last one stops at the goal waypoint rather than running to the end of the road.
int stop = c == chain.Count - 1 && _GoalIndices.TryGetValue(lane, out int goalIndex)
? goalIndex
: lane.Waypoints.Count - 1;
for (int i = 0; i <= stop; i++)
_Route.Add(lane.Waypoints[i]);
}
}
/// <summary>Walks the predecessor map back from a goal lane and returns the chain in travel order.</summary>
private List<TrafficLane> BuildLaneChain(Dictionary<TrafficLane, TrafficLane> _CameFrom, TrafficLane _Goal)
{
var chain = new List<TrafficLane>();
TrafficLane current = _Goal;
// Bounded by the lane count so a cycle in the map can't spin forever.
for (int step = 0; step <= Lanes.Count && current is not null; step++)
{
chain.Add(current);
if (!_CameFrom.TryGetValue(current, out TrafficLane previous))
break;
current = previous;
}
chain.Reverse();
return chain;
}
/// <summary>True when this was an improvement, so the caller knows whether to record the predecessor.</summary>
private static bool Relax(TrafficLane _Lane, float _Cost, Dictionary<TrafficLane, float> _Best, PriorityQueue<TrafficLane, float> _Queue)
{
if (_Best.TryGetValue(_Lane, out float existing) && existing <= _Cost)
return false;
_Best[_Lane] = _Cost;
_Queue.Enqueue(_Lane, _Cost);
return true;
}
/// <summary>
/// Every drivable lane with a waypoint within <paramref name="_Radius"/> of the point, and which waypoint
/// that was — at most one entry per lane.
///
/// Taking ALL of them, rather than just the closest, is what makes routing reliable. A road carries a lane
/// per direction, so any point on it is near at least two; picking only the nearest is a coin flip that can
/// land on the one pointing away from where you're going, or on one nothing feeds into. The route then comes
/// back as impossible even though the lane a few metres over is trivially routable. Seeding the search with
/// every candidate — and accepting any of them at the far end — also gets the natural answer for free:
/// either side of the street will do, whichever is closer to drive.
///
/// Road lanes only. Snapping an endpoint onto an intersection cross-lane would measure from the middle of
/// a junction.
/// </summary>
public List<(TrafficLane Lane, int Index)> FindNearbyLanes(Vector3 _Point, float _Radius = DefaultRouteSnapRadius)
{
var results = new List<(TrafficLane, int)>();
TrafficLane nearestLane = null;
int nearestIndex = 0;
float nearestDistance = float.MaxValue;
float radiusSquared = _Radius * _Radius;
foreach (TrafficLane lane in Lanes)
{
if (!lane.IsRoadLane)
continue;
int laneIndex = -1;
float laneDistance = float.MaxValue;
for (int i = 0; i < lane.Waypoints.Count; i++)
{
float distance = lane.Waypoints[i].DistanceSquared(_Point);
if (distance >= laneDistance)
continue;
laneDistance = distance;
laneIndex = i;
}
if (laneIndex < 0)
continue;
if (laneDistance < nearestDistance)
{
nearestDistance = laneDistance;
nearestLane = lane;
nearestIndex = laneIndex;
}
if (laneDistance <= radiusSquared)
results.Add((lane, laneIndex));
}
// Off-road entirely (a car park, a field) — the closest lane is still the honest answer, so don't come
// back empty and turn a long route into "no route".
if (results.Count == 0 && nearestLane is not null)
results.Add((nearestLane, nearestIndex));
return results;
}
/// <summary>
/// The single drivable lane closest to a point, and which waypoint that was. Prefer
/// <see cref="FindNearbyLanes"/> for routing — one lane is rarely the whole answer for a two-way road.
/// </summary>
public TrafficLane FindNearestLane(Vector3 _Point, out int _Index)
{
TrafficLane bestLane = null;
float bestDistance = float.MaxValue;
_Index = 0;
foreach (TrafficLane lane in Lanes)
{
if (!lane.IsRoadLane)
continue;
for (int i = 0; i < lane.Waypoints.Count; i++)
{
float distance = lane.Waypoints[i].DistanceSquared(_Point);
if (distance >= bestDistance)
continue;
bestDistance = distance;
bestLane = lane;
_Index = i;
}
}
return bestLane;
}
}