Add a PathFinderOverlay to visualize path searches.

Activated with the '/path-debug' chat command, this displays the explored search space and costs when searching for paths. It supports custom movement layers, bi-directional searches as well as visualizing searches over the abstract graph of the HierarchicalPathFinder. The most recent search among selected units is shown.
This commit is contained in:
RoosterDragon
2022-05-22 15:20:38 +01:00
committed by Matthias Mailänder
parent aef65d353d
commit 93998dc4a7
8 changed files with 283 additions and 20 deletions

View File

@@ -586,11 +586,13 @@ namespace OpenRA.Mods.Common.Pathfinder
/// </summary>
public List<CPos> FindPath(Actor self, IReadOnlyCollection<CPos> sources, CPos target,
BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
Actor ignoreActor, bool laneBias)
Actor ignoreActor, bool laneBias, PathFinderOverlay pathFinderOverlay)
{
if (costEstimator == null)
return PathFinder.NoPath;
pathFinderOverlay?.NewRecording(self, sources, target);
RebuildDirtyGrids();
var targetAbstractCell = AbstractCellForLocalCell(target);
@@ -622,7 +624,7 @@ namespace OpenRA.Mods.Common.Pathfinder
// Determine an abstract path to all sources, for use in a unidirectional search.
var estimatedSearchSize = (abstractGraph.Count + 2) / 8;
using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph(
fullGraph.GetConnections, locomotor, target, target, estimatedSearchSize))
fullGraph.GetConnections, locomotor, target, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
{
var sourcesWithPathableNodes = new List<CPos>(sourcesWithReachableNodes.Count);
foreach (var source in sourcesWithReachableNodes)
@@ -637,7 +639,8 @@ namespace OpenRA.Mods.Common.Pathfinder
using (var fromSrc = GetLocalPathSearch(
self, sourcesWithPathableNodes, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize)))
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize),
recorder: pathFinderOverlay?.RecordLocalEdges(self)))
return fromSrc.FindPath();
}
}
@@ -649,7 +652,7 @@ namespace OpenRA.Mods.Common.Pathfinder
/// </summary>
public List<CPos> FindPath(Actor self, CPos source, CPos target,
BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
Actor ignoreActor, bool laneBias)
Actor ignoreActor, bool laneBias, PathFinderOverlay pathFinderOverlay)
{
if (costEstimator == null)
return PathFinder.NoPath;
@@ -670,15 +673,20 @@ namespace OpenRA.Mods.Common.Pathfinder
source.Layer),
false);
pathFinderOverlay?.NewRecording(self, new[] { source }, target);
List<CPos> localPath;
using (var search = GetLocalPathSearch(
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, gridToSearch, heuristicWeightPercentage))
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, gridToSearch, heuristicWeightPercentage,
recorder: pathFinderOverlay?.RecordLocalEdges(self)))
localPath = search.FindPath();
if (localPath.Count > 0)
return localPath;
}
pathFinderOverlay?.NewRecording(self, new[] { source }, target);
RebuildDirtyGrids();
var targetAbstractCell = AbstractCellForLocalCell(target);
@@ -697,22 +705,24 @@ namespace OpenRA.Mods.Common.Pathfinder
// Determine an abstract path in both directions, for use in a bidirectional search.
var estimatedSearchSize = (abstractGraph.Count + 2) / 8;
using (var forwardAbstractSearch = PathSearch.ToTargetCellOverGraph(
fullGraph.GetConnections, locomotor, source, target, estimatedSearchSize))
fullGraph.GetConnections, locomotor, source, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
{
if (!forwardAbstractSearch.ExpandToTarget())
return PathFinder.NoPath;
using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph(
fullGraph.GetConnections, locomotor, target, source, estimatedSearchSize))
fullGraph.GetConnections, locomotor, target, source, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self)))
{
reverseAbstractSearch.ExpandToTarget();
using (var fromSrc = GetLocalPathSearch(
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize)))
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize),
recorder: pathFinderOverlay?.RecordLocalEdges(self)))
using (var fromDest = GetLocalPathSearch(
self, new[] { target }, source, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
heuristic: Heuristic(forwardAbstractSearch, estimatedSearchSize),
recorder: pathFinderOverlay?.RecordLocalEdges(self),
inReverse: true))
return PathSearch.FindBidiPath(fromDest, fromSrc);
}
@@ -979,11 +989,12 @@ namespace OpenRA.Mods.Common.Pathfinder
Actor self, IEnumerable<CPos> srcs, CPos dst, Func<CPos, int> customCost,
Actor ignoreActor, BlockedByActor check, bool laneBias, Grid? grid, int heuristicWeightPercentage,
Func<CPos, int> heuristic = null,
bool inReverse = false)
bool inReverse = false,
PathSearch.IRecorder recorder = null)
{
return PathSearch.ToTargetCell(
world, locomotor, self, srcs, dst, check, heuristicWeightPercentage,
customCost, ignoreActor, laneBias, inReverse, heuristic, grid);
customCost, ignoreActor, laneBias, inReverse, heuristic, grid, recorder);
}
}
}

View File

@@ -20,6 +20,11 @@ namespace OpenRA.Mods.Common.Pathfinder
{
public sealed class PathSearch : IDisposable
{
public interface IRecorder
{
void Add(CPos source, CPos destination, int costSoFar, int estimatedRemainingCost);
}
// PERF: Maintain a pool of layers used for paths searches for each world. These searches are performed often
// so we wish to avoid the high cost of initializing a new search space every time by reusing the old ones.
static readonly ConditionalWeakTable<World, CellInfoLayerPool> LayerPoolTable = new ConditionalWeakTable<World, CellInfoLayerPool>();
@@ -35,10 +40,11 @@ namespace OpenRA.Mods.Common.Pathfinder
IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true)
bool laneBias = true,
IRecorder recorder = null)
{
var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false);
var search = new PathSearch(graph, loc => 0, 0, targetPredicate);
var search = new PathSearch(graph, loc => 0, 0, targetPredicate, recorder);
foreach (var sl in froms)
if (world.Map.Contains(sl))
@@ -55,7 +61,8 @@ namespace OpenRA.Mods.Common.Pathfinder
bool laneBias = true,
bool inReverse = false,
Func<CPos, int> heuristic = null,
Grid? grid = null)
Grid? grid = null,
IRecorder recorder = null)
{
IPathGraph graph;
if (grid != null)
@@ -64,7 +71,7 @@ namespace OpenRA.Mods.Common.Pathfinder
graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse);
heuristic = heuristic ?? DefaultCostEstimator(locomotor, target);
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target);
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target, recorder);
foreach (var sl in froms)
if (world.Map.Contains(sl))
@@ -75,10 +82,10 @@ namespace OpenRA.Mods.Common.Pathfinder
public static PathSearch ToTargetCellOverGraph(
Func<CPos, List<GraphConnection>> edges, Locomotor locomotor, CPos from, CPos target,
int estimatedSearchSize = 0)
int estimatedSearchSize = 0, IRecorder recorder = null)
{
var graph = new SparsePathGraph(edges, estimatedSearchSize);
var search = new PathSearch(graph, DefaultCostEstimator(locomotor, target), 100, loc => loc == target);
var search = new PathSearch(graph, DefaultCostEstimator(locomotor, target), 100, loc => loc == target, recorder);
search.AddInitialCell(from);
@@ -128,6 +135,7 @@ namespace OpenRA.Mods.Common.Pathfinder
public Func<CPos, bool> TargetPredicate { get; set; }
readonly Func<CPos, int> heuristic;
readonly int heuristicWeightPercentage;
readonly IRecorder recorder;
readonly IPriorityQueue<GraphConnection> openQueue;
/// <summary>
@@ -144,12 +152,13 @@ namespace OpenRA.Mods.Common.Pathfinder
/// The search can skip some areas of the search space, meaning it has less work to do.
/// </param>
/// <param name="targetPredicate">Determines if the given cell is the target.</param>
PathSearch(IPathGraph graph, Func<CPos, int> heuristic, int heuristicWeightPercentage, Func<CPos, bool> targetPredicate)
PathSearch(IPathGraph graph, Func<CPos, int> heuristic, int heuristicWeightPercentage, Func<CPos, bool> targetPredicate, IRecorder recorder)
{
Graph = graph;
this.heuristic = heuristic;
this.heuristicWeightPercentage = heuristicWeightPercentage;
TargetPredicate = targetPredicate;
this.recorder = recorder;
openQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
}
@@ -219,6 +228,8 @@ namespace OpenRA.Mods.Common.Pathfinder
else
estimatedRemainingCostToTarget = heuristic(neighbor) * heuristicWeightPercentage / 100;
recorder?.Add(currentMinNode, neighbor, costSoFarToNeighbor, estimatedRemainingCostToTarget);
var estimatedTotalCostToTarget = costSoFarToNeighbor + estimatedRemainingCostToTarget;
Graph[neighbor] = new CellInfo(CellStatus.Open, costSoFarToNeighbor, estimatedTotalCostToTarget, currentMinNode);
openQueue.Add(new GraphConnection(neighbor, estimatedTotalCostToTarget));