diff --git a/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs index 1b2f39e4ad..9eddb539c8 100644 --- a/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs @@ -11,6 +11,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; namespace OpenRA.Mods.Common.Pathfinder @@ -39,6 +41,8 @@ namespace OpenRA.Mods.Common.Pathfinder IPathSearch WithHeuristic(Func h); + IPathSearch WithHeuristicWeight(int percentage); + IPathSearch WithCustomCost(Func w); IPathSearch WithoutLaneBias(); @@ -71,6 +75,7 @@ namespace OpenRA.Mods.Common.Pathfinder public bool Debug { get; set; } protected Func heuristic; protected Func isGoal; + protected int heuristicWeightPercentage; // This member is used to compute the ID of PathSearch. // Essentially, it represents a collection of the initial @@ -79,12 +84,20 @@ namespace OpenRA.Mods.Common.Pathfinder // a deterministic set of calculations protected readonly IPriorityQueue StartPoints; + private readonly int cellCost, diagonalCellCost; + protected BasePathSearch(IGraph graph) { Graph = graph; OpenQueue = new PriorityQueue(GraphConnection.ConnectionCostComparer); StartPoints = new PriorityQueue(GraphConnection.ConnectionCostComparer); MaxCost = 0; + heuristicWeightPercentage = 100; + + // 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. + cellCost = graph.Actor.Trait().Locomotor.Info.TerrainSpeeds.Values.Min(ti => ti.Cost); + diagonalCellCost = cellCost * 141421 / 100000; } /// @@ -92,7 +105,7 @@ namespace OpenRA.Mods.Common.Pathfinder /// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html /// /// A delegate that calculates the estimation for a node - protected static Func DefaultEstimator(CPos destination) + protected Func DefaultEstimator(CPos destination) { return here => { @@ -102,7 +115,7 @@ namespace OpenRA.Mods.Common.Pathfinder // 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 Constants.CellCost * straight + (Constants.DiagonalCellCost - 2 * Constants.CellCost) * diag; + return (cellCost * straight + (diagonalCellCost - 2 * cellCost) * diag) * heuristicWeightPercentage / 100; }; } @@ -130,6 +143,12 @@ namespace OpenRA.Mods.Common.Pathfinder return this; } + public IPathSearch WithHeuristicWeight(int percentage) + { + heuristicWeightPercentage = percentage; + return this; + } + public IPathSearch WithCustomCost(Func w) { Graph.CustomCost = w; diff --git a/OpenRA.Mods.Common/Pathfinder/Constants.cs b/OpenRA.Mods.Common/Pathfinder/Constants.cs deleted file mode 100644 index 3e4644d2bb..0000000000 --- a/OpenRA.Mods.Common/Pathfinder/Constants.cs +++ /dev/null @@ -1,32 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2019 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 - -namespace OpenRA.Mods.Common.Pathfinder -{ - public static class Constants - { - /// - /// Min cost to arrive from once cell to an adjacent one - /// (125 according to runtime tests where we could assess the cost - /// a unit took to move one cell horizontally) - /// - public const int CellCost = 125; - - /// - /// Min cost to arrive from once cell to a diagonal adjacent one - /// (125 * Sqrt(2) according to runtime tests where we could assess the cost - /// a unit took to move one cell diagonally) - /// - public const int DiagonalCellCost = 177; - - public const int InvalidNode = int.MaxValue; - } -} diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index 53859de3bf..b8a0e145a6 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -75,6 +75,8 @@ namespace OpenRA.Mods.Common.Pathfinder sealed class PathGraph : IGraph { + public const int CostForInvalidCell = int.MaxValue; + public Actor Actor { get; private set; } public World World { get; private set; } public Func CustomBlock { get; set; } @@ -146,7 +148,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var neighbor = position + directions[i]; var movementCost = GetCostToNode(neighbor, directions[i]); - if (movementCost != Constants.InvalidNode) + if (movementCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(neighbor, movementCost)); } @@ -156,7 +158,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var layerPosition = new CPos(position.X, position.Y, cli.First.Index); var entryCost = cli.First.EntryMovementCost(Actor.Info, locomotor.Info, layerPosition); - if (entryCost != Constants.InvalidNode) + if (entryCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(layerPosition, entryCost)); } } @@ -164,7 +166,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var layerPosition = new CPos(position.X, position.Y, 0); var exitCost = customLayerInfo[position.Layer].First.ExitMovementCost(Actor.Info, locomotor.Info, layerPosition); - if (exitCost != Constants.InvalidNode) + if (exitCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(layerPosition, exitCost)); } @@ -177,7 +179,7 @@ namespace OpenRA.Mods.Common.Pathfinder if (movementCost != short.MaxValue && !(CustomBlock != null && CustomBlock(destNode))) return CalculateCellCost(destNode, direction, movementCost); - return Constants.InvalidNode; + return CostForInvalidCell; } int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost) @@ -190,8 +192,8 @@ namespace OpenRA.Mods.Common.Pathfinder if (CustomCost != null) { var customCost = CustomCost(neighborCPos); - if (customCost == Constants.InvalidNode) - return Constants.InvalidNode; + if (customCost == CostForInvalidCell) + return CostForInvalidCell; cellCost += customCost; } @@ -201,7 +203,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var from = neighborCPos - direction; if (Math.Abs(World.Map.Height[neighborCPos] - World.Map.Height[from]) > 1) - return Constants.InvalidNode; + return CostForInvalidCell; } // Directional bonuses for smoother flow! diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index 782c126f8e..c29bd0c2da 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -57,31 +57,23 @@ namespace OpenRA.Mods.Common.Pathfinder public static IPathSearch FromPoint(World world, Locomotor locomotor, Actor self, CPos @from, CPos target, BlockedByActor check) { - var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check); - var search = new PathSearch(graph) - { - heuristic = DefaultEstimator(target) - }; - - search.isGoal = loc => - { - var locInfo = search.Graph[loc]; - return locInfo.EstimatedTotal - locInfo.CostSoFar == 0; - }; - - if (world.Map.Contains(from)) - search.AddInitialCell(from); - - return search; + return FromPoints(world, locomotor, self, new[] { from }, target, check); } public static IPathSearch FromPoints(World world, Locomotor locomotor, Actor self, IEnumerable froms, CPos target, BlockedByActor check) { var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check); - var search = new PathSearch(graph) - { - heuristic = DefaultEstimator(target) - }; + var search = new PathSearch(graph); + search.heuristic = search.DefaultEstimator(target); + + // The search will aim for the shortest path by default, a weight of 100%. + // We can allow the search to find paths that aren't optimal by changing the weight. + // We provide a weight that 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. + // We allow paths up to 25% longer than the shortest, optimal path, to improve pathfinding time. + search.heuristicWeightPercentage = 125; search.isGoal = loc => { @@ -89,8 +81,9 @@ namespace OpenRA.Mods.Common.Pathfinder return locInfo.EstimatedTotal - locInfo.CostSoFar == 0; }; - foreach (var sl in froms.Where(sl => world.Map.Contains(sl))) - search.AddInitialCell(sl); + foreach (var sl in froms) + if (world.Map.Contains(sl)) + search.AddInitialCell(sl); return search; } @@ -119,7 +112,7 @@ namespace OpenRA.Mods.Common.Pathfinder var currentCell = Graph[currentMinNode]; Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed); - if (Graph.CustomCost != null && Graph.CustomCost(currentMinNode) == Constants.InvalidNode) + if (Graph.CustomCost != null && Graph.CustomCost(currentMinNode) == PathGraph.CostForInvalidCell) return currentMinNode; foreach (var connection in Graph.GetConnections(currentMinNode)) diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index ceb6e44137..49a36f292f 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -194,7 +194,7 @@ namespace OpenRA.Mods.Common.Traits // Too many harvesters clogs up the refinery's delivery location: if (occupancy >= Info.MaxUnloadQueue) - return Constants.InvalidNode; + return PathGraph.CostForInvalidCell; // Prefer refineries with less occupancy (multiplier is to offset distance cost): return occupancy * Info.UnloadQueueCostModifier;