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:
committed by
Paul Chote
parent
2583a7af31
commit
d2935672ca
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user