422 lines
16 KiB
C#
422 lines
16 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Primitives;
|
|
|
|
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();
|
|
static readonly ConditionalWeakTable<World, CellInfoLayerPool>.CreateValueCallback CreateLayerPool = world => new CellInfoLayerPool(world.Map);
|
|
|
|
static CellInfoLayerPool LayerPoolForWorld(World world)
|
|
{
|
|
return LayerPoolTable.GetValue(world, CreateLayerPool);
|
|
}
|
|
|
|
public static PathSearch ToTargetCellByPredicate(
|
|
World world, Locomotor locomotor, Actor self,
|
|
IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
|
Func<CPos, int> customCost = null,
|
|
Actor ignoreActor = null,
|
|
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, (_, _) => 0, 0, targetPredicate, recorder);
|
|
|
|
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, false, search);
|
|
|
|
return search;
|
|
}
|
|
|
|
public static PathSearch ToTargetCell(
|
|
World world, Locomotor locomotor, Actor self,
|
|
IEnumerable<CPos> froms, CPos target, BlockedByActor check, int heuristicWeightPercentage,
|
|
Func<CPos, int> customCost = null,
|
|
Actor ignoreActor = null,
|
|
bool laneBias = true,
|
|
bool inReverse = false,
|
|
Func<CPos, bool, int> heuristic = null,
|
|
Grid? grid = null,
|
|
IRecorder recorder = null)
|
|
{
|
|
IPathGraph graph;
|
|
if (grid != null)
|
|
graph = new GridPathGraph(locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse, grid.Value);
|
|
else
|
|
graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse);
|
|
|
|
heuristic ??= DefaultCostEstimator(locomotor, target);
|
|
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target, recorder);
|
|
|
|
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, inReverse, search);
|
|
|
|
return search;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a cell is a valid pathfinding location.
|
|
/// <list type="bullet">
|
|
/// <item>It is in the world.</item>
|
|
/// <item>It is either on the ground layer (0) or on an *enabled* custom movement layer.</item>
|
|
/// <item>It has not been excluded by the <paramref name="customCost"/>.</item>
|
|
/// </list>
|
|
/// If required, follow this with a call to
|
|
/// <see cref="Locomotor.MovementCostToEnterCell(Actor, CPos, CPos, BlockedByActor, Actor, bool)"/> to
|
|
/// determine if the cell is accessible.
|
|
/// </summary>
|
|
public static bool CellAllowsMovement(World world, Locomotor locomotor, CPos cell, Func<CPos, int> customCost)
|
|
{
|
|
return world.Map.Contains(cell) &&
|
|
(cell.Layer == 0 || world.GetCustomMovementLayers()[cell.Layer].EnabledForLocomotor(locomotor.Info)) &&
|
|
(customCost == null || customCost(cell) != PathGraph.PathCostForInvalidPath);
|
|
}
|
|
|
|
static void AddInitialCells(World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms,
|
|
BlockedByActor check, Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, PathSearch search)
|
|
{
|
|
// A source cell is allowed to have an unreachable movement cost.
|
|
// Therefore we don't need to check if the cell is accessible, only that it allows movement.
|
|
// *Unless* the search is being done in reverse, in this case the source is really a target,
|
|
// and a target is required to have a reachable cost.
|
|
// We also need to ignore self, so we don't consider the location blocked by ourselves!
|
|
foreach (var sl in froms)
|
|
if (CellAllowsMovement(world, locomotor, sl, customCost) &&
|
|
(!inReverse || locomotor.MovementCostToEnterCell(self, sl, check, ignoreActor, true)
|
|
!= PathGraph.MovementCostForUnreachableCell))
|
|
search.AddInitialCell(sl, customCost);
|
|
}
|
|
|
|
public static PathSearch ToTargetCellOverGraph(
|
|
Func<CPos, List<GraphConnection>> edges, Locomotor locomotor, CPos from, CPos target,
|
|
int estimatedSearchSize = 0, IRecorder recorder = null)
|
|
{
|
|
var graph = new SparsePathGraph(edges, estimatedSearchSize);
|
|
var search = new PathSearch(graph, DefaultCostEstimator(locomotor, target), 100, loc => loc == target, recorder);
|
|
|
|
search.AddInitialCell(from, null);
|
|
|
|
return search;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default: Diagonal distance heuristic. More information:
|
|
/// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
|
/// Layers are ignored and incur no additional cost.
|
|
/// </summary>
|
|
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
|
|
/// <param name="destination">The cell for which costs are to be given by the estimation function.</param>
|
|
/// <returns>A delegate that calculates the cost estimation between the <paramref name="destination"/> and the given cell.</returns>
|
|
public static Func<CPos, bool, int> DefaultCostEstimator(Locomotor locomotor, CPos destination)
|
|
{
|
|
var estimator = DefaultCostEstimator(locomotor);
|
|
return (here, _) => estimator(here, destination);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default: Diagonal distance heuristic. More information:
|
|
/// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
|
/// Layers are ignored and incur no additional cost.
|
|
/// </summary>
|
|
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
|
|
/// <returns>A delegate that calculates the cost estimation between the given cells.</returns>
|
|
public static Func<CPos, CPos, int> DefaultCostEstimator(Locomotor locomotor)
|
|
{
|
|
// Determine the minimum possible cost for moving horizontally between cells based on terrain speeds.
|
|
// The minimum possible cost diagonally is then Sqrt(2) times more costly.
|
|
var cellCost = locomotor.Info.TerrainSpeeds.Values.Min(ti => ti.Cost);
|
|
var diagonalCellCost = Exts.MultiplyBySqrtTwo(cellCost);
|
|
return (here, destination) =>
|
|
{
|
|
var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y));
|
|
var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y);
|
|
|
|
// According to the information link, this is the shape of the function.
|
|
// We just extract factors to simplify.
|
|
// Possible simplification: var h = Constants.CellCost * (straight + (Constants.Sqrt2 - 2) * diag);
|
|
return cellCost * straight + (diagonalCellCost - 2 * cellCost) * diag;
|
|
};
|
|
}
|
|
|
|
public IPathGraph Graph { get; }
|
|
public Func<CPos, bool> TargetPredicate { get; set; }
|
|
readonly Func<CPos, bool, int> heuristic;
|
|
readonly int heuristicWeightPercentage;
|
|
readonly IRecorder recorder;
|
|
readonly IPriorityQueue<GraphConnection> openQueue;
|
|
|
|
/// <summary>
|
|
/// Initialize a new search.
|
|
/// </summary>
|
|
/// <param name="graph">Graph over which the search is conducted.</param>
|
|
/// <param name="heuristic">Provides an estimation of the distance between the given cell and the target.
|
|
/// The Boolean parameter indicates if the cell is known to be accessible.
|
|
/// When true, it is known accessible as it is being explored by the search.
|
|
/// When false, the cell is being considered as a starting location and might not be accessible.</param>
|
|
/// <param name="heuristicWeightPercentage">
|
|
/// The search will aim for the shortest path when given a weight of 100%.
|
|
/// We can allow the search to find paths that aren't optimal by changing the weight.
|
|
/// The weight limits the worst case length of the path,
|
|
/// e.g. a weight of 110% will find a path no more than 10% longer than the shortest possible.
|
|
/// The benefit of allowing the search to return suboptimal paths is faster computation time.
|
|
/// 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>
|
|
/// <param name="recorder">If provided, will record all nodes explored by searches performed.</param>
|
|
PathSearch(IPathGraph graph, Func<CPos, bool, 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 Primitives.PriorityQueue<GraphConnection, GraphConnection.CostComparer>(default);
|
|
}
|
|
|
|
void AddInitialCell(CPos location, Func<CPos, int> customCost)
|
|
{
|
|
var initialCost = 0;
|
|
if (customCost != null)
|
|
{
|
|
initialCost = customCost(location);
|
|
if (initialCost == PathGraph.PathCostForInvalidPath)
|
|
return;
|
|
}
|
|
|
|
var heuristicCost = heuristic(location, false);
|
|
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
|
return;
|
|
|
|
var estimatedCost = heuristicCost * heuristicWeightPercentage / 100;
|
|
Graph[location] = new CellInfo(CellStatus.Open, initialCost, initialCost + estimatedCost, location);
|
|
var connection = new GraphConnection(location, estimatedCost);
|
|
openQueue.Add(connection);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if there are more reachable cells and the search can be continued.
|
|
/// If false, <see cref="Expand"/> can no longer be called.
|
|
/// </summary>
|
|
bool CanExpand()
|
|
{
|
|
// Connections to a cell can appear more than once if a search discovers a lower cost route to the cell.
|
|
// The lower cost gets processed first and the cell will be Closed.
|
|
// When checking if we can expand, pop any Closed cells off the queue, so Expand will only see Open cells.
|
|
CellStatus status;
|
|
do
|
|
{
|
|
if (openQueue.Empty)
|
|
return false;
|
|
|
|
status = Graph[openQueue.Peek().Destination].Status;
|
|
if (status == CellStatus.Closed)
|
|
openQueue.Pop();
|
|
}
|
|
while (status == CellStatus.Closed);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function analyzes the neighbors of the most promising node in the pathfinding graph
|
|
/// using the A* algorithm (A-star) and returns that node.
|
|
/// </summary>
|
|
/// <returns>The most promising node of the iteration.</returns>
|
|
CPos Expand()
|
|
{
|
|
var currentMinNode = openQueue.Pop().Destination;
|
|
|
|
var currentInfo = Graph[currentMinNode];
|
|
Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode);
|
|
|
|
foreach (var connection in Graph.GetConnections(currentMinNode, TargetPredicate))
|
|
{
|
|
// Calculate the cost up to that point
|
|
var costSoFarToNeighbor = currentInfo.CostSoFar + connection.Cost;
|
|
|
|
var neighbor = connection.Destination;
|
|
var neighborInfo = Graph[neighbor];
|
|
|
|
// Cost is even higher; next direction:
|
|
if (neighborInfo.Status == CellStatus.Closed ||
|
|
(neighborInfo.Status == CellStatus.Open && costSoFarToNeighbor >= neighborInfo.CostSoFar))
|
|
continue;
|
|
|
|
// Now we may seriously consider this direction using heuristics.
|
|
int estimatedRemainingCostToTarget;
|
|
if (neighborInfo.Status == CellStatus.Open)
|
|
{
|
|
// If the cell has already been processed, we can reuse the result
|
|
// (just the difference between the estimated total and the cost so far)
|
|
estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar;
|
|
}
|
|
else
|
|
{
|
|
// If the heuristic reports the cell is unreachable, we won't consider it.
|
|
var heuristicCost = heuristic(neighbor, true);
|
|
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
|
continue;
|
|
estimatedRemainingCostToTarget = heuristicCost * 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));
|
|
}
|
|
|
|
return currentMinNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands the path search until a path is found, and returns whether a path is found successfully.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the path search has previously been expanded it will only return true if a path can be found during
|
|
/// *this* expansion of the search. If the search was expanded previously and the target is already
|
|
/// <see cref="CellStatus.Closed"/> then this method will return false.
|
|
/// </remarks>
|
|
public bool ExpandToTarget()
|
|
{
|
|
while (CanExpand())
|
|
if (TargetPredicate(Expand()))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands the path search over the whole search space.
|
|
/// Returns the cells that were visited during the search.
|
|
/// </summary>
|
|
public List<CPos> ExpandAll()
|
|
{
|
|
var consideredCells = new List<CPos>();
|
|
while (CanExpand())
|
|
consideredCells.Add(Expand());
|
|
return consideredCells;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands the path search until a path is found, and returns that path.
|
|
/// Returned path is *reversed* and given target to source.
|
|
/// </summary>
|
|
public List<CPos> FindPath()
|
|
{
|
|
while (CanExpand())
|
|
{
|
|
var p = Expand();
|
|
if (TargetPredicate(p))
|
|
return MakePath(Graph, p);
|
|
}
|
|
|
|
return PathFinder.NoPath;
|
|
}
|
|
|
|
// Build the path from the destination.
|
|
// When we find a node that has the same previous position than itself, that node is the source node.
|
|
static List<CPos> MakePath(IPathGraph graph, CPos destination)
|
|
{
|
|
var ret = new List<CPos>();
|
|
var currentNode = destination;
|
|
|
|
while (graph[currentNode].PreviousNode != currentNode)
|
|
{
|
|
ret.Add(currentNode);
|
|
currentNode = graph[currentNode].PreviousNode;
|
|
}
|
|
|
|
ret.Add(currentNode);
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands both path searches until they intersect, and returns the path.
|
|
/// Returned path is from the source of the first search to the source of the second search.
|
|
/// </summary>
|
|
public static List<CPos> FindBidiPath(PathSearch first, PathSearch second)
|
|
{
|
|
while (first.CanExpand() && second.CanExpand())
|
|
{
|
|
// make some progress on the first search
|
|
var p = first.Expand();
|
|
var pInfo = second.Graph[p];
|
|
if (pInfo.Status == CellStatus.Closed &&
|
|
pInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
|
|
return MakeBidiPath(first, second, p);
|
|
|
|
// make some progress on the second search
|
|
var q = second.Expand();
|
|
var qInfo = first.Graph[q];
|
|
if (qInfo.Status == CellStatus.Closed &&
|
|
qInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
|
|
return MakeBidiPath(first, second, q);
|
|
}
|
|
|
|
return PathFinder.NoPath;
|
|
}
|
|
|
|
// Build the path from the destination of each search.
|
|
// When we find a node that has the same previous position than itself, that is the source of that search.
|
|
static List<CPos> MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode)
|
|
{
|
|
var ca = first.Graph;
|
|
var cb = second.Graph;
|
|
|
|
var ret = new List<CPos>();
|
|
|
|
var q = confluenceNode;
|
|
var previous = ca[q].PreviousNode;
|
|
while (previous != q)
|
|
{
|
|
ret.Add(q);
|
|
q = previous;
|
|
previous = ca[q].PreviousNode;
|
|
}
|
|
|
|
ret.Add(q);
|
|
|
|
ret.Reverse();
|
|
|
|
q = confluenceNode;
|
|
previous = cb[q].PreviousNode;
|
|
while (previous != q)
|
|
{
|
|
q = previous;
|
|
previous = cb[q].PreviousNode;
|
|
ret.Add(q);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Graph.Dispose();
|
|
}
|
|
}
|
|
}
|