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:
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenRA.Mods.Common.Traits;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.Pathfinder
|
namespace OpenRA.Mods.Common.Pathfinder
|
||||||
@@ -39,6 +41,8 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
|
|
||||||
IPathSearch WithHeuristic(Func<CPos, int> h);
|
IPathSearch WithHeuristic(Func<CPos, int> h);
|
||||||
|
|
||||||
|
IPathSearch WithHeuristicWeight(int percentage);
|
||||||
|
|
||||||
IPathSearch WithCustomCost(Func<CPos, int> w);
|
IPathSearch WithCustomCost(Func<CPos, int> w);
|
||||||
|
|
||||||
IPathSearch WithoutLaneBias();
|
IPathSearch WithoutLaneBias();
|
||||||
@@ -71,6 +75,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
public bool Debug { get; set; }
|
public bool Debug { get; set; }
|
||||||
protected Func<CPos, int> heuristic;
|
protected Func<CPos, int> heuristic;
|
||||||
protected Func<CPos, bool> isGoal;
|
protected Func<CPos, bool> isGoal;
|
||||||
|
protected int heuristicWeightPercentage;
|
||||||
|
|
||||||
// This member is used to compute the ID of PathSearch.
|
// This member is used to compute the ID of PathSearch.
|
||||||
// Essentially, it represents a collection of the initial
|
// Essentially, it represents a collection of the initial
|
||||||
@@ -79,12 +84,20 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
// a deterministic set of calculations
|
// a deterministic set of calculations
|
||||||
protected readonly IPriorityQueue<GraphConnection> StartPoints;
|
protected readonly IPriorityQueue<GraphConnection> StartPoints;
|
||||||
|
|
||||||
|
private readonly int cellCost, diagonalCellCost;
|
||||||
|
|
||||||
protected BasePathSearch(IGraph<CellInfo> graph)
|
protected BasePathSearch(IGraph<CellInfo> graph)
|
||||||
{
|
{
|
||||||
Graph = graph;
|
Graph = graph;
|
||||||
OpenQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
|
OpenQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
|
||||||
StartPoints = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
|
StartPoints = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
|
||||||
MaxCost = 0;
|
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>
|
/// <summary>
|
||||||
@@ -92,7 +105,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A delegate that calculates the estimation for a node</returns>
|
/// <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 =>
|
return here =>
|
||||||
{
|
{
|
||||||
@@ -102,7 +115,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
// According to the information link, this is the shape of the function.
|
// According to the information link, this is the shape of the function.
|
||||||
// We just extract factors to simplify.
|
// We just extract factors to simplify.
|
||||||
// Possible simplification: var h = Constants.CellCost * (straight + (Constants.Sqrt2 - 2) * diag);
|
// 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IPathSearch WithHeuristicWeight(int percentage)
|
||||||
|
{
|
||||||
|
heuristicWeightPercentage = percentage;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public IPathSearch WithCustomCost(Func<CPos, int> w)
|
public IPathSearch WithCustomCost(Func<CPos, int> w)
|
||||||
{
|
{
|
||||||
Graph.CustomCost = w;
|
Graph.CustomCost = w;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,6 +75,8 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
|
|
||||||
sealed class PathGraph : IGraph<CellInfo>
|
sealed class PathGraph : IGraph<CellInfo>
|
||||||
{
|
{
|
||||||
|
public const int CostForInvalidCell = int.MaxValue;
|
||||||
|
|
||||||
public Actor Actor { get; private set; }
|
public Actor Actor { get; private set; }
|
||||||
public World World { get; private set; }
|
public World World { get; private set; }
|
||||||
public Func<CPos, bool> CustomBlock { get; set; }
|
public Func<CPos, bool> CustomBlock { get; set; }
|
||||||
@@ -146,7 +148,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
{
|
{
|
||||||
var neighbor = position + directions[i];
|
var neighbor = position + directions[i];
|
||||||
var movementCost = GetCostToNode(neighbor, directions[i]);
|
var movementCost = GetCostToNode(neighbor, directions[i]);
|
||||||
if (movementCost != Constants.InvalidNode)
|
if (movementCost != CostForInvalidCell)
|
||||||
validNeighbors.Add(new GraphConnection(neighbor, movementCost));
|
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 layerPosition = new CPos(position.X, position.Y, cli.First.Index);
|
||||||
var entryCost = cli.First.EntryMovementCost(Actor.Info, locomotor.Info, layerPosition);
|
var entryCost = cli.First.EntryMovementCost(Actor.Info, locomotor.Info, layerPosition);
|
||||||
if (entryCost != Constants.InvalidNode)
|
if (entryCost != CostForInvalidCell)
|
||||||
validNeighbors.Add(new GraphConnection(layerPosition, entryCost));
|
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 layerPosition = new CPos(position.X, position.Y, 0);
|
||||||
var exitCost = customLayerInfo[position.Layer].First.ExitMovementCost(Actor.Info, locomotor.Info, layerPosition);
|
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));
|
validNeighbors.Add(new GraphConnection(layerPosition, exitCost));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
if (movementCost != short.MaxValue && !(CustomBlock != null && CustomBlock(destNode)))
|
if (movementCost != short.MaxValue && !(CustomBlock != null && CustomBlock(destNode)))
|
||||||
return CalculateCellCost(destNode, direction, movementCost);
|
return CalculateCellCost(destNode, direction, movementCost);
|
||||||
|
|
||||||
return Constants.InvalidNode;
|
return CostForInvalidCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost)
|
int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost)
|
||||||
@@ -190,8 +192,8 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
if (CustomCost != null)
|
if (CustomCost != null)
|
||||||
{
|
{
|
||||||
var customCost = CustomCost(neighborCPos);
|
var customCost = CustomCost(neighborCPos);
|
||||||
if (customCost == Constants.InvalidNode)
|
if (customCost == CostForInvalidCell)
|
||||||
return Constants.InvalidNode;
|
return CostForInvalidCell;
|
||||||
|
|
||||||
cellCost += customCost;
|
cellCost += customCost;
|
||||||
}
|
}
|
||||||
@@ -201,7 +203,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
{
|
{
|
||||||
var from = neighborCPos - direction;
|
var from = neighborCPos - direction;
|
||||||
if (Math.Abs(World.Map.Height[neighborCPos] - World.Map.Height[from]) > 1)
|
if (Math.Abs(World.Map.Height[neighborCPos] - World.Map.Height[from]) > 1)
|
||||||
return Constants.InvalidNode;
|
return CostForInvalidCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directional bonuses for smoother flow!
|
// Directional bonuses for smoother flow!
|
||||||
|
|||||||
@@ -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)
|
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);
|
return FromPoints(world, locomotor, self, new[] { from }, target, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IPathSearch FromPoints(World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, CPos target, BlockedByActor 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 graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check);
|
||||||
var search = new PathSearch(graph)
|
var search = new PathSearch(graph);
|
||||||
{
|
search.heuristic = search.DefaultEstimator(target);
|
||||||
heuristic = 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 =>
|
search.isGoal = loc =>
|
||||||
{
|
{
|
||||||
@@ -89,8 +81,9 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
return locInfo.EstimatedTotal - locInfo.CostSoFar == 0;
|
return locInfo.EstimatedTotal - locInfo.CostSoFar == 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var sl in froms.Where(sl => world.Map.Contains(sl)))
|
foreach (var sl in froms)
|
||||||
search.AddInitialCell(sl);
|
if (world.Map.Contains(sl))
|
||||||
|
search.AddInitialCell(sl);
|
||||||
|
|
||||||
return search;
|
return search;
|
||||||
}
|
}
|
||||||
@@ -119,7 +112,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
|||||||
var currentCell = Graph[currentMinNode];
|
var currentCell = Graph[currentMinNode];
|
||||||
Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed);
|
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;
|
return currentMinNode;
|
||||||
|
|
||||||
foreach (var connection in Graph.GetConnections(currentMinNode))
|
foreach (var connection in Graph.GetConnections(currentMinNode))
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
|
|
||||||
// Too many harvesters clogs up the refinery's delivery location:
|
// Too many harvesters clogs up the refinery's delivery location:
|
||||||
if (occupancy >= Info.MaxUnloadQueue)
|
if (occupancy >= Info.MaxUnloadQueue)
|
||||||
return Constants.InvalidNode;
|
return PathGraph.CostForInvalidCell;
|
||||||
|
|
||||||
// Prefer refineries with less occupancy (multiplier is to offset distance cost):
|
// Prefer refineries with less occupancy (multiplier is to offset distance cost):
|
||||||
return occupancy * Info.UnloadQueueCostModifier;
|
return occupancy * Info.UnloadQueueCostModifier;
|
||||||
|
|||||||
Reference in New Issue
Block a user