From d2935672cab0666d69802eadf1a0d64a27c2f24c Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sun, 30 Jan 2022 13:18:56 +0000 Subject: [PATCH] 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. --- .../Activities/FindAndDeliverResources.cs | 9 +- OpenRA.Mods.Common/Activities/Move/Move.cs | 9 +- .../Activities/Move/MoveAdjacentTo.cs | 7 +- OpenRA.Mods.Common/Pathfinder/PathGraph.cs | 6 +- OpenRA.Mods.Common/Pathfinder/PathSearch.cs | 122 ++++++++--- .../Traits/BotModules/HarvesterBotModule.cs | 21 +- OpenRA.Mods.Common/Traits/Harvester.cs | 9 +- OpenRA.Mods.Common/Traits/Mobile.cs | 10 +- OpenRA.Mods.Common/Traits/World/PathFinder.cs | 197 ++++++------------ OpenRA.Mods.Common/TraitsInterfaces.cs | 25 +++ 10 files changed, 213 insertions(+), 202 deletions(-) diff --git a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs index ca07ddace3..e5ed100ebd 100644 --- a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs +++ b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs @@ -182,9 +182,9 @@ namespace OpenRA.Mods.Common.Activities var harvPos = self.CenterPosition; // Find any harvestable resources: - List 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]; diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index d84574b58f..3fb84e10d0 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -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 diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 450ce9719b..e4ebe98b2a 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -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 GetTargets(Actor self) diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index 9b6fda28c9..c6f4aa883e 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -34,14 +34,14 @@ namespace OpenRA.Mods.Common.Pathfinder readonly BlockedByActor check; readonly Func customCost; readonly Actor ignoreActor; - readonly bool inReverse; readonly bool laneBias; + readonly bool inReverse; readonly bool checkTerrainHeight; readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer; readonly CellLayer[] cellInfoForLayer; public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check, - Func customCost, Actor ignoreActor, bool inReverse, bool laneBias) + Func 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, diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index fb014cf6a3..95e768e433 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -26,13 +26,6 @@ namespace OpenRA.Mods.Common.Pathfinder /// public const int DefaultHeuristicWeightPercentage = 125; - /// - /// 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. - /// - 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 LayerPoolTable = new ConditionalWeakTable(); @@ -45,9 +38,11 @@ namespace OpenRA.Mods.Common.Pathfinder public static PathSearch ToTargetCellByPredicate( World world, Locomotor locomotor, Actor self, IEnumerable froms, Func targetPredicate, BlockedByActor check, - Func customCost = null) + Func 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 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 froms, CPos target, BlockedByActor check, Func customCost = null, Actor ignoreActor = null, + bool laneBias = true, bool inReverse = false, - bool laneBias = DefaultLaneBias, Func 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, can no longer be called. /// - public bool CanExpand => !openQueue.Empty; + bool CanExpand => !openQueue.Empty; /// /// This function analyzes the neighbors of the most promising node in the pathfinding graph /// using the A* algorithm (A-star) and returns that node /// /// The most promising node of the iteration - public CPos Expand() + CPos Expand() { var currentMinNode = openQueue.Pop().Destination; @@ -215,11 +200,96 @@ namespace OpenRA.Mods.Common.Pathfinder } /// - /// Determines if 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. /// - public bool IsTarget(CPos location) + public List 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 MakePath(IPathGraph graph, CPos destination) + { + var ret = new List(); + var currentNode = destination; + + while (graph[currentNode].PreviousNode != currentNode) + { + ret.Add(currentNode); + currentNode = graph[currentNode].PreviousNode; + } + + ret.Add(currentNode); + return ret; + } + + /// + /// 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. + /// + public static List 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 MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode) + { + var ca = first.Graph; + var cb = second.Graph; + + var ret = new List(); + + 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() diff --git a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs index d4f5ac59f2..f5bd8bc9db 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs @@ -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(); Parachutable = actor.TraitOrDefault(); - var mobile = actor.Trait(); - Locomotor = mobile.Locomotor; + Mobile = actor.Trait(); } } @@ -61,7 +59,6 @@ namespace OpenRA.Mods.Common.Traits readonly Func unitCannotBeOrdered; readonly Dictionary harvesters = new Dictionary(); - 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(); resourceLayer = world.WorldActor.TraitOrDefault(); claimLayer = world.WorldActor.TraitOrDefault(); @@ -146,14 +142,11 @@ namespace OpenRA.Mods.Common.Traits harv.Harvester.CanHarvestCell(cell) && claimLayer.CanClaimCell(actor, cell); - List 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; diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index f395e2d310..d76eabe86e 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -193,10 +193,8 @@ namespace OpenRA.Mods.Common.Traits }).ToLookup(r => r.Location); // Start a search from each refinery's delivery location: - List 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; diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index a2c950e56e..ce0590e0f6 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -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().ToArray(); notifyFinishedMoving = self.TraitsImplementing().ToArray(); moveWrappers = self.TraitsImplementing().ToArray(); - Pathfinder = self.World.WorldActor.Trait(); + PathFinder = self.World.WorldActor.Trait(); Locomotor = self.World.WorldActor.TraitsImplementing() .Single(l => l.Info.Name == Info.Locomotor); @@ -825,10 +825,8 @@ namespace OpenRA.Mods.Common.Traits if (CanEnterCell(above)) return above; - List 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]; diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index 54e059614f..435d070ba3 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -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 { public override object Create(ActorInitializer init) @@ -25,34 +27,11 @@ namespace OpenRA.Mods.Common.Traits } } - public interface IPathFinder - { - /// - /// Calculates a path for the actor from source to target. - /// Returned path is *reversed* and given target to source. - /// - List FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check); - - /// - /// Expands the path search until a path is found, and returns that path. - /// Returned path is *reversed* and given target to source. - /// - List FindPath(PathSearch search); - - /// - /// 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. - /// - List FindBidiPath(PathSearch fromSrc, PathSearch fromDest); - } - public class PathFinder : IPathFinder { public static readonly List NoPath = new List(0); readonly World world; - DomainIndex domainIndex; - bool cached; public PathFinder(World world) { @@ -60,131 +39,83 @@ namespace OpenRA.Mods.Common.Traits } /// - /// 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. /// - public List FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check) + /// + /// 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. + /// + public List FindUnitPathToTargetCell( + Actor self, IEnumerable sources, CPos target, BlockedByActor check, + Func 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(); - 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(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 { target }; - - List 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(); } /// - /// 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. /// - public List FindPath(PathSearch search) + /// + /// Searches with this method are slower than due to the need to search for + /// and discover an acceptable target cell. Use this search sparingly. + /// + public List FindUnitPathToTargetCellByPredicate( + Actor self, IEnumerable sources, Func targetPredicate, BlockedByActor check, + Func 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 MakePath(IPathGraph graph, CPos destination) + static Locomotor GetLocomotor(Actor self) { - var ret = new List(); - var currentNode = destination; - - while (graph[currentNode].PreviousNode != currentNode) - { - ret.Add(currentNode); - currentNode = graph[currentNode].PreviousNode; - } - - ret.Add(currentNode); - return ret; - } - - /// - /// 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. - /// - public List 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 MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode) - { - var ca = first.Graph; - var cb = second.Graph; - - var ret = new List(); - - 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; } } } diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index c5610e60c7..8dc6965c7d 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -791,4 +791,29 @@ namespace OpenRA.Mods.Common.Traits void SetPosition(Actor self, WPos pos); void SetCenterPosition(Actor self, WPos pos); } + + public interface IPathFinder + { + /// + /// 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. + /// + List FindUnitPathToTargetCell( + Actor self, IEnumerable sources, CPos target, BlockedByActor check, + Func customCost = null, + Actor ignoreActor = null, + bool laneBias = true); + + /// + /// 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. + /// + List FindUnitPathToTargetCellByPredicate( + Actor self, IEnumerable sources, Func targetPredicate, BlockedByActor check, + Func customCost = null, + Actor ignoreActor = null, + bool laneBias = true); + } }