Rearrange various API surfaces related to pathfinding.

The existing APIs surfaces for pathfinding are in a wonky shape. We rearrange various responsibilities to better locations and simplify some abstractions that aren't providing value.

- IPathSearch, BasePathSearch and PathSearch are combined into only PathSearch. Its role is now to run a search space over a graph, maintaining the open queue and evaluating the provided heuristic function. The builder-like methods (WithHeuristic, Reverse, FromPoint, etc) are removed in favour of optional parameters in static creation methods. This removes confusion between the builder-aspect and the search function itself. It also becomes responsible for applying the heuristic weight to the heuristic. This fixes an issue where an externally provided heuristic ignored the weighting adjustment, as previously the weight was baked into the default heuristic only.
- Reduce the IGraph interface to the concepts of nodes and edges. Make it non-generic as it is specifically for pathfinding, and rename to IPathGraph accordingly. This is sufficient for a PathSearch to perform a search over any given IGraph. The various customization options are concrete properties of PathGraph only.
- PathFinder does not need to deal with disposal of the search/graph, that is the caller's responsibility.
- Remove CustomBlock from PathGraph as it was unused.
- Remove FindUnitPathToRange as it was unused.
- Use PathFinder.NoPath as the single helper to represent no/empty paths.
This commit is contained in:
RoosterDragon
2021-11-27 12:23:08 +00:00
committed by reaperrr
parent cd1fe2d23b
commit 6dc189b7d1
13 changed files with 379 additions and 498 deletions

View File

@@ -185,9 +185,14 @@ namespace OpenRA.Mods.Common.Activities
// Find any harvestable resources: // Find any harvestable resources:
List<CPos> path; List<CPos> path;
using (var search = PathSearch.Search(self.World, mobile.Locomotor, self, BlockedByActor.Stationary, loc => using (var search = PathSearch.ToTargetCellByPredicate(
domainIndex.IsPassable(self.Location, loc, mobile.Locomotor) && harv.CanHarvestCell(self, loc) && claimLayer.CanClaimCell(self, loc)) self.World, mobile.Locomotor, self, new[] { searchFromLoc, self.Location },
.WithCustomCost(loc => loc =>
domainIndex.IsPassable(self.Location, loc, mobile.Locomotor) &&
harv.CanHarvestCell(self, loc) &&
claimLayer.CanClaimCell(self, loc),
BlockedByActor.Stationary,
loc =>
{ {
if ((loc - searchFromLoc).LengthSquared > searchRadiusSquared) if ((loc - searchFromLoc).LengthSquared > searchRadiusSquared)
return PathGraph.PathCostForInvalidPath; return PathGraph.PathCostForInvalidPath;
@@ -216,9 +221,7 @@ namespace OpenRA.Mods.Common.Activities
} }
return 0; return 0;
}) }))
.FromPoint(searchFromLoc)
.FromPoint(self.Location))
path = mobile.Pathfinder.FindPath(search); path = mobile.Pathfinder.FindPath(search);
if (path.Count > 0) if (path.Count > 0)

View File

@@ -22,8 +22,6 @@ namespace OpenRA.Mods.Common.Activities
{ {
public class Move : Activity public class Move : Activity
{ {
static readonly List<CPos> NoPath = new List<CPos>();
readonly Mobile mobile; readonly Mobile mobile;
readonly WDist nearEnough; readonly WDist nearEnough;
readonly Func<BlockedByActor, List<CPos>> getPath; readonly Func<BlockedByActor, List<CPos>> getPath;
@@ -52,7 +50,7 @@ namespace OpenRA.Mods.Common.Activities
readonly bool evaluateNearestMovableCell; readonly bool evaluateNearestMovableCell;
// Scriptable move order // Scriptable move order
// Ignores lane bias and nearby units // Ignores lane bias
public Move(Actor self, CPos destination, Color? targetLineColor = null) public Move(Actor self, CPos destination, Color? targetLineColor = null)
{ {
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
@@ -60,12 +58,9 @@ namespace OpenRA.Mods.Common.Activities
getPath = check => getPath = check =>
{ {
List<CPos> path; using (var search = PathSearch.ToTargetCell(
using (var search = self.World, mobile.Locomotor, self, mobile.ToCell, destination, check, laneBias: false))
PathSearch.FromPoint(self.World, mobile.Locomotor, self, mobile.ToCell, destination, check) return mobile.Pathfinder.FindPath(search);
.WithoutLaneBias())
path = mobile.Pathfinder.FindPath(search);
return path;
}; };
this.destination = destination; this.destination = destination;
@@ -82,7 +77,7 @@ namespace OpenRA.Mods.Common.Activities
getPath = check => getPath = check =>
{ {
if (!this.destination.HasValue) if (!this.destination.HasValue)
return NoPath; return PathFinder.NoPath;
return mobile.Pathfinder.FindUnitPath(mobile.ToCell, this.destination.Value, self, ignoreActor, check); return mobile.Pathfinder.FindUnitPath(mobile.ToCell, this.destination.Value, self, ignoreActor, check);
}; };

View File

@@ -21,8 +21,6 @@ namespace OpenRA.Mods.Common.Activities
{ {
public class MoveAdjacentTo : Activity public class MoveAdjacentTo : Activity
{ {
static readonly List<CPos> NoPath = new List<CPos>();
protected readonly Mobile Mobile; protected readonly Mobile Mobile;
readonly DomainIndex domainIndex; readonly DomainIndex domainIndex;
readonly Color? targetLineColor; readonly Color? targetLineColor;
@@ -130,10 +128,10 @@ namespace OpenRA.Mods.Common.Activities
} }
if (!searchCells.Any()) if (!searchCells.Any())
return NoPath; return PathFinder.NoPath;
using (var fromSrc = PathSearch.FromPoints(self.World, Mobile.Locomotor, self, searchCells, loc, check)) using (var fromSrc = PathSearch.ToTargetCell(self.World, Mobile.Locomotor, self, searchCells, loc, check))
using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Locomotor, self, loc, lastVisibleTargetLocation, check).Reverse()) using (var fromDest = PathSearch.ToTargetCell(self.World, Mobile.Locomotor, self, loc, lastVisibleTargetLocation, check, inReverse: true))
return Mobile.Pathfinder.FindBidiPath(fromSrc, fromDest); return Mobile.Pathfinder.FindBidiPath(fromSrc, fromDest);
} }

View File

@@ -1,182 +0,0 @@
#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.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
namespace OpenRA.Mods.Common.Pathfinder
{
public interface IPathSearch : IDisposable
{
/// <summary>
/// The Graph used by the A*
/// </summary>
IGraph<CellInfo> Graph { get; }
Player Owner { get; }
IPathSearch Reverse();
IPathSearch WithCustomBlocker(Func<CPos, bool> customBlock);
IPathSearch WithIgnoredActor(Actor b);
IPathSearch WithHeuristic(Func<CPos, int> h);
IPathSearch WithHeuristicWeight(int percentage);
IPathSearch WithCustomCost(Func<CPos, int> w);
IPathSearch WithoutLaneBias();
IPathSearch FromPoint(CPos from);
/// <summary>
/// Decides whether a location is a target based on its estimate
/// (An estimate of 0 means that the location and the unit's goal
/// are the same. There could be multiple goals).
/// </summary>
/// <param name="location">The location to assess</param>
/// <returns>Whether the location is a target</returns>
bool IsTarget(CPos location);
bool CanExpand { get; }
CPos Expand();
}
public abstract class BasePathSearch : IPathSearch
{
public IGraph<CellInfo> Graph { get; set; }
protected IPriorityQueue<GraphConnection> OpenQueue { get; private set; }
public Player Owner => Graph.Actor.Owner;
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
// points considered and their Heuristics to reach
// the target. It pretty match identifies, in conjunction of the Actor,
// a deterministic set of calculations
protected readonly IPriorityQueue<GraphConnection> StartPoints;
readonly short cellCost;
readonly int diagonalCellCost;
protected BasePathSearch(IGraph<CellInfo> graph)
{
Graph = graph;
OpenQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
StartPoints = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
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 = ((Mobile)graph.Actor.OccupiesSpace).Info.LocomotorInfo.TerrainSpeeds.Values.Min(ti => ti.Cost);
diagonalCellCost = Exts.MultiplyBySqrtTwo(cellCost);
}
/// <summary>
/// Default: Diagonal distance heuristic. More information:
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
/// </summary>
/// <returns>A delegate that calculates the estimation for a node</returns>
protected Func<CPos, int> DefaultEstimator(CPos destination)
{
return here =>
{
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) * heuristicWeightPercentage / 100;
};
}
public IPathSearch Reverse()
{
Graph.InReverse = true;
return this;
}
public IPathSearch WithCustomBlocker(Func<CPos, bool> customBlock)
{
Graph.CustomBlock = customBlock;
return this;
}
public IPathSearch WithIgnoredActor(Actor b)
{
Graph.IgnoreActor = b;
return this;
}
public IPathSearch WithHeuristic(Func<CPos, int> h)
{
heuristic = h;
return this;
}
public IPathSearch WithHeuristicWeight(int percentage)
{
heuristicWeightPercentage = percentage;
return this;
}
public IPathSearch WithCustomCost(Func<CPos, int> w)
{
Graph.CustomCost = w;
return this;
}
public IPathSearch WithoutLaneBias()
{
Graph.LaneBias = 0;
return this;
}
public IPathSearch FromPoint(CPos from)
{
if (Graph.World.Map.Contains(from))
AddInitialCell(from);
return this;
}
protected abstract void AddInitialCell(CPos cell);
public bool IsTarget(CPos location)
{
return isGoal(location);
}
public bool CanExpand => !OpenQueue.Empty;
public abstract CPos Expand();
protected virtual void Dispose(bool disposing)
{
if (disposing)
Graph.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,68 @@
#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;
namespace OpenRA.Mods.Common.Pathfinder
{
/// <summary>
/// Represents a pathfinding graph with nodes and edges.
/// Nodes are represented as cells, and pathfinding information
/// in the form of <see cref="CellInfo"/> is attached to each one.
/// </summary>
public interface IPathGraph : IDisposable
{
/// <summary>
/// Given a source node, returns connections to all reachable destination nodes with their cost.
/// </summary>
List<GraphConnection> GetConnections(CPos source);
/// <summary>
/// Gets or sets the pathfinding information for a given node.
/// </summary>
CellInfo this[CPos node] { get; set; }
}
/// <summary>
/// Represents part of an edge in a graph, giving the cost to traverse to a node.
/// </summary>
public readonly struct GraphConnection
{
public static readonly CostComparer ConnectionCostComparer = CostComparer.Instance;
public sealed class CostComparer : IComparer<GraphConnection>
{
public static readonly CostComparer Instance = new CostComparer();
CostComparer() { }
public int Compare(GraphConnection x, GraphConnection y)
{
return x.Cost.CompareTo(y.Cost);
}
}
public readonly CPos Destination;
public readonly int Cost;
public GraphConnection(CPos destination, int cost)
{
if (cost < 0)
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be negative");
if (cost == PathGraph.PathCostForInvalidPath)
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be used for an unreachable path");
Destination = destination;
Cost = cost;
}
public override string ToString() => $"-> {Destination} = {Cost}";
}
}

View File

@@ -18,100 +18,54 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Pathfinder namespace OpenRA.Mods.Common.Pathfinder
{ {
/// <summary> /// <summary>
/// Represents a graph with nodes and edges /// A dense pathfinding graph that supports a search over all cells within a map.
/// It implements the ability to cost and get connections for cells, and supports <see cref="ICustomMovementLayer"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The type of node used in the graph</typeparam> sealed class PathGraph : IPathGraph
public interface IGraph<T> : IDisposable
{
/// <summary>
/// Gets all the Connections for a given node in the graph
/// </summary>
List<GraphConnection> GetConnections(CPos position);
/// <summary>
/// Retrieves an object given a node in the graph
/// </summary>
T this[CPos pos] { get; set; }
Func<CPos, bool> CustomBlock { get; set; }
Func<CPos, int> CustomCost { get; set; }
int LaneBias { get; set; }
bool InReverse { get; set; }
Actor IgnoreActor { get; set; }
World World { get; }
Actor Actor { get; }
}
public readonly struct GraphConnection
{
public static readonly CostComparer ConnectionCostComparer = CostComparer.Instance;
public sealed class CostComparer : IComparer<GraphConnection>
{
public static readonly CostComparer Instance = new CostComparer();
CostComparer() { }
public int Compare(GraphConnection x, GraphConnection y)
{
return x.Cost.CompareTo(y.Cost);
}
}
public readonly CPos Destination;
public readonly int Cost;
public GraphConnection(CPos destination, int cost)
{
Destination = destination;
Cost = cost;
}
}
sealed class PathGraph : IGraph<CellInfo>
{ {
public const int PathCostForInvalidPath = int.MaxValue; public const int PathCostForInvalidPath = int.MaxValue;
public const short MovementCostForUnreachableCell = short.MaxValue; public const short MovementCostForUnreachableCell = short.MaxValue;
const int LaneBiasCost = 1;
public Actor Actor { get; private set; } readonly ICustomMovementLayer[] customMovementLayers;
public World World { get; private set; } readonly int customMovementLayersEnabledForLocomotor;
public Func<CPos, bool> CustomBlock { get; set; }
public Func<CPos, int> CustomCost { get; set; }
public int LaneBias { get; set; }
public bool InReverse { get; set; }
public Actor IgnoreActor { get; set; }
readonly BlockedByActor checkConditions;
readonly Locomotor locomotor; readonly Locomotor locomotor;
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer; readonly Actor actor;
readonly World world;
readonly BlockedByActor check;
readonly Func<CPos, int> customCost;
readonly Actor ignoreActor;
readonly bool inReverse;
readonly bool laneBias;
readonly bool checkTerrainHeight; readonly bool checkTerrainHeight;
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer;
readonly CellLayer<CellInfo>[] cellInfoForLayer; readonly CellLayer<CellInfo>[] cellInfoForLayer;
public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check) public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check,
Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, bool laneBias)
{ {
customMovementLayers = world.GetCustomMovementLayers();
customMovementLayersEnabledForLocomotor = customMovementLayers.Count(cml => cml != null && cml.EnabledForLocomotor(locomotor.Info));
this.locomotor = locomotor; this.locomotor = locomotor;
this.world = world;
this.actor = actor;
this.check = check;
this.customCost = customCost;
this.ignoreActor = ignoreActor;
this.inReverse = inReverse;
this.laneBias = laneBias;
checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
// As we support a search over the whole map area, // As we support a search over the whole map area,
// use the pool to grab the CellInfos we need to track the graph state. // use the pool to grab the CellInfos we need to track the graph state.
// This allows us to avoid the cost of allocating large arrays constantly. // This allows us to avoid the cost of allocating large arrays constantly.
// PERF: Avoid LINQ // PERF: Avoid LINQ
var cmls = world.GetCustomMovementLayers();
pooledLayer = layerPool.Get(); pooledLayer = layerPool.Get();
cellInfoForLayer = new CellLayer<CellInfo>[cmls.Length]; cellInfoForLayer = new CellLayer<CellInfo>[customMovementLayers.Length];
cellInfoForLayer[0] = pooledLayer.GetLayer(); cellInfoForLayer[0] = pooledLayer.GetLayer();
foreach (var cml in cmls) foreach (var cml in customMovementLayers)
if (cml != null && cml.EnabledForLocomotor(locomotor.Info)) if (cml != null && cml.EnabledForLocomotor(locomotor.Info))
cellInfoForLayer[cml.Index] = pooledLayer.GetLayer(); cellInfoForLayer[cml.Index] = pooledLayer.GetLayer();
World = world;
Actor = actor;
LaneBias = 1;
checkConditions = check;
checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
} }
// Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed
@@ -156,35 +110,34 @@ namespace OpenRA.Mods.Common.Pathfinder
public List<GraphConnection> GetConnections(CPos position) public List<GraphConnection> GetConnections(CPos position)
{ {
var layer = position.Layer; var layer = position.Layer;
var info = cellInfoForLayer[layer]; var info = this[position];
var previousNode = info[position].PreviousNode; var previousNode = info.PreviousNode;
var dx = position.X - previousNode.X; var dx = position.X - previousNode.X;
var dy = position.Y - previousNode.Y; var dy = position.Y - previousNode.Y;
var index = dy * 3 + dx + 4; var index = dy * 3 + dx + 4;
var heightLayer = World.Map.Height; var heightLayer = world.Map.Height;
var directions = var directions =
(checkTerrainHeight && layer == 0 && previousNode.Layer == 0 && heightLayer[position] != heightLayer[previousNode] (checkTerrainHeight && layer == 0 && previousNode.Layer == 0 && heightLayer[position] != heightLayer[previousNode]
? DirectedNeighborsConservative ? DirectedNeighborsConservative
: DirectedNeighbors)[index]; : DirectedNeighbors)[index];
var validNeighbors = new List<GraphConnection>(directions.Length + (layer == 0 ? cellInfoForLayer.Length : 1)); var validNeighbors = new List<GraphConnection>(directions.Length + (layer == 0 ? customMovementLayersEnabledForLocomotor : 1));
for (var i = 0; i < directions.Length; i++) for (var i = 0; i < directions.Length; i++)
{ {
var dir = directions[i]; var dir = directions[i];
var neighbor = position + dir; var neighbor = position + dir;
var pathCost = GetPathCostToNode(position, neighbor, dir);
// PERF: Skip closed cells already, 15% of all cells var pathCost = GetPathCostToNode(position, neighbor, dir);
if (pathCost != PathCostForInvalidPath && info[neighbor].Status != CellStatus.Closed) if (pathCost != PathCostForInvalidPath &&
this[neighbor].Status != CellStatus.Closed)
validNeighbors.Add(new GraphConnection(neighbor, pathCost)); validNeighbors.Add(new GraphConnection(neighbor, pathCost));
} }
var cmls = World.GetCustomMovementLayers();
if (layer == 0) if (layer == 0)
{ {
foreach (var cml in cmls) foreach (var cml in customMovementLayers)
{ {
if (cml == null || !cml.EnabledForLocomotor(locomotor.Info)) if (cml == null || !cml.EnabledForLocomotor(locomotor.Info))
continue; continue;
@@ -199,12 +152,12 @@ namespace OpenRA.Mods.Common.Pathfinder
} }
else else
{ {
var layerPosition = new CPos(position.X, position.Y, 0); var groundPosition = new CPos(position.X, position.Y, 0);
var exitCost = cmls[layer].ExitMovementCost(locomotor.Info, layerPosition); var exitCost = customMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
if (exitCost != MovementCostForUnreachableCell && if (exitCost != MovementCostForUnreachableCell &&
CanEnterNode(position, layerPosition) && CanEnterNode(position, groundPosition) &&
this[layerPosition].Status != CellStatus.Closed) this[groundPosition].Status != CellStatus.Closed)
validNeighbors.Add(new GraphConnection(layerPosition, exitCost)); validNeighbors.Add(new GraphConnection(groundPosition, exitCost));
} }
return validNeighbors; return validNeighbors;
@@ -213,14 +166,14 @@ namespace OpenRA.Mods.Common.Pathfinder
bool CanEnterNode(CPos srcNode, CPos destNode) bool CanEnterNode(CPos srcNode, CPos destNode)
{ {
return return
locomotor.MovementCostToEnterCell(Actor, srcNode, destNode, checkConditions, IgnoreActor) locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor)
!= MovementCostForUnreachableCell; != MovementCostForUnreachableCell;
} }
int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction) int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction)
{ {
var movementCost = locomotor.MovementCostToEnterCell(Actor, srcNode, destNode, checkConditions, IgnoreActor); var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor);
if (movementCost != MovementCostForUnreachableCell && !(CustomBlock != null && CustomBlock(destNode))) if (movementCost != MovementCostForUnreachableCell)
return CalculateCellPathCost(destNode, direction, movementCost); return CalculateCellPathCost(destNode, direction, movementCost);
return PathCostForInvalidPath; return PathCostForInvalidPath;
@@ -232,26 +185,26 @@ namespace OpenRA.Mods.Common.Pathfinder
? Exts.MultiplyBySqrtTwo(movementCost) ? Exts.MultiplyBySqrtTwo(movementCost)
: movementCost; : movementCost;
if (CustomCost != null) if (customCost != null)
{ {
var customCost = CustomCost(neighborCPos); var customCellCost = customCost(neighborCPos);
if (customCost == PathCostForInvalidPath) if (customCellCost == PathCostForInvalidPath)
return PathCostForInvalidPath; return PathCostForInvalidPath;
cellCost += customCost; cellCost += customCellCost;
} }
// Directional bonuses for smoother flow! // Directional bonuses for smoother flow!
if (LaneBias != 0) if (laneBias)
{ {
var ux = neighborCPos.X + (InReverse ? 1 : 0) & 1; var ux = neighborCPos.X + (inReverse ? 1 : 0) & 1;
var uy = neighborCPos.Y + (InReverse ? 1 : 0) & 1; var uy = neighborCPos.Y + (inReverse ? 1 : 0) & 1;
if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0)) if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0))
cellCost += LaneBias; cellCost += LaneBiasCost;
if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0)) if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0))
cellCost += LaneBias; cellCost += LaneBiasCost;
} }
return cellCost; return cellCost;

View File

@@ -11,14 +11,29 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA.Mods.Common.Pathfinder namespace OpenRA.Mods.Common.Pathfinder
{ {
public sealed class PathSearch : BasePathSearch public sealed class PathSearch : IDisposable
{ {
/// <summary>
/// When searching for paths, use a default weight of 125% to reduce
/// computation effort - even if this means paths may be sub-optimal.
/// </summary>
public const int DefaultHeuristicWeightPercentage = 125;
/// <summary>
/// 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.
/// </summary>
public const bool DefaultLaneBias = true;
// PERF: Maintain a pool of layers used for paths searches for each world. These searches are performed often // 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. // so we wish to avoid the high cost of initializing a new search space every time by reusing the old ones.
static readonly ConditionalWeakTable<World, CellInfoLayerPool> LayerPoolTable = new ConditionalWeakTable<World, CellInfoLayerPool>(); static readonly ConditionalWeakTable<World, CellInfoLayerPool> LayerPoolTable = new ConditionalWeakTable<World, CellInfoLayerPool>();
@@ -29,46 +44,12 @@ namespace OpenRA.Mods.Common.Pathfinder
return LayerPoolTable.GetValue(world, CreateLayerPool); return LayerPoolTable.GetValue(world, CreateLayerPool);
} }
PathSearch(IGraph<CellInfo> graph) public static PathSearch ToTargetCellByPredicate(
: base(graph) World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
Func<CPos, int> 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);
public static IPathSearch Search(World world, Locomotor locomotor, Actor self, BlockedByActor check, Func<CPos, bool> goalCondition)
{
var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check);
return new PathSearch(graph)
{
isGoal = goalCondition,
heuristic = loc => 0
};
}
public static IPathSearch FromPoint(World world, Locomotor locomotor, Actor self, CPos @from, CPos target, BlockedByActor check)
{
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);
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 =>
{
var locInfo = search.Graph[loc];
return locInfo.EstimatedTotalCost - locInfo.CostSoFar == 0;
};
foreach (var sl in froms) foreach (var sl in froms)
if (world.Map.Contains(sl)) if (world.Map.Contains(sl))
@@ -77,30 +58,131 @@ namespace OpenRA.Mods.Common.Pathfinder
return search; return search;
} }
protected override void AddInitialCell(CPos location) public static PathSearch ToTargetCell(
World world, Locomotor locomotor, Actor self, CPos from, CPos target, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool inReverse = false,
bool laneBias = DefaultLaneBias)
{ {
var cost = heuristic(location); return ToTargetCell(world, locomotor, self, new[] { from }, target, check, customCost, ignoreActor, inReverse, laneBias);
Graph[location] = new CellInfo(CellStatus.Open, 0, cost, location); }
var connection = new GraphConnection(location, cost);
OpenQueue.Add(connection); public static PathSearch ToTargetCell(
StartPoints.Add(connection); World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, CPos target, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool inReverse = false,
bool laneBias = DefaultLaneBias,
Func<CPos, int> 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;
} }
/// <summary> /// <summary>
/// This function analyzes the neighbors of the most promising node in the Pathfinding graph /// Default: Diagonal distance heuristic. More information:
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
/// Layers are ignored and incur no additional cost.
/// </summary>
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
/// <param name="destination">The cell for which costs are to be given by the estimation function.</param>
/// <returns>A delegate that calculates the cost estimation between the <paramref name="destination"/> and the given cell.</returns>
public static Func<CPos, int> DefaultCostEstimator(Locomotor locomotor, CPos destination)
{
var estimator = DefaultCostEstimator(locomotor);
return here => estimator(here, destination);
}
/// <summary>
/// Default: Diagonal distance heuristic. More information:
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
/// Layers are ignored and incur no additional cost.
/// </summary>
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
/// <returns>A delegate that calculates the cost estimation between the given cells.</returns>
public static Func<CPos, CPos, int> 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<CPos, int> heuristic;
readonly int heuristicWeightPercentage;
public Func<CPos, bool> TargetPredicate { get; set; }
readonly IPriorityQueue<GraphConnection> openQueue;
/// <summary>
/// Initialize a new search.
/// </summary>
/// <param name="graph">Graph over which the search is conducted.</param>
/// <param name="heuristic">Provides an estimation of the distance between the given cell and the target.</param>
/// <param name="heuristicWeightPercentage">
/// 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.
/// </param>
/// <param name="targetPredicate">Determines if the given cell is the target.</param>
PathSearch(IPathGraph graph, Func<CPos, int> heuristic, int heuristicWeightPercentage, Func<CPos, bool> targetPredicate)
{
Graph = graph;
this.heuristic = heuristic;
this.heuristicWeightPercentage = heuristicWeightPercentage;
TargetPredicate = targetPredicate;
openQueue = new PriorityQueue<GraphConnection>(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);
}
/// <summary>
/// Determines if there are more reachable cells and the search can be continued.
/// If false, <see cref="Expand"/> can no longer be called.
/// </summary>
public bool CanExpand => !openQueue.Empty;
/// <summary>
/// This function analyzes the neighbors of the most promising node in the pathfinding graph
/// using the A* algorithm (A-star) and returns that node /// using the A* algorithm (A-star) and returns that node
/// </summary> /// </summary>
/// <returns>The most promising node of the iteration</returns> /// <returns>The most promising node of the iteration</returns>
public override CPos Expand() public CPos Expand()
{ {
var currentMinNode = OpenQueue.Pop().Destination; var currentMinNode = openQueue.Pop().Destination;
var currentInfo = Graph[currentMinNode]; var currentInfo = Graph[currentMinNode];
Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode); Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode);
if (Graph.CustomCost != null && Graph.CustomCost(currentMinNode) == PathGraph.PathCostForInvalidPath)
return currentMinNode;
foreach (var connection in Graph.GetConnections(currentMinNode)) foreach (var connection in Graph.GetConnections(currentMinNode))
{ {
// Calculate the cost up to that point // Calculate the cost up to that point
@@ -121,16 +203,29 @@ namespace OpenRA.Mods.Common.Pathfinder
if (neighborInfo.Status == CellStatus.Open) if (neighborInfo.Status == CellStatus.Open)
estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar; estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar;
else else
estimatedRemainingCostToTarget = heuristic(neighbor); estimatedRemainingCostToTarget = heuristic(neighbor) * heuristicWeightPercentage / 100;
var estimatedTotalCostToTarget = costSoFarToNeighbor + estimatedRemainingCostToTarget; var estimatedTotalCostToTarget = costSoFarToNeighbor + estimatedRemainingCostToTarget;
Graph[neighbor] = new CellInfo(CellStatus.Open, costSoFarToNeighbor, estimatedTotalCostToTarget, currentMinNode); Graph[neighbor] = new CellInfo(CellStatus.Open, costSoFarToNeighbor, estimatedTotalCostToTarget, currentMinNode);
if (neighborInfo.Status != CellStatus.Open) if (neighborInfo.Status != CellStatus.Open)
OpenQueue.Add(new GraphConnection(neighbor, estimatedTotalCostToTarget)); openQueue.Add(new GraphConnection(neighbor, estimatedTotalCostToTarget));
} }
return currentMinNode; return currentMinNode;
} }
/// <summary>
/// Determines if <paramref name="location"/> is the target of the search.
/// </summary>
public bool IsTarget(CPos location)
{
return TargetPredicate(location);
}
public void Dispose()
{
Graph.Dispose();
}
} }
} }

View File

@@ -149,12 +149,14 @@ namespace OpenRA.Mods.Common.Traits
harv.Harvester.CanHarvestCell(actor, cell) && harv.Harvester.CanHarvestCell(actor, cell) &&
claimLayer.CanClaimCell(actor, cell); claimLayer.CanClaimCell(actor, cell);
var path = pathfinder.FindPath( List<CPos> path;
PathSearch.Search(world, harv.Locomotor, actor, BlockedByActor.Stationary, isValidResource) using (var search =
.WithCustomCost(loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius) PathSearch.ToTargetCellByPredicate(
world, harv.Locomotor, actor, new[] { actor.Location }, isValidResource, BlockedByActor.Stationary,
loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius)
.Where(u => !u.IsDead && actor.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy) .Where(u => !u.IsDead && actor.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy)
.Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))) .Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))))
.FromPoint(actor.Location)); path = pathfinder.FindPath(search);
if (path.Count == 0) if (path.Count == 0)
return Target.Invalid; return Target.Invalid;

View File

@@ -195,8 +195,9 @@ namespace OpenRA.Mods.Common.Traits
// Start a search from each refinery's delivery location: // Start a search from each refinery's delivery location:
List<CPos> path; List<CPos> path;
using (var search = PathSearch.FromPoints(self.World, mobile.Locomotor, self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None) using (var search = PathSearch.ToTargetCell(
.WithCustomCost(location => self.World, mobile.Locomotor, self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None,
location =>
{ {
if (!refineries.Contains(location)) if (!refineries.Contains(location))
return 0; return 0;
@@ -212,7 +213,7 @@ namespace OpenRA.Mods.Common.Traits
})) }))
path = mobile.Pathfinder.FindPath(search); path = mobile.Pathfinder.FindPath(search);
if (path.Count != 0) if (path.Count > 0)
return refineries[path.Last()].First().Actor; return refineries[path.Last()].First().Actor;
return null; return null;

View File

@@ -799,7 +799,6 @@ namespace OpenRA.Mods.Common.Traits
notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes); notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes);
} }
public Activity ScriptedMove(CPos cell) { return new Move(self, cell); }
public Activity MoveTo(Func<BlockedByActor, List<CPos>> pathFunc) { return new Move(self, pathFunc); } public Activity MoveTo(Func<BlockedByActor, List<CPos>> pathFunc) { return new Move(self, pathFunc); }
Activity LocalMove(Actor self, WPos fromPos, WPos toPos, CPos cell) Activity LocalMove(Actor self, WPos fromPos, WPos toPos, CPos cell)
@@ -821,9 +820,8 @@ namespace OpenRA.Mods.Common.Traits
return above; return above;
List<CPos> path; List<CPos> path;
using (var search = PathSearch.Search(self.World, Locomotor, self, BlockedByActor.All, using (var search = PathSearch.ToTargetCellByPredicate(
loc => loc.Layer == 0 && CanEnterCell(loc)) self.World, Locomotor, self, new[] { self.Location }, loc => loc.Layer == 0 && CanEnterCell(loc), BlockedByActor.All))
.FromPoint(self.Location))
path = Pathfinder.FindPath(search); path = Pathfinder.FindPath(search);
if (path.Count > 0) if (path.Count > 0)

View File

@@ -211,15 +211,15 @@ namespace OpenRA.Mods.Common.Traits
void INotifyCreated.Created(Actor self) void INotifyCreated.Created(Actor self)
{ {
var cmls = self.TraitsImplementing<ICustomMovementLayer>().ToList(); var customMovementLayers = self.TraitsImplementing<ICustomMovementLayer>().ToList();
if (cmls.Count == 0) if (customMovementLayers.Count == 0)
return; return;
var length = cmls.Max(cml => cml.Index) + 1; var length = customMovementLayers.Max(cml => cml.Index) + 1;
Array.Resize(ref CustomMovementLayers, length); Array.Resize(ref CustomMovementLayers, length);
Array.Resize(ref influence, length); Array.Resize(ref influence, length);
foreach (var cml in cmls) foreach (var cml in customMovementLayers)
{ {
CustomMovementLayers[cml.Index] = cml; CustomMovementLayers[cml.Index] = cml;
influence[cml.Index] = new CellLayer<InfluenceNode>(self.World.Map); influence[cml.Index] = new CellLayer<InfluenceNode>(self.World.Map);

View File

@@ -379,10 +379,10 @@ namespace OpenRA.Mods.Common.Traits
// This section needs to run after WorldLoaded() because we need to be sure that all types of ICustomMovementLayer have been initialized. // This section needs to run after WorldLoaded() because we need to be sure that all types of ICustomMovementLayer have been initialized.
w.AddFrameEndTask(_ => w.AddFrameEndTask(_ =>
{ {
var cmls = world.GetCustomMovementLayers(); var customMovementLayers = world.GetCustomMovementLayers();
Array.Resize(ref cellsCost, cmls.Length); Array.Resize(ref cellsCost, customMovementLayers.Length);
Array.Resize(ref blockingCache, cmls.Length); Array.Resize(ref blockingCache, customMovementLayers.Length);
foreach (var cml in cmls) foreach (var cml in customMovementLayers)
{ {
if (cml == null) if (cml == null)
continue; continue;

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Traits; using OpenRA.Traits;
@@ -29,29 +28,28 @@ namespace OpenRA.Mods.Common.Traits
public interface IPathFinder public interface IPathFinder
{ {
/// <summary> /// <summary>
/// Calculates a path for the actor from source to destination /// Calculates a path for the actor from source to target.
/// Returned path is *reversed* and given target to source.
/// </summary> /// </summary>
/// <returns>A path from start to target</returns>
List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check); List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check);
List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self, BlockedByActor check); /// <summary>
/// Expands the path search until a path is found, and returns that path.
/// Returned path is *reversed* and given target to source.
/// </summary>
List<CPos> FindPath(PathSearch search);
/// <summary> /// <summary>
/// Calculates a path given a search specification /// Expands both path searches until they intersect, and returns the path.
/// Returned path is from the source of the first search to the source of the second search.
/// </summary> /// </summary>
List<CPos> FindPath(IPathSearch search); List<CPos> FindBidiPath(PathSearch fromSrc, PathSearch fromDest);
/// <summary>
/// Calculates a path given two search specifications, and
/// then returns a path when both search intersect each other
/// TODO: This should eventually disappear
/// </summary>
List<CPos> FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest);
} }
public class PathFinder : IPathFinder public class PathFinder : IPathFinder
{ {
static readonly List<CPos> EmptyPath = new List<CPos>(0); public static readonly List<CPos> NoPath = new List<CPos>(0);
readonly World world; readonly World world;
DomainIndex domainIndex; DomainIndex domainIndex;
bool cached; bool cached;
@@ -61,6 +59,10 @@ namespace OpenRA.Mods.Common.Traits
this.world = world; this.world = world;
} }
/// <summary>
/// Calculates a path for the actor from source to target.
/// Returned path is *reversed* and given target to source.
/// </summary>
public List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check) public List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check)
{ {
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
@@ -74,153 +76,99 @@ namespace OpenRA.Mods.Common.Traits
// If a water-land transition is required, bail early // If a water-land transition is required, bail early
if (domainIndex != null && !domainIndex.IsPassable(source, target, locomotor)) if (domainIndex != null && !domainIndex.IsPassable(source, target, locomotor))
return EmptyPath; return NoPath;
var distance = source - target; var distance = source - target;
var canMoveFreely = locomotor.CanMoveFreelyInto(self, target, check, null); var canMoveFreely = locomotor.CanMoveFreelyInto(self, target, check, null);
if (distance.LengthSquared < 3 && !canMoveFreely) if (distance.LengthSquared < 3 && !canMoveFreely)
return new List<CPos> { }; return NoPath;
if (source.Layer == target.Layer && distance.LengthSquared < 3 && canMoveFreely) if (source.Layer == target.Layer && distance.LengthSquared < 3 && canMoveFreely)
return new List<CPos> { target }; return new List<CPos> { target };
List<CPos> pb; List<CPos> pb;
using (var fromSrc = PathSearch.ToTargetCell(world, locomotor, self, target, source, check, ignoreActor: ignoreActor))
using (var fromSrc = PathSearch.FromPoint(world, locomotor, self, target, source, check).WithIgnoredActor(ignoreActor)) using (var fromDest = PathSearch.ToTargetCell(world, locomotor, self, source, target, check, ignoreActor: ignoreActor, inReverse: true))
using (var fromDest = PathSearch.FromPoint(world, locomotor, self, source, target, check).WithIgnoredActor(ignoreActor).Reverse())
pb = FindBidiPath(fromSrc, fromDest); pb = FindBidiPath(fromSrc, fromDest);
return pb; return pb;
} }
public List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self, BlockedByActor check) /// <summary>
/// Expands the path search until a path is found, and returns that path.
/// Returned path is *reversed* and given target to source.
/// </summary>
public List<CPos> FindPath(PathSearch search)
{ {
if (!cached)
{
domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
cached = true;
}
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
var mobile = (Mobile)self.OccupiesSpace;
var locomotor = mobile.Locomotor;
var targetCell = world.Map.CellContaining(target);
// Correct for SubCell offset
target -= world.Map.Grid.OffsetOfSubCell(srcSub);
var rangeLengthSquared = range.LengthSquared;
var map = world.Map;
// Select only the tiles that are within range from the requested SubCell
// This assumes that the SubCell does not change during the path traversal
var tilesInRange = map.FindTilesInCircle(targetCell, range.Length / 1024 + 1)
.Where(t => (map.CenterOfCell(t) - target).LengthSquared <= rangeLengthSquared
&& mobile.Info.CanEnterCell(world, self, t));
// See if there is any cell within range that does not involve a cross-domain request
// Really, we only need to check the circle perimeter, but it's not clear that would be a performance win
if (domainIndex != null)
{
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(source, t, locomotor)));
if (!tilesInRange.Any())
return EmptyPath;
}
using (var fromSrc = PathSearch.FromPoints(world, locomotor, self, tilesInRange, source, check))
using (var fromDest = PathSearch.FromPoint(world, locomotor, self, source, targetCell, check).Reverse())
return FindBidiPath(fromSrc, fromDest);
}
public List<CPos> FindPath(IPathSearch search)
{
List<CPos> path = null;
while (search.CanExpand) while (search.CanExpand)
{ {
var p = search.Expand(); var p = search.Expand();
if (search.IsTarget(p)) if (search.IsTarget(p))
{ return MakePath(search.Graph, p);
path = MakePath(search.Graph, p);
break;
}
} }
search.Graph.Dispose(); return NoPath;
if (path != null)
return path;
// no path exists
return EmptyPath;
} }
// Searches from both ends toward each other. This is used to prevent blockings in case we find // Build the path from the destination.
// units in the middle of the path that prevent us to continue. // When we find a node that has the same previous position than itself, that node is the source node.
public List<CPos> FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) static List<CPos> MakePath(IPathGraph graph, CPos destination)
{
List<CPos> path = null;
while (fromSrc.CanExpand && fromDest.CanExpand)
{
// make some progress on the first search
var p = fromSrc.Expand();
if (fromDest.Graph[p].Status == CellStatus.Closed &&
fromDest.Graph[p].CostSoFar != PathGraph.PathCostForInvalidPath)
{
path = MakeBidiPath(fromSrc, fromDest, p);
break;
}
// make some progress on the second search
var q = fromDest.Expand();
if (fromSrc.Graph[q].Status == CellStatus.Closed &&
fromSrc.Graph[q].CostSoFar != PathGraph.PathCostForInvalidPath)
{
path = MakeBidiPath(fromSrc, fromDest, q);
break;
}
}
fromSrc.Graph.Dispose();
fromDest.Graph.Dispose();
if (path != null)
return path;
return EmptyPath;
}
// Build the path from the destination. When we find a node that has the same previous
// position than itself, that node is the source node.
static List<CPos> MakePath(IGraph<CellInfo> cellInfo, CPos destination)
{ {
var ret = new List<CPos>(); var ret = new List<CPos>();
var currentNode = destination; var currentNode = destination;
while (cellInfo[currentNode].PreviousNode != currentNode) while (graph[currentNode].PreviousNode != currentNode)
{ {
ret.Add(currentNode); ret.Add(currentNode);
currentNode = cellInfo[currentNode].PreviousNode; currentNode = graph[currentNode].PreviousNode;
} }
ret.Add(currentNode); ret.Add(currentNode);
return ret; return ret;
} }
static List<CPos> MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode) /// <summary>
/// Expands both path searches until they intersect, and returns the path.
/// Returned path is from the source of the first search to the source of the second search.
/// </summary>
public List<CPos> FindBidiPath(PathSearch first, PathSearch second)
{ {
var ca = a.Graph; while (first.CanExpand && second.CanExpand)
var cb = b.Graph; {
// make some progress on the first search
var p = first.Expand();
var pInfo = second.Graph[p];
if (pInfo.Status == CellStatus.Closed &&
pInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
return MakeBidiPath(first, second, p);
// make some progress on the second search
var q = second.Expand();
var qInfo = first.Graph[q];
if (qInfo.Status == CellStatus.Closed &&
qInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
return MakeBidiPath(first, second, q);
}
return NoPath;
}
// Build the path from the destination of each search.
// When we find a node that has the same previous position than itself, that is the source of that search.
static List<CPos> MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode)
{
var ca = first.Graph;
var cb = second.Graph;
var ret = new List<CPos>(); var ret = new List<CPos>();
var q = confluenceNode; var q = confluenceNode;
while (ca[q].PreviousNode != q) var previous = ca[q].PreviousNode;
while (previous != q)
{ {
ret.Add(q); ret.Add(q);
q = ca[q].PreviousNode; q = previous;
previous = ca[q].PreviousNode;
} }
ret.Add(q); ret.Add(q);
@@ -228,9 +176,11 @@ namespace OpenRA.Mods.Common.Traits
ret.Reverse(); ret.Reverse();
q = confluenceNode; q = confluenceNode;
while (cb[q].PreviousNode != q) previous = cb[q].PreviousNode;
while (previous != q)
{ {
q = cb[q].PreviousNode; q = previous;
previous = cb[q].PreviousNode;
ret.Add(q); ret.Add(q);
} }