#region Copyright & License Information /* * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) * 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 { /// /// When searching for paths, use a default weight of 125% to reduce /// computation effort - even if this means paths may be sub-optimal. /// public const int DefaultHeuristicWeightPercentage = 125; /// /// When searching for paths, use a lane bias to guide units into /// "lanes" whilst moving, to promote smooth unit flow when groups of /// units are moving. /// public const bool DefaultLaneBias = true; // 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 ConditionalWeakTable(); 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) { var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, null, false, DefaultLaneBias); var search = new PathSearch(graph, loc => 0, DefaultHeuristicWeightPercentage, targetPredicate); foreach (var sl in froms) if (world.Map.Contains(sl)) search.AddInitialCell(sl); return search; } public static PathSearch ToTargetCell( World world, Locomotor locomotor, Actor self, CPos from, CPos target, BlockedByActor check, Func customCost = null, Actor ignoreActor = null, bool inReverse = false, bool laneBias = DefaultLaneBias) { return ToTargetCell(world, locomotor, self, new[] { from }, target, check, customCost, ignoreActor, inReverse, laneBias); } public static PathSearch ToTargetCell( World world, Locomotor locomotor, Actor self, IEnumerable froms, CPos target, BlockedByActor check, Func customCost = null, Actor ignoreActor = null, bool inReverse = false, bool laneBias = DefaultLaneBias, Func heuristic = null, int heuristicWeightPercentage = DefaultHeuristicWeightPercentage) { var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, inReverse, laneBias); heuristic = heuristic ?? DefaultCostEstimator(locomotor, target); var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target); foreach (var sl in froms) if (world.Map.Contains(sl)) search.AddInitialCell(sl); return search; } /// /// Default: Diagonal distance heuristic. More information: /// http://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: /// http://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; } readonly Func heuristic; readonly int heuristicWeightPercentage; public Func TargetPredicate { get; set; } 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 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. PathSearch(IPathGraph graph, Func heuristic, int heuristicWeightPercentage, Func targetPredicate) { Graph = graph; this.heuristic = heuristic; this.heuristicWeightPercentage = heuristicWeightPercentage; TargetPredicate = targetPredicate; openQueue = new PriorityQueue(GraphConnection.ConnectionCostComparer); } void AddInitialCell(CPos location) { var estimatedCost = heuristic(location) * heuristicWeightPercentage / 100; Graph[location] = new CellInfo(CellStatus.Open, 0, 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. /// public bool CanExpand => !openQueue.Empty; /// /// 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 public 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)) { // 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. If the cell has // already been processed, we can reuse the result (just the difference between the // estimated total and the cost so far) int estimatedRemainingCostToTarget; if (neighborInfo.Status == CellStatus.Open) estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar; else estimatedRemainingCostToTarget = heuristic(neighbor) * heuristicWeightPercentage / 100; var estimatedTotalCostToTarget = costSoFarToNeighbor + estimatedRemainingCostToTarget; Graph[neighbor] = new CellInfo(CellStatus.Open, costSoFarToNeighbor, estimatedTotalCostToTarget, currentMinNode); if (neighborInfo.Status != CellStatus.Open) openQueue.Add(new GraphConnection(neighbor, estimatedTotalCostToTarget)); } return currentMinNode; } /// /// Determines if is the target of the search. /// public bool IsTarget(CPos location) { return TargetPredicate(location); } public void Dispose() { Graph.Dispose(); } } }