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.
This commit is contained in:
RoosterDragon
2019-10-24 20:56:30 +01:00
committed by reaperrr
parent 72eb4e1749
commit 04912ea996
5 changed files with 47 additions and 65 deletions

View File

@@ -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<CPos, int> h);
IPathSearch WithHeuristicWeight(int percentage);
IPathSearch WithCustomCost(Func<CPos, int> w);
IPathSearch WithoutLaneBias();
@@ -71,6 +75,7 @@ namespace OpenRA.Mods.Common.Pathfinder
public bool Debug { get; set; }
protected Func<CPos, int> heuristic;
protected Func<CPos, bool> 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<GraphConnection> StartPoints;
private readonly int cellCost, diagonalCellCost;
protected BasePathSearch(IGraph<CellInfo> graph)
{
Graph = graph;
OpenQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
StartPoints = new PriorityQueue<GraphConnection>(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<Mobile>().Locomotor.Info.TerrainSpeeds.Values.Min(ti => ti.Cost);
diagonalCellCost = cellCost * 141421 / 100000;
}
/// <summary>
@@ -92,7 +105,7 @@ namespace OpenRA.Mods.Common.Pathfinder
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
/// </summary>
/// <returns>A delegate that calculates the estimation for a node</returns>
protected static Func<CPos, int> DefaultEstimator(CPos destination)
protected Func<CPos, int> 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<CPos, int> w)
{
Graph.CustomCost = w;

View File

@@ -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
{
/// <summary>
/// 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)
/// </summary>
public const int CellCost = 125;
/// <summary>
/// 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)
/// </summary>
public const int DiagonalCellCost = 177;
public const int InvalidNode = int.MaxValue;
}
}

View File

@@ -75,6 +75,8 @@ namespace OpenRA.Mods.Common.Pathfinder
sealed class PathGraph : IGraph<CellInfo>
{
public const int CostForInvalidCell = int.MaxValue;
public Actor Actor { get; private set; }
public World World { get; private set; }
public Func<CPos, bool> 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!

View File

@@ -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<CPos> 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,7 +81,8 @@ namespace OpenRA.Mods.Common.Pathfinder
return locInfo.EstimatedTotal - locInfo.CostSoFar == 0;
};
foreach (var sl in froms.Where(sl => world.Map.Contains(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))

View File

@@ -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;