From 04912ea996851bdb6aa0a2093487d3181206a29c Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 24 Oct 2019 20:56:30 +0100 Subject: [PATCH] Expose a setting for Weighted A* Replace Constants.CellCost and Constants.DiagonalCellCost with a dynamically calculated value based on the lowest cost terrain to traverse. Using a fixed value meant the pathfinder heuristics would be incorrect. In the four default mods, the minimum cost is in fact 100, not 125. This increase would essentially allow the pathfinder to return suboptimal paths up to 25% longer in the worst case, but it would be quicker to do so. This is exactly what Weighted A* does - overestimate the heuristic by some factor in order to speed up the search by checking fewer routes. This makes the heuristic inadmissible and it may now return suboptimal paths, but their worst case length is bounded by the weight. A weight of 125% will never produce paths more than 25% longer than the shortest, optimal, path. We set the default weight to 25% to effectively maintain the existing, suboptimal, behaviour due to the choice of the old constant - in future it may prove a useful tuning knob for performance. --- .../Pathfinder/BasePathSearch.cs | 23 ++++++++++- OpenRA.Mods.Common/Pathfinder/Constants.cs | 32 --------------- OpenRA.Mods.Common/Pathfinder/PathGraph.cs | 16 ++++---- OpenRA.Mods.Common/Pathfinder/PathSearch.cs | 39 ++++++++----------- OpenRA.Mods.Common/Traits/Harvester.cs | 2 +- 5 files changed, 47 insertions(+), 65 deletions(-) delete mode 100644 OpenRA.Mods.Common/Pathfinder/Constants.cs 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;