#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 LayerPoolTable = new(); static readonly ConditionalWeakTable.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 froms, Func targetPredicate, BlockedByActor check, Func 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 froms, CPos target, BlockedByActor check, int heuristicWeightPercentage, Func customCost = null, Actor ignoreActor = null, bool laneBias = true, bool inReverse = false, Func 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; } /// /// Determines if a cell is a valid pathfinding location. /// /// It is in the world. /// It is either on the ground layer (0) or on an *enabled* custom movement layer. /// It has not been excluded by the . /// /// If required, follow this with a call to /// to /// determine if the cell is accessible. /// public static bool CellAllowsMovement(World world, Locomotor locomotor, CPos cell, Func 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 froms, BlockedByActor check, Func 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> 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; } /// /// Default: Diagonal distance heuristic. More information: /// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html /// Layers are ignored and incur no additional cost. /// /// Locomotor used to provide terrain costs. /// The cell for which costs are to be given by the estimation function. /// A delegate that calculates the cost estimation between the and the given cell. public static Func DefaultCostEstimator(Locomotor locomotor, CPos destination) { var estimator = DefaultCostEstimator(locomotor); return (here, _) => estimator(here, destination); } /// /// Default: Diagonal distance heuristic. More information: /// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html /// Layers are ignored and incur no additional cost. /// /// Locomotor used to provide terrain costs. /// A delegate that calculates the cost estimation between the given cells. public static Func 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 TargetPredicate { get; set; } readonly Func heuristic; readonly int heuristicWeightPercentage; readonly IRecorder recorder; readonly IPriorityQueue openQueue; /// /// Initialize a new search. /// /// Graph over which the search is conducted. /// 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. /// /// 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. /// /// Determines if the given cell is the target. /// If provided, will record all nodes explored by searches performed. PathSearch(IPathGraph graph, Func heuristic, int heuristicWeightPercentage, Func targetPredicate, IRecorder recorder) { Graph = graph; this.heuristic = heuristic; this.heuristicWeightPercentage = heuristicWeightPercentage; TargetPredicate = targetPredicate; this.recorder = recorder; openQueue = new Primitives.PriorityQueue(default); } void AddInitialCell(CPos location, Func 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); } /// /// Determines if there are more reachable cells and the search can be continued. /// If false, can no longer be called. /// 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; } /// /// This function analyzes the neighbors of the most promising node in the pathfinding graph /// using the A* algorithm (A-star) and returns that node. /// /// The most promising node of the iteration. 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; } /// /// Expands the path search until a path is found, and returns whether a path is found successfully. /// /// /// 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 /// then this method will return false. /// public bool ExpandToTarget() { while (CanExpand()) if (TargetPredicate(Expand())) return true; return false; } /// /// Expands the path search over the whole search space. /// Returns the cells that were visited during the search. /// public List ExpandAll() { var consideredCells = new List(); while (CanExpand()) consideredCells.Add(Expand()); return consideredCells; } /// /// Expands the path search until a path is found, and returns that path. /// Returned path is *reversed* and given target to source. /// public List 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 MakePath(IPathGraph graph, CPos destination) { var ret = new List(); var currentNode = destination; while (graph[currentNode].PreviousNode != currentNode) { ret.Add(currentNode); currentNode = graph[currentNode].PreviousNode; } ret.Add(currentNode); return ret; } /// /// 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. /// public static List 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 MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode) { var ca = first.Graph; var cb = second.Graph; var ret = new List(); 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(); } } }