Fix the shape of the IPathFinder interface, ensure all path searches use it.

Some path searches, using PathSearch, were created directly at the callsite rather than using the pathfinder trait. This means some searches did not not benefit from the performance checks done in the pathfinder trait. It also means the pathfinder trait was not responsible for all pathing done in the game. Fix this with the following changes:
- Create a sensible shape for the IPathFinder interface and promote it to a trait interface, allowing theoretical replacements of the implementation. Ensure none of the concrete classes in OpenRA.Mods.Common.Pathfinder are exposed in the interface to ensure this is possible.
- Update the PathFinder class to implement the interface, and update several callsites manually running pathfinding code to instead call the IPathFinder interface.
- Overall, this allows any implementation of the IPathFinder interface to intercept and control all path searching performed by the game. Previously some searches would not have used it, and no alternate implementations were possible as the existing implementation was hardcoded into the interface shape.

Additionally:
- Move the responsibility of finding paths on completed path searches from pathfinder to path search, which is a more sensible location.
- Clean up the pathfinder pre-search optimizations.
This commit is contained in:
RoosterDragon
2022-01-30 13:18:56 +00:00
committed by Paul Chote
parent 2583a7af31
commit d2935672ca
10 changed files with 213 additions and 202 deletions

View File

@@ -182,9 +182,9 @@ namespace OpenRA.Mods.Common.Activities
var harvPos = self.CenterPosition;
// Find any harvestable resources:
List<CPos> path;
using (var search = PathSearch.ToTargetCellByPredicate(
self.World, mobile.Locomotor, self, new[] { searchFromLoc, self.Location },
var path = mobile.PathFinder.FindUnitPathToTargetCellByPredicate(
self,
new[] { searchFromLoc, self.Location },
loc =>
harv.CanHarvestCell(loc) &&
claimLayer.CanClaimCell(self, loc),
@@ -218,8 +218,7 @@ namespace OpenRA.Mods.Common.Activities
}
return 0;
}))
path = mobile.Pathfinder.FindPath(search);
});
if (path.Count > 0)
return path[0];

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -59,9 +58,8 @@ namespace OpenRA.Mods.Common.Activities
getPath = check =>
{
using (var search = PathSearch.ToTargetCell(
self.World, mobile.Locomotor, self, mobile.ToCell, destination, check, laneBias: false))
return mobile.Pathfinder.FindPath(search);
return mobile.PathFinder.FindUnitPathToTargetCell(
self, new[] { mobile.ToCell }, destination, check, laneBias: false);
};
this.destination = destination;
@@ -80,7 +78,8 @@ namespace OpenRA.Mods.Common.Activities
if (!this.destination.HasValue)
return PathFinder.NoPath;
return mobile.Pathfinder.FindUnitPath(mobile.ToCell, this.destination.Value, self, ignoreActor, check);
return mobile.PathFinder.FindUnitPathToTargetCell(
self, new[] { mobile.ToCell }, this.destination.Value, check, ignoreActor: ignoreActor);
};
// Note: Will be recalculated from OnFirstRun if evaluateNearestMovableCell is true

View File

@@ -12,7 +12,6 @@
using System.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -128,9 +127,9 @@ namespace OpenRA.Mods.Common.Activities
if (!searchCells.Any())
return PathFinder.NoPath;
using (var fromSrc = PathSearch.ToTargetCell(self.World, Mobile.Locomotor, self, searchCells, loc, check))
using (var fromDest = PathSearch.ToTargetCell(self.World, Mobile.Locomotor, self, loc, lastVisibleTargetLocation, check, inReverse: true))
return Mobile.Pathfinder.FindBidiPath(fromSrc, fromDest);
var path = Mobile.PathFinder.FindUnitPathToTargetCell(self, searchCells, loc, check);
path.Reverse();
return path;
}
public override IEnumerable<Target> GetTargets(Actor self)

View File

@@ -34,14 +34,14 @@ namespace OpenRA.Mods.Common.Pathfinder
readonly BlockedByActor check;
readonly Func<CPos, int> customCost;
readonly Actor ignoreActor;
readonly bool inReverse;
readonly bool laneBias;
readonly bool inReverse;
readonly bool checkTerrainHeight;
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer;
readonly CellLayer<CellInfo>[] cellInfoForLayer;
public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check,
Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, bool laneBias)
Func<CPos, int> customCost, Actor ignoreActor, bool laneBias, bool inReverse)
{
customMovementLayers = world.GetCustomMovementLayers();
customMovementLayersEnabledForLocomotor = customMovementLayers.Count(cml => cml != null && cml.EnabledForLocomotor(locomotor.Info));
@@ -51,8 +51,8 @@ namespace OpenRA.Mods.Common.Pathfinder
this.check = check;
this.customCost = customCost;
this.ignoreActor = ignoreActor;
this.inReverse = inReverse;
this.laneBias = laneBias;
this.inReverse = inReverse;
checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
// As we support a search over the whole map area,

View File

@@ -26,13 +26,6 @@ namespace OpenRA.Mods.Common.Pathfinder
/// </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
// 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>();
@@ -45,9 +38,11 @@ namespace OpenRA.Mods.Common.Pathfinder
public static PathSearch ToTargetCellByPredicate(
World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
Func<CPos, int> customCost = null)
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true)
{
var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, null, false, DefaultLaneBias);
var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false);
var search = new PathSearch(graph, loc => 0, DefaultHeuristicWeightPercentage, targetPredicate);
foreach (var sl in froms)
@@ -57,26 +52,16 @@ namespace OpenRA.Mods.Common.Pathfinder
return search;
}
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)
{
return ToTargetCell(world, locomotor, self, new[] { from }, target, check, customCost, ignoreActor, inReverse, laneBias);
}
public static PathSearch ToTargetCell(
World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, CPos target, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true,
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);
var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse);
heuristic = heuristic ?? DefaultCostEstimator(locomotor, target);
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target);
@@ -168,14 +153,14 @@ namespace OpenRA.Mods.Common.Pathfinder
/// 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;
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
/// </summary>
/// <returns>The most promising node of the iteration</returns>
public CPos Expand()
CPos Expand()
{
var currentMinNode = openQueue.Pop().Destination;
@@ -215,11 +200,96 @@ namespace OpenRA.Mods.Common.Pathfinder
}
/// <summary>
/// Determines if <paramref name="location"/> is the target of the search.
/// Expands the path search until a path is found, and returns that path.
/// Returned path is *reversed* and given target to source.
/// </summary>
public bool IsTarget(CPos location)
public List<CPos> FindPath()
{
return TargetPredicate(location);
while (CanExpand)
{
var p = Expand();
if (TargetPredicate(p))
return MakePath(Graph, p);
}
return PathFinder.NoPath;
}
// 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(IPathGraph graph, CPos destination)
{
var ret = new List<CPos>();
var currentNode = destination;
while (graph[currentNode].PreviousNode != currentNode)
{
ret.Add(currentNode);
currentNode = graph[currentNode].PreviousNode;
}
ret.Add(currentNode);
return ret;
}
/// <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 static List<CPos> FindBidiPath(PathSearch first, PathSearch second)
{
while (first.CanExpand && second.CanExpand)
{
// 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 PathFinder.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 q = confluenceNode;
var previous = ca[q].PreviousNode;
while (previous != q)
{
ret.Add(q);
q = previous;
previous = ca[q].PreviousNode;
}
ret.Add(q);
ret.Reverse();
q = confluenceNode;
previous = cb[q].PreviousNode;
while (previous != q)
{
q = previous;
previous = cb[q].PreviousNode;
ret.Add(q);
}
return ret;
}
public void Dispose()

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
@@ -44,15 +43,14 @@ namespace OpenRA.Mods.Common.Traits
public readonly Actor Actor;
public readonly Harvester Harvester;
public readonly Parachutable Parachutable;
public readonly Locomotor Locomotor;
public readonly Mobile Mobile;
public HarvesterTraitWrapper(Actor actor)
{
Actor = actor;
Harvester = actor.Trait<Harvester>();
Parachutable = actor.TraitOrDefault<Parachutable>();
var mobile = actor.Trait<Mobile>();
Locomotor = mobile.Locomotor;
Mobile = actor.Trait<Mobile>();
}
}
@@ -61,7 +59,6 @@ namespace OpenRA.Mods.Common.Traits
readonly Func<Actor, bool> unitCannotBeOrdered;
readonly Dictionary<Actor, HarvesterTraitWrapper> harvesters = new Dictionary<Actor, HarvesterTraitWrapper>();
IPathFinder pathfinder;
IResourceLayer resourceLayer;
ResourceClaimLayer claimLayer;
IBotRequestUnitProduction[] requestUnitProduction;
@@ -82,7 +79,6 @@ namespace OpenRA.Mods.Common.Traits
protected override void TraitEnabled(Actor self)
{
pathfinder = world.WorldActor.Trait<IPathFinder>();
resourceLayer = world.WorldActor.TraitOrDefault<IResourceLayer>();
claimLayer = world.WorldActor.TraitOrDefault<ResourceClaimLayer>();
@@ -146,14 +142,11 @@ namespace OpenRA.Mods.Common.Traits
harv.Harvester.CanHarvestCell(cell) &&
claimLayer.CanClaimCell(actor, cell);
List<CPos> path;
using (var search =
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)
.Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))))
path = pathfinder.FindPath(search);
var path = harv.Mobile.PathFinder.FindUnitPathToTargetCellByPredicate(
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)
.Sum(u => Math.Max(WDist.Zero.Length, Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length)));
if (path.Count == 0)
return Target.Invalid;

View File

@@ -193,10 +193,8 @@ namespace OpenRA.Mods.Common.Traits
}).ToLookup(r => r.Location);
// Start a search from each refinery's delivery location:
List<CPos> path;
using (var search = PathSearch.ToTargetCell(
self.World, mobile.Locomotor, self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None,
var path = mobile.PathFinder.FindUnitPathToTargetCell(
self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None,
location =>
{
if (!refineries.Contains(location))
@@ -210,8 +208,7 @@ namespace OpenRA.Mods.Common.Traits
// Prefer refineries with less occupancy (multiplier is to offset distance cost):
return occupancy * Info.UnloadQueueCostModifier;
}))
path = mobile.Pathfinder.FindPath(search);
});
if (path.Count > 0)
return refineries[path.Last()].First().Actor;

View File

@@ -235,7 +235,7 @@ namespace OpenRA.Mods.Common.Traits
public Locomotor Locomotor { get; private set; }
public IPathFinder Pathfinder { get; private set; }
public IPathFinder PathFinder { get; private set; }
#region IOccupySpace
@@ -302,7 +302,7 @@ namespace OpenRA.Mods.Common.Traits
notifyMoving = self.TraitsImplementing<INotifyMoving>().ToArray();
notifyFinishedMoving = self.TraitsImplementing<INotifyFinishedMoving>().ToArray();
moveWrappers = self.TraitsImplementing<IWrapMove>().ToArray();
Pathfinder = self.World.WorldActor.Trait<IPathFinder>();
PathFinder = self.World.WorldActor.Trait<IPathFinder>();
Locomotor = self.World.WorldActor.TraitsImplementing<Locomotor>()
.Single(l => l.Info.Name == Info.Locomotor);
@@ -825,10 +825,8 @@ namespace OpenRA.Mods.Common.Traits
if (CanEnterCell(above))
return above;
List<CPos> path;
using (var search = PathSearch.ToTargetCellByPredicate(
self.World, Locomotor, self, new[] { self.Location }, loc => loc.Layer == 0 && CanEnterCell(loc), BlockedByActor.All))
path = Pathfinder.FindPath(search);
var path = PathFinder.FindUnitPathToTargetCellByPredicate(
self, new[] { self.Location }, loc => loc.Layer == 0 && CanEnterCell(loc), BlockedByActor.All);
if (path.Count > 0)
return path[0];

View File

@@ -9,14 +9,16 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World)]
[Desc("Calculates routes for mobile units based on the A* search algorithm.", " Attach this to the world actor.")]
[Desc("Calculates routes for mobile units with locomotors based on the A* search algorithm.", " Attach this to the world actor.")]
public class PathFinderInfo : TraitInfo, Requires<LocomotorInfo>
{
public override object Create(ActorInitializer init)
@@ -25,34 +27,11 @@ namespace OpenRA.Mods.Common.Traits
}
}
public interface IPathFinder
{
/// <summary>
/// Calculates a path for the actor from source to target.
/// Returned path is *reversed* and given target to source.
/// </summary>
List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, 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>
/// 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>
List<CPos> FindBidiPath(PathSearch fromSrc, PathSearch fromDest);
}
public class PathFinder : IPathFinder
{
public static readonly List<CPos> NoPath = new List<CPos>(0);
readonly World world;
DomainIndex domainIndex;
bool cached;
public PathFinder(World world)
{
@@ -60,131 +39,83 @@ namespace OpenRA.Mods.Common.Traits
}
/// <summary>
/// Calculates a path for the actor from source to target.
/// Calculates a path for the actor from multiple possible sources to target.
/// Returned path is *reversed* and given target to source.
/// The shortest path between a source and the target is returned.
/// </summary>
public List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check)
/// <remarks>
/// Searches that provide a multiple source cells are slower than those than provide only a single source cell,
/// as optimizations are possible for the single source case. Use searches from multiple source cells
/// sparingly.
/// </remarks>
public List<CPos> FindUnitPathToTargetCell(
Actor self, IEnumerable<CPos> sources, CPos target, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true)
{
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
var locomotor = ((Mobile)self.OccupiesSpace).Locomotor;
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
throw new ArgumentException($"{nameof(sources)} must not be empty.", nameof(sources));
if (!cached)
var locomotor = GetLocomotor(self);
// If the target cell is inaccessible, bail early.
var inaccessible =
!locomotor.CanMoveFreelyInto(self, target, check, ignoreActor) ||
(!(customCost is null) && customCost(target) == PathGraph.PathCostForInvalidPath);
if (inaccessible)
return NoPath;
// When searching from only one source cell, some optimizations are possible.
if (sourcesList.Count == 1)
{
domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
cached = true;
var source = sourcesList[0];
// For adjacent cells on the same layer, we can return the path without invoking a full search.
if (source.Layer == target.Layer && (source - target).LengthSquared < 3)
return new List<CPos>(2) { target, source };
// With one starting point, we can use a bidirectional search.
using (var fromTarget = PathSearch.ToTargetCell(
world, locomotor, self, new[] { target }, source, check, ignoreActor: ignoreActor))
using (var fromSource = PathSearch.ToTargetCell(
world, locomotor, self, new[] { source }, target, check, ignoreActor: ignoreActor, inReverse: true))
return PathSearch.FindBidiPath(fromTarget, fromSource);
}
// If a water-land transition is required, bail early
if (domainIndex != null && !domainIndex.IsPassable(source, target, locomotor))
return NoPath;
var distance = source - target;
var canMoveFreely = locomotor.CanMoveFreelyInto(self, target, check, null);
if (distance.LengthSquared < 3 && !canMoveFreely)
return NoPath;
if (source.Layer == target.Layer && distance.LengthSquared < 3 && canMoveFreely)
return new List<CPos> { target };
List<CPos> pb;
using (var fromSrc = PathSearch.ToTargetCell(world, locomotor, self, target, source, check, ignoreActor: ignoreActor))
using (var fromDest = PathSearch.ToTargetCell(world, locomotor, self, source, target, check, ignoreActor: ignoreActor, inReverse: true))
pb = FindBidiPath(fromSrc, fromDest);
return pb;
// With multiple starting points, we can only use a unidirectional search.
using (var search = PathSearch.ToTargetCell(
world, locomotor, self, sourcesList, target, check, customCost, ignoreActor, laneBias))
return search.FindPath();
}
/// <summary>
/// Expands the path search until a path is found, and returns that path.
/// Calculates a path for the actor from multiple possible sources, whilst searching for an acceptable target.
/// Returned path is *reversed* and given target to source.
/// The shortest path between a source and a discovered target is returned.
/// </summary>
public List<CPos> FindPath(PathSearch search)
/// <remarks>
/// Searches with this method are slower than <see cref="FindUnitPathToTargetCell"/> due to the need to search for
/// and discover an acceptable target cell. Use this search sparingly.
/// </remarks>
public List<CPos> FindUnitPathToTargetCellByPredicate(
Actor self, IEnumerable<CPos> sources, Func<CPos, bool> targetPredicate, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true)
{
while (search.CanExpand)
{
var p = search.Expand();
if (search.IsTarget(p))
return MakePath(search.Graph, p);
}
return NoPath;
// With no pre-specified target location, we can only use a unidirectional search.
using (var search = PathSearch.ToTargetCellByPredicate(
world, GetLocomotor(self), self, sources, targetPredicate, check, customCost, ignoreActor, laneBias))
return search.FindPath();
}
// 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(IPathGraph graph, CPos destination)
static Locomotor GetLocomotor(Actor self)
{
var ret = new List<CPos>();
var currentNode = destination;
while (graph[currentNode].PreviousNode != currentNode)
{
ret.Add(currentNode);
currentNode = graph[currentNode].PreviousNode;
}
ret.Add(currentNode);
return ret;
}
/// <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)
{
while (first.CanExpand && second.CanExpand)
{
// 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 q = confluenceNode;
var previous = ca[q].PreviousNode;
while (previous != q)
{
ret.Add(q);
q = previous;
previous = ca[q].PreviousNode;
}
ret.Add(q);
ret.Reverse();
q = confluenceNode;
previous = cb[q].PreviousNode;
while (previous != q)
{
q = previous;
previous = cb[q].PreviousNode;
ret.Add(q);
}
return ret;
// PERF: This PathFinder trait requires the use of Mobile, so we can be sure that is in use.
// We can save some performance by avoiding querying for the Locomotor trait and retrieving it from Mobile.
return ((Mobile)self.OccupiesSpace).Locomotor;
}
}
}

View File

@@ -791,4 +791,29 @@ namespace OpenRA.Mods.Common.Traits
void SetPosition(Actor self, WPos pos);
void SetCenterPosition(Actor self, WPos pos);
}
public interface IPathFinder
{
/// <summary>
/// Calculates a path for the actor from multiple possible sources to target.
/// Returned path is *reversed* and given target to source.
/// The shortest path between a source and the target is returned.
/// </summary>
List<CPos> FindUnitPathToTargetCell(
Actor self, IEnumerable<CPos> sources, CPos target, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true);
/// <summary>
/// Calculates a path for the actor from multiple possible sources, whilst searching for an acceptable target.
/// Returned path is *reversed* and given target to source.
/// The shortest path between a source and a discovered target is returned.
/// </summary>
List<CPos> FindUnitPathToTargetCellByPredicate(
Actor self, IEnumerable<CPos> sources, Func<CPos, bool> targetPredicate, BlockedByActor check,
Func<CPos, int> customCost = null,
Actor ignoreActor = null,
bool laneBias = true);
}
}