diff --git a/OpenRA.Game/CPos.cs b/OpenRA.Game/CPos.cs index 9ef4afaa98da..ee2d0fd80679 100644 --- a/OpenRA.Game/CPos.cs +++ b/OpenRA.Game/CPos.cs @@ -85,6 +85,11 @@ public MPos ToMPos(MapGridType gridType) return new MPos(u, v); } + public CPos AtLayer(byte layer) + { + return new CPos((Bits & 0xff) | layer); + } + #region Scripting interface public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) diff --git a/OpenRA.Game/CacheStorage.cs b/OpenRA.Game/CacheStorage.cs index 6477217623cc..2f35d1371691 100644 --- a/OpenRA.Game/CacheStorage.cs +++ b/OpenRA.Game/CacheStorage.cs @@ -11,10 +11,10 @@ namespace OpenRA { - public interface ICacheStorage + public interface ICacheStorage { - void Remove(string key); - void Store(string key, T data); - T Retrieve(string key); + void Remove(in K key); + void Store(in K key, V data); + V Retrieve(in K key); } } diff --git a/OpenRA.Game/Map/CellLayer.cs b/OpenRA.Game/Map/CellLayer.cs index ed5dae94e926..5adedf7adbd7 100644 --- a/OpenRA.Game/Map/CellLayer.cs +++ b/OpenRA.Game/Map/CellLayer.cs @@ -49,10 +49,17 @@ public static CellLayer CreateInstance(Func initialCellValueFactory, return cellLayer; } - // Resolve an array index from cell coordinates - int Index(CPos cell) + int Index(CPos pos) { - return Index(cell.ToMPos(GridType)); + // PERF: Inline CPos.ToMPos to avoid MPos allocation + var x = pos.X; + var y = pos.Y; + if (GridType == MapGridType.Rectangular) + return y * Size.Width + x; + + var u = (x - y) / 2; + var v = x + y; + return v * Size.Width + u; } // Resolve an array index from map coordinates diff --git a/OpenRA.Game/Primitives/PriorityQueue.cs b/OpenRA.Game/Primitives/PriorityQueue.cs index 79fd09c5c754..a2484a54f7a1 100644 --- a/OpenRA.Game/Primitives/PriorityQueue.cs +++ b/OpenRA.Game/Primitives/PriorityQueue.cs @@ -20,6 +20,8 @@ public interface IPriorityQueue bool Empty { get; } T Peek(); T Pop(); + bool TryPeek(out T elem); + bool TryPop(out T elem); } public class PriorityQueue : IPriorityQueue @@ -41,10 +43,11 @@ public void Add(T item) { var addLevel = level; var addIndex = index; + T above; - while (addLevel >= 1 && comparer.Compare(Above(addLevel, addIndex), item) > 0) + while (addLevel >= 1 && comparer.Compare((above = Above(addLevel, addIndex)), item) > 0) { - items[addLevel][addIndex] = Above(addLevel, addIndex); + items[addLevel][addIndex] = above; --addLevel; addIndex >>= 1; } @@ -91,6 +94,33 @@ public T Pop() return ret; } + public bool TryPeek(out T item) + { + if (level == 0) + { + item = default(T); + return false; + } + + item = At(0, 0); + return true; + } + + public bool TryPop(out T item) + { + if (level == 0) + { + item = default(T); + return false; + } + + item = At(0, 0); + BubbleInto(0, 0, Last()); + if (--index < 0) + index = (1 << --level) - 1; + return true; + } + void BubbleInto(int intoLevel, int intoIndex, T val) { var downLevel = intoLevel + 1; diff --git a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs index 02c231e9a909..f214f71e4605 100644 --- a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs +++ b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs @@ -180,42 +180,71 @@ public override bool Tick(Actor self) var searchRadiusSquared = searchRadius * searchRadius; - var procPos = procLoc.HasValue ? (WPos?)self.World.Map.CenterOfCell(procLoc.Value) : null; + var map = self.World.Map; + var procPos = procLoc.HasValue ? (WPos?)map.CenterOfCell(procLoc.Value) : null; var harvPos = self.CenterPosition; // Find any harvestable resources: List path; - using (var search = PathSearch.Search(self.World, mobile.Locomotor, self, BlockedByActor.Stationary, loc => - domainIndex.IsPassable(self.Location, loc, mobile.Locomotor) && harv.CanHarvestCell(self, loc) && claimLayer.CanClaimCell(self, loc)) - .WithCustomCost(loc => + Func customCost; + + if (procPos.HasValue && harvInfo.ResourceRefineryDirectionPenalty > 0) + { + customCost = loc => { if ((loc - searchFromLoc).LengthSquared > searchRadiusSquared) - return int.MaxValue; + return PathGraph.CostForInvalidCell; - // Add a cost modifier to harvestable cells to prefer resources that are closer to the refinery. + // Add a cost modifier to harvestable cells to prefer resources + // that are closer to the refinery. // This reduces the tendancy for harvesters to move in straight lines - if (procPos.HasValue && harvInfo.ResourceRefineryDirectionPenalty > 0 && harv.CanHarvestCell(self, loc)) + if (harv.CanHarvestCell(self, loc)) { - var pos = self.World.Map.CenterOfCell(loc); + var pos = map.CenterOfCell(loc); // Calculate harv-cell-refinery angle (cosine rule) - var a = harvPos - procPos.Value; var b = pos - procPos.Value; - var c = pos - harvPos; - - if (b != WVec.Zero && c != WVec.Zero) + if (b != WVec.Zero) { - var cosA = (int)(512 * (b.LengthSquared + c.LengthSquared - a.LengthSquared) / b.Length / c.Length); - - // Cost modifier varies between 0 and ResourceRefineryDirectionPenalty - return Math.Abs(harvInfo.ResourceRefineryDirectionPenalty / 2) + harvInfo.ResourceRefineryDirectionPenalty * cosA / 2048; + var c = pos - harvPos; + if (c != WVec.Zero) + { + var a = harvPos - procPos.Value; + var cosA = (int)(512 * (b.LengthSquared + c.LengthSquared - a.LengthSquared) / b.Length / c.Length); + + // Cost modifier varies between 0 and ResourceRefineryDirectionPenalty + return Math.Abs(harvInfo.ResourceRefineryDirectionPenalty / 2) + harvInfo.ResourceRefineryDirectionPenalty * cosA / 2048; + } } } return 0; - }) - .FromPoint(searchFromLoc) - .FromPoint(self.Location)) + }; + } + else + { + customCost = loc => + { + if ((loc - searchFromLoc).LengthSquared > searchRadiusSquared) + return PathGraph.CostForInvalidCell; + return 0; + }; + } + + var query = new PathQuery( + queryType: PathQueryType.ConditionUnidirectional, + world: self.World, + locomotor: mobile.Locomotor, + actor: self, + check: BlockedByActor.Stationary, + isGoal: loc => domainIndex.IsPassable(self.Location, loc, mobile.Locomotor) + && harv.CanHarvestCell(self, loc) + && claimLayer.CanClaimCell(self, loc), + customCost: customCost, + fromPositions: self.Location.Equals(searchFromLoc) ? + new CPos[] { self.Location } : new CPos[] { searchFromLoc, self.Location }); + + using (var search = new PathSearch(query)) path = mobile.Pathfinder.FindPath(search); if (path.Count > 0) diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index 54a8dee11cf1..445fa8c9c07f 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -58,9 +58,17 @@ public Move(Actor self, CPos destination, Color? targetLineColor = null) getPath = check => { List path; - using (var search = - PathSearch.FromPoint(self.World, mobile.Locomotor, self, mobile.ToCell, destination, check) - .WithoutLaneBias()) + var query = new PathQuery( + queryType: PathQueryType.PositionUnidirectional, + world: self.World, + locomotor: mobile.Locomotor, + actor: self, + laneBiasDisabled: true, + fromPosition: mobile.ToCell, + toPosition: destination, + check: check); + + using (var search = new PathSearch(query)) path = mobile.Pathfinder.FindPath(search); return path; }; diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 89ac58e30825..9955ec0a0cd0 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -132,8 +132,27 @@ List CalculatePathToTarget(Actor self, BlockedByActor check) if (!searchCells.Any()) return NoPath; - using (var fromSrc = PathSearch.FromPoints(self.World, Mobile.Locomotor, self, searchCells, loc, check)) - using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Locomotor, self, loc, lastVisibleTargetLocation, check).Reverse()) + var fromSrcQuery = new PathQuery( + queryType: PathQueryType.PositionBidirectional, + world: self.World, + locomotor: Mobile.Locomotor, + actor: self, + fromPositions: searchCells, + toPosition: loc, + check: check); + + var fromDestQuery = new PathQuery( + queryType: PathQueryType.PositionBidirectional, + world: self.World, + locomotor: Mobile.Locomotor, + actor: self, + fromPosition: loc, + toPosition: lastVisibleTargetLocation, + check: check, + reverse: true); + + using (var fromSrc = new PathSearch(fromSrcQuery)) + using (var fromDest = new PathSearch(fromDestQuery)) return Mobile.Pathfinder.FindBidiPath(fromSrc, fromDest); } diff --git a/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs index c5e8fbbbe2c3..a6db504d8e52 100644 --- a/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs @@ -14,90 +14,194 @@ using System.Linq; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Pathfinder { - public interface IPathSearch : IDisposable + public enum PathQueryType { - /// - /// The Graph used by the A* - /// - IGraph Graph { get; } - - /// - /// Stores the analyzed nodes by the expand function - /// - IEnumerable<(CPos Cell, int Cost)> Considered { get; } - - Player Owner { get; } - - int MaxCost { get; } - - IPathSearch Reverse(); + // Destination unknown. Search until `IsGoal` returns true. + ConditionUnidirectional, - IPathSearch WithCustomBlocker(Func customBlock); + // Destination known. Search from 'from' to 'to'. + PositionUnidirectional, - IPathSearch WithIgnoredActor(Actor b); - - IPathSearch WithHeuristic(Func h); - - IPathSearch WithHeuristicWeight(int percentage); - - IPathSearch WithCustomCost(Func w); - - IPathSearch WithoutLaneBias(); + // Destination known. Search from both 'from' and 'to', meet in middle. + PositionBidirectional + } - IPathSearch FromPoint(CPos from); + public class PathQuery + { + public readonly PathQueryType QueryType; + public readonly World World; + public readonly Locomotor Locomotor; + public readonly Actor Actor; + public readonly BlockedByActor Check; + public readonly IEnumerable FromPositions; + + // To be set for Position searches. + public readonly CPos? ToPosition; + + public readonly Func CustomBlock; + public readonly Actor IgnoreActor; + public readonly Func CustomCost; + public readonly Func IsGoal; + public readonly bool LaneBiasDisabled; + + // The other end of a bidirectional search. + public readonly bool Reverse; + + public PathQuery(PathQueryType queryType, + World world, + Locomotor locomotor, + Actor actor, + BlockedByActor check, + IEnumerable fromPositions, + CPos? toPosition = null, + Func customBlock = null, + Actor ignoreActor = null, + Func customCost = null, + Func isGoal = null, + bool laneBiasDisabled = false, + bool reverse = false) + { + QueryType = queryType; + World = world; + Locomotor = locomotor; + Actor = actor; + Check = check; + FromPositions = fromPositions; + ToPosition = toPosition; + CustomBlock = customBlock; + IgnoreActor = ignoreActor; + CustomCost = customCost; + IsGoal = isGoal; + LaneBiasDisabled = laneBiasDisabled; + Reverse = reverse; + } - /// - /// 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). - /// - /// The location to assess - /// Whether the location is a target - bool IsTarget(CPos location); + public PathQuery(PathQueryType queryType, + World world, + Locomotor locomotor, + Actor actor, + BlockedByActor check, + CPos fromPosition, + CPos? toPosition = null, + Func customBlock = null, + Actor ignoreActor = null, + Func customCost = null, + Func isGoal = null, + bool laneBiasDisabled = false, + bool reverse = false) + { + QueryType = queryType; + World = world; + Locomotor = locomotor; + Actor = actor; + Check = check; + FromPositions = new[] { fromPosition }; + ToPosition = toPosition; + CustomBlock = customBlock; + IgnoreActor = ignoreActor; + CustomCost = customCost; + IsGoal = isGoal; + LaneBiasDisabled = laneBiasDisabled; + Reverse = reverse; + } - bool CanExpand { get; } - CPos Expand(); + public PathQuery CreateReverse() + { + if (QueryType != PathQueryType.PositionBidirectional) + throw new ArgumentException("Only bidirectional queries use a reverse"); + + if (!ToPosition.HasValue) + throw new ArgumentException("ToPosition not set"); + + if (FromPositions.Count() > 1) + throw new ArgumentException("Reverse requires a single FromPosition"); + + return new PathQuery( + QueryType, + World, + Locomotor, + Actor, + Check, + new[] { ToPosition.Value }, + FromPositions.First(), + CustomBlock, + IgnoreActor, + CustomCost, + IsGoal, + LaneBiasDisabled, + !Reverse); + } } - public abstract class BasePathSearch : IPathSearch + public abstract class BasePathSearch : IDisposable { - public IGraph Graph { get; set; } + public readonly PathGraph Graph; + public readonly PathQuery Query; + public readonly Func IsGoal; + public readonly bool Debug; - protected IPriorityQueue OpenQueue { get; private set; } + // Stores maximum cost in debug mode. Zero otherwise. + public int MaxCost { get; protected set; } + // Stores considered cells in debug mode. Empty otherwise. public abstract IEnumerable<(CPos Cell, int Cost)> Considered { get; } - public Player Owner => Graph.Actor.Owner; - public int MaxCost { get; protected set; } - public bool Debug { get; set; } - protected Func heuristic; - protected Func 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 StartPoints; + protected IPriorityQueue OpenQueue { get; private set; } - private readonly int cellCost, diagonalCellCost; + protected readonly Func heuristic; + protected readonly int heuristicWeightPercentage; + readonly int cellCost, diagonalCellCost; - protected BasePathSearch(IGraph graph) + protected BasePathSearch(PathGraph graph, PathQuery query, bool debug) { + Debug = debug; Graph = graph; + Query = query; OpenQueue = new PriorityQueue(GraphConnection.ConnectionCostComparer); - StartPoints = new PriorityQueue(GraphConnection.ConnectionCostComparer); MaxCost = 0; - heuristicWeightPercentage = 100; // Determine the minimum possible cost for moving horizontally between cells based on terrain speeds. // The minimum possible cost diagonally is then Sqrt(2) times more costly. - cellCost = ((Mobile)graph.Actor.OccupiesSpace).Info.LocomotorInfo.TerrainSpeeds.Values.Min(ti => ti.Cost); + cellCost = ((Mobile)Query.Actor.OccupiesSpace).Info.LocomotorInfo.TerrainSpeeds.Values.Min(ti => ti.Cost); diagonalCellCost = cellCost * 141421 / 100000; + + if (Query.QueryType == PathQueryType.ConditionUnidirectional) + { + if (Query.IsGoal == null) + throw new ArgumentException("IsGoal not set in path query"); + + IsGoal = Query.IsGoal; + heuristicWeightPercentage = 100; + heuristic = loc => 0; + } + else + { + if (Query.FromPositions == null) + throw new ArgumentException("FromPositions not set in path query"); + + if (!Query.ToPosition.HasValue) + throw new ArgumentException("ToPosition set in path query"); + + IsGoal = loc => + { + var locInfo = graph[loc]; + return locInfo.EstimatedTotal - locInfo.CostSoFar == 0; + }; + + // 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. + heuristicWeightPercentage = 125; + heuristic = DefaultEstimator(query.ToPosition.Value); + } } /// @@ -105,7 +209,7 @@ protected BasePathSearch(IGraph graph) /// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html /// /// A delegate that calculates the estimation for a node - protected Func DefaultEstimator(CPos destination) + Func DefaultEstimator(CPos destination) { return here => { @@ -119,65 +223,8 @@ protected BasePathSearch(IGraph graph) }; } - public IPathSearch Reverse() - { - Graph.InReverse = true; - return this; - } - - public IPathSearch WithCustomBlocker(Func customBlock) - { - Graph.CustomBlock = customBlock; - return this; - } - - public IPathSearch WithIgnoredActor(Actor b) - { - Graph.IgnoreActor = b; - return this; - } - - public IPathSearch WithHeuristic(Func h) - { - heuristic = h; - return this; - } - - public IPathSearch WithHeuristicWeight(int percentage) - { - heuristicWeightPercentage = percentage; - return this; - } - - public IPathSearch WithCustomCost(Func 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(); + public abstract bool TryExpand(out CPos mostPromisingNode); protected virtual void Dispose(bool disposing) { diff --git a/OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs b/OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs index f56570b4967b..8c45cd864898 100644 --- a/OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs +++ b/OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs @@ -15,7 +15,7 @@ namespace OpenRA.Mods.Common.Pathfinder { - sealed class CellInfoLayerPool + public sealed class CellInfoLayerPool { const int MaxPoolSize = 4; readonly Stack> pool = new Stack>(MaxPoolSize); diff --git a/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs b/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs index 59d745aaf82e..f31f465e46c9 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs @@ -9,12 +9,67 @@ */ #endregion +using System; using System.Collections.Generic; using System.Linq; namespace OpenRA.Mods.Common.Pathfinder { - public class PathCacheStorage : ICacheStorage> + public enum PathCacheQueryType : byte + { + UnitPath, + UnitPathToRange + } + + public readonly struct PathCacheKey + { + readonly PathCacheQueryType queryType; + readonly uint actorID; + readonly int source; + readonly int target; + readonly int targetY; + readonly int hash; + + public PathCacheKey(PathCacheQueryType queryType, uint actorID, CPos source, CPos target) + { + this.queryType = queryType; + this.actorID = actorID; + this.source = source.Bits; + this.target = target.Bits; + targetY = -1; + hash = HashCode.Combine( + queryType, actorID, this.source, this.target); + } + + public PathCacheKey(PathCacheQueryType queryType, uint actorID, CPos source, WPos target) + { + this.queryType = queryType; + this.actorID = actorID; + this.source = source.Bits; + this.target = target.X; + targetY = target.Y; + hash = HashCode.Combine( + queryType, actorID, this.source, this.target, targetY); + } + + public static bool operator ==(PathCacheKey me, PathCacheKey other) + { + return me.hash == other.hash + && me.actorID == other.actorID + && me.source == other.source + && me.target == other.target + && me.targetY == other.targetY + && me.queryType == other.queryType; + } + + public static bool operator !=(PathCacheKey me, PathCacheKey other) { return !(me == other); } + public override int GetHashCode() { return hash; } + + public bool Equals(PathCacheKey other) { return this == other; } + public override bool Equals(object obj) { return obj is PathCacheKey && Equals((PathCacheKey)obj); } + } + + public class PathCacheStorage : ICacheStorage> { class CachedPath { @@ -24,19 +79,19 @@ class CachedPath const int MaxPathAge = 50; readonly World world; - Dictionary cachedPaths = new Dictionary(100); + readonly Dictionary cachedPaths = new Dictionary(100); public PathCacheStorage(World world) { this.world = world; } - public void Remove(string key) + public void Remove(in PathCacheKey key) { cachedPaths.Remove(key); } - public void Store(string key, List data) + public void Store(in PathCacheKey key, List data) { // Eventually clean up the cachedPaths dictionary if (cachedPaths.Count >= 100) @@ -50,7 +105,7 @@ public void Store(string key, List data) }); } - public List Retrieve(string key) + public List Retrieve(in PathCacheKey key) { if (cachedPaths.TryGetValue(key, out var cached)) { diff --git a/OpenRA.Mods.Common/Pathfinder/PathFinderUnitPathCacheDecorator.cs b/OpenRA.Mods.Common/Pathfinder/PathFinderUnitPathCacheDecorator.cs index 49771f3cdcbe..110d59976d41 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathFinderUnitPathCacheDecorator.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathFinderUnitPathCacheDecorator.cs @@ -22,9 +22,9 @@ namespace OpenRA.Mods.Common.Pathfinder public class PathFinderUnitPathCacheDecorator : IPathFinder { readonly IPathFinder pathFinder; - readonly ICacheStorage> cacheStorage; + readonly ICacheStorage> cacheStorage; - public PathFinderUnitPathCacheDecorator(IPathFinder pathFinder, ICacheStorage> cacheStorage) + public PathFinderUnitPathCacheDecorator(IPathFinder pathFinder, ICacheStorage> cacheStorage) { this.pathFinder = pathFinder; this.cacheStorage = cacheStorage; @@ -34,7 +34,7 @@ public List FindUnitPath(CPos source, CPos target, Actor self, Actor ignor { using (new PerfSample("Pathfinder")) { - var key = "FindUnitPath" + self.ActorID + source.X + source.Y + target.X + target.Y; + var key = new PathCacheKey(PathCacheQueryType.UnitPath, self.ActorID, source, target); // Only cache path when transient actors are ignored, otherwise there is no guarantee that the path // is still valid at the next check. @@ -58,7 +58,7 @@ public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, { using (new PerfSample("Pathfinder")) { - var key = "FindUnitPathToRange" + self.ActorID + source.X + source.Y + target.X + target.Y; + var key = new PathCacheKey(PathCacheQueryType.UnitPathToRange, self.ActorID, source, target); if (check == BlockedByActor.None) { @@ -76,13 +76,13 @@ public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, } } - public List FindPath(IPathSearch search) + public List FindPath(BasePathSearch search) { using (new PerfSample("Pathfinder")) return pathFinder.FindPath(search); } - public List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) + public List FindBidiPath(BasePathSearch fromSrc, BasePathSearch fromDest) { using (new PerfSample("Pathfinder")) return pathFinder.FindBidiPath(fromSrc, fromDest); diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index 54f20b72cf6d..2c7288bcf651 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -18,37 +18,6 @@ namespace OpenRA.Mods.Common.Pathfinder { - /// - /// Represents a graph with nodes and edges - /// - /// The type of node used in the graph - public interface IGraph : IDisposable - { - /// - /// Gets all the Connections for a given node in the graph - /// - List GetConnections(CPos position); - - /// - /// Retrieves an object given a node in the graph - /// - T this[CPos pos] { get; set; } - - Func CustomBlock { get; set; } - - Func 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; @@ -73,44 +42,41 @@ public GraphConnection(CPos destination, int cost) } } - sealed class PathGraph : IGraph + public class PathGraph { public const int CostForInvalidCell = int.MaxValue; - public Actor Actor { get; private set; } - public World World { get; private set; } - public Func CustomBlock { get; set; } - public Func CustomCost { get; set; } - public int LaneBias { get; set; } - public bool InReverse { get; set; } - public Actor IgnoreActor { get; set; } + public readonly PathQuery Query; - readonly BlockedByActor checkConditions; - readonly Locomotor locomotor; readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer; readonly bool checkTerrainHeight; + readonly int laneBias; CellLayer groundInfo; readonly Dictionary Info)> customLayerInfo = new Dictionary)>(); - public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check) + ICustomMovementLayer[] customMovementLayers; + + public PathGraph(CellInfoLayerPool layerPool, PathQuery query) { + Query = query; pooledLayer = layerPool.Get(); groundInfo = pooledLayer.GetLayer(); - var locomotorInfo = locomotor.Info; - this.locomotor = locomotor; - var layers = world.GetCustomMovementLayers().Values - .Where(cml => cml.EnabledForActor(actor.Info, locomotorInfo)); - - foreach (var cml in layers) - customLayerInfo[cml.Index] = (cml, pooledLayer.GetLayer()); - - World = world; - Actor = actor; - LaneBias = 1; - checkConditions = check; - checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0; + + var locomotorInfo = Query.Locomotor.Info; + foreach (var cml in Query.World.GetCustomMovementLayers().Values) + if (cml.EnabledForActor(Query.Actor.Info, locomotorInfo)) + customLayerInfo[cml.Index] = (cml, pooledLayer.GetLayer()); + + // PERF: Store dictionary values as array. + customMovementLayers = new ICustomMovementLayer[customLayerInfo.Count]; + var i = 0; + foreach (var cli in customLayerInfo.Values) + customMovementLayers[i++] = cli.Layer; + + laneBias = Query.LaneBiasDisabled ? 0 : 1; + checkTerrainHeight = Query.World.Map.Grid.MaximumTerrainHeight > 0; } // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed @@ -133,7 +99,8 @@ public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, public List GetConnections(CPos position) { - var info = position.Layer == 0 ? groundInfo : customLayerInfo[position.Layer].Info; + var positionLayer = position.Layer; + var info = positionLayer == 0 ? groundInfo : customLayerInfo[positionLayer].Info; var previousPos = info[position].PreviousPos; var dx = position.X - previousPos.X; @@ -141,29 +108,39 @@ public List GetConnections(CPos position) var index = dy * 3 + dx + 4; var directions = DirectedNeighbors[index]; - var validNeighbors = new List(directions.Length); + var validNeighbors = new List(directions.Length + + (positionLayer == 0 ? customMovementLayers.Length : 1)); for (var i = 0; i < directions.Length; i++) { - var neighbor = position + directions[i]; - var movementCost = GetCostToNode(neighbor, directions[i]); + var direction = directions[i]; + var neighbor = position + direction; + var neighborCell = info[neighbor]; + + // PERF: Skip closed cells already, ~15% of all cells + if (neighborCell.Status == CellStatus.Closed) + continue; + + var movementCost = GetCostToNode(neighbor, direction); if (movementCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(neighbor, movementCost)); } - if (position.Layer == 0) + var actorInfo = Query.Actor.Info; + var locomotorInfo = Query.Locomotor.Info; + if (positionLayer == 0) { - foreach (var cli in customLayerInfo.Values) + foreach (var layer in customMovementLayers) { - var layerPosition = new CPos(position.X, position.Y, cli.Layer.Index); - var entryCost = cli.Layer.EntryMovementCost(Actor.Info, locomotor.Info, layerPosition); + var layerPosition = position.AtLayer(layer.Index); + var entryCost = layer.EntryMovementCost(actorInfo, locomotorInfo, layerPosition); if (entryCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(layerPosition, entryCost)); } } else { - var layerPosition = new CPos(position.X, position.Y, 0); - var exitCost = customLayerInfo[position.Layer].Layer.ExitMovementCost(Actor.Info, locomotor.Info, layerPosition); + var layerPosition = position.AtLayer(0); + var exitCost = customLayerInfo[positionLayer].Layer.ExitMovementCost(actorInfo, locomotorInfo, layerPosition); if (exitCost != CostForInvalidCell) validNeighbors.Add(new GraphConnection(layerPosition, exitCost)); } @@ -173,8 +150,14 @@ public List GetConnections(CPos position) int GetCostToNode(CPos destNode, CVec direction) { - var movementCost = locomotor.MovementCostToEnterCell(Actor, destNode, checkConditions, IgnoreActor); - if (movementCost != short.MaxValue && !(CustomBlock != null && CustomBlock(destNode))) + var movementCost = Query.Locomotor.MovementCostToEnterCell( + Query.Actor, + destNode, + Query.Check, + Query.IgnoreActor); + + if (movementCost != short.MaxValue && + !(Query.CustomBlock != null && Query.CustomBlock(destNode))) return CalculateCellCost(destNode, direction, movementCost); return CostForInvalidCell; @@ -187,49 +170,59 @@ int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost) if (direction.X * direction.Y != 0) cellCost = (cellCost * 34) / 24; - if (CustomCost != null) + var customCost = Query.CustomCost; + if (customCost != null) { - var customCost = CustomCost(neighborCPos); - if (customCost == CostForInvalidCell) + var cc = customCost(neighborCPos); + if (cc == CostForInvalidCell) return CostForInvalidCell; - cellCost += customCost; + cellCost += cc; } // Prevent units from jumping over height discontinuities if (checkTerrainHeight && neighborCPos.Layer == 0) { + var heightLayer = Query.World.Map.Height; var from = neighborCPos - direction; - if (Math.Abs(World.Map.Height[neighborCPos] - World.Map.Height[from]) > 1) + if (Math.Abs(heightLayer[neighborCPos] - heightLayer[from]) > 1) return CostForInvalidCell; } // Directional bonuses for smoother flow! - if (LaneBias != 0) + if (laneBias != 0) { - var ux = neighborCPos.X + (InReverse ? 1 : 0) & 1; - var uy = neighborCPos.Y + (InReverse ? 1 : 0) & 1; + var reverse = Query.Reverse ? 1 : 0; + var ux = neighborCPos.X + reverse & 1; + var uy = neighborCPos.Y + reverse & 1; if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0)) - cellCost += LaneBias; + cellCost += laneBias; if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0)) - cellCost += LaneBias; + cellCost += laneBias; } return cellCost; } + public CellInfo CellAt(CPos pos) + { + var layer = pos.Layer; + return (layer == 0 ? groundInfo : customLayerInfo[layer].Info)[pos]; + } + public CellInfo this[CPos pos] { - get => (pos.Layer == 0 ? groundInfo : customLayerInfo[pos.Layer].Info)[pos]; - set => (pos.Layer == 0 ? groundInfo : customLayerInfo[pos.Layer].Info)[pos] = value; + get { var layer = pos.Layer; return (layer == 0 ? groundInfo : customLayerInfo[layer].Info)[pos]; } + set { var layer = pos.Layer; (layer == 0 ? groundInfo : customLayerInfo[layer].Info)[pos] = value; } } public void Dispose() { groundInfo = null; customLayerInfo.Clear(); + customMovementLayers = null; pooledLayer.Dispose(); } } diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index 82770e32d612..1d78464830ad 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -36,61 +36,25 @@ static CellInfoLayerPool LayerPoolForWorld(World world) #region Constructors - private PathSearch(IGraph graph) - : base(graph) + public PathSearch(PathQuery query, bool debug = false) + : base(new PathGraph(LayerPoolForWorld(query.World), query), query, debug) { considered = new LinkedList<(CPos, int)>(); - } - - public static IPathSearch Search(World world, Locomotor locomotor, Actor self, BlockedByActor check, Func goalCondition) - { - var graph = new PathGraph(LayerPoolForWorld(world), locomotor, self, world, check); - var search = new PathSearch(graph); - search.isGoal = goalCondition; - search.heuristic = loc => 0; - return search; - } - - 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 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 => + if (Query.FromPositions != null) { - var locInfo = search.Graph[loc]; - return locInfo.EstimatedTotal - locInfo.CostSoFar == 0; - }; - - foreach (var sl in froms) - if (world.Map.Contains(sl)) - search.AddInitialCell(sl); - - return search; + var map = Query.World.Map; + foreach (var sl in Query.FromPositions) + if (map.Contains(sl)) + AddInitialCell(sl); + } } - protected override void AddInitialCell(CPos location) + void AddInitialCell(CPos location) { var cost = heuristic(location); Graph[location] = new CellInfo(0, cost, location, CellStatus.Open); var connection = new GraphConnection(location, cost); OpenQueue.Add(connection); - StartPoints.Add(connection); considered.AddLast((location, 0)); } @@ -100,16 +64,27 @@ protected override void AddInitialCell(CPos location) /// 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 override CPos Expand() + /// The most promising node of the iteration or false if expansion is no longer possible + public override bool TryExpand(out CPos mostPromisingNode) { - var currentMinNode = OpenQueue.Pop().Destination; + if (!OpenQueue.TryPop(out var currentMinConnection)) + { + mostPromisingNode = default(CPos); + return false; + } + + var currentMinNode = currentMinConnection.Destination; var currentCell = Graph[currentMinNode]; - Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed); + Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, + currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed); - if (Graph.CustomCost != null && Graph.CustomCost(currentMinNode) == PathGraph.CostForInvalidCell) - return currentMinNode; + var customCost = Query.CustomCost; + if (customCost != null && customCost(currentMinNode) == PathGraph.CostForInvalidCell) + { + mostPromisingNode = currentMinNode; + return true; + } foreach (var connection in Graph.GetConnections(currentMinNode)) { @@ -147,7 +122,8 @@ public override CPos Expand() } } - return currentMinNode; + mostPromisingNode = currentMinNode; + return true; } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs index 7f2ef2231286..bf9f31ddde07 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs @@ -149,17 +149,38 @@ Target FindNextResource(Actor actor, HarvesterTraitWrapper harv) harv.Harvester.CanHarvestCell(actor, cell) && claimLayer.CanClaimCell(actor, cell); - var path = pathfinder.FindPath( - PathSearch.Search(world, harv.Locomotor, actor, BlockedByActor.Stationary, isValidResource) - .WithCustomCost(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))) - .FromPoint(actor.Location)); - - if (path.Count == 0) - return Target.Invalid; + var map = world.Map; + var avoidRadius = Info.HarvesterEnemyAvoidanceRadius; + var avoidRadiusLength = avoidRadius.Length; + var owner = actor.Owner; + Func customCost = loc => + { + var enemies = world.FindActorsInCircle(map.CenterOfCell(loc), avoidRadius) + .Where(u => !u.IsDead + && owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy); + + return enemies.Sum(u => Math.Max(WDist.Zero.Length, + avoidRadiusLength - (map.CenterOfCell(loc) - u.CenterPosition).Length)); + }; + + var query = new PathQuery( + queryType: PathQueryType.ConditionUnidirectional, + world: world, + locomotor: harv.Locomotor, + actor: actor, + check: BlockedByActor.Stationary, + customCost: customCost, + fromPosition: actor.Location, + isGoal: isValidResource); + + using (var search = new PathSearch(query)) + { + var path = pathfinder.FindPath(search); + if (path.Count == 0) + return Target.Invalid; - return Target.FromCell(world, path[0]); + return Target.FromCell(world, path[0]); + } } } } diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index 8a57192c18bc..bb0cace65103 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -178,38 +178,56 @@ bool IsAcceptableProcType(Actor proc) public Actor ClosestProc(Actor self, Actor ignore) { - // Find all refineries and their occupancy count: - var refineries = self.World.ActorsWithTrait() + var world = self.World; + + // Find all refineries and their occupancy count + var refineries = world.ActorsWithTrait() .Where(r => r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor)) - .Select(r => new - { - Location = r.Actor.Location + r.Trait.DeliveryOffset, - Actor = r.Actor, - Occupancy = self.World.ActorsHavingTrait(h => h.LinkedProc == r.Actor).Count() - }).ToLookup(r => r.Location); + .ToArray(); - // Start a search from each refinery's delivery location: - List path; + if (refineries.Length == 0) + return null; - using (var search = PathSearch.FromPoints(self.World, mobile.Locomotor, self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None) - .WithCustomCost(location => - { - if (!refineries.Contains(location)) - return 0; - - var occupancy = refineries[location].First().Occupancy; + // PERF: Avoid LINQ, use Dictionary over Lookup, calculate cost already + var locationCost = new Dictionary(refineries.Length); + foreach (var r in refineries) + { + var location = r.Actor.Location + r.Trait.DeliveryOffset; + var occupancy = world.ActorsHavingTrait(h => h.LinkedProc == r.Actor) + .Count(); + int cost; + if (occupancy >= Info.MaxUnloadQueue) + { // Too many harvesters clogs up the refinery's delivery location: - if (occupancy >= Info.MaxUnloadQueue) - return PathGraph.CostForInvalidCell; - + cost = PathGraph.CostForInvalidCell; + } + else + { // Prefer refineries with less occupancy (multiplier is to offset distance cost): - return occupancy * Info.UnloadQueueCostModifier; - })) + cost = occupancy * Info.UnloadQueueCostModifier; + } + + locationCost.TryAdd(location, (r.Actor, cost)); + } + + // Start a search from each refinery's delivery location: + List path; + var query = new PathQuery( + queryType: PathQueryType.PositionUnidirectional, + world: world, + locomotor: mobile.Locomotor, + actor: self, + fromPositions: locationCost.Keys, + toPosition: self.Location, + check: BlockedByActor.None, + customCost: location => locationCost.TryGetValue(location, out var lc) ? lc.Cost : 0); + + using (var search = new PathSearch(query)) path = mobile.Pathfinder.FindPath(search); if (path.Count != 0) - return refineries[path.Last()].First().Actor; + return locationCost[path.Last()].Actor; return null; } diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index ff52f4b501d3..3171098ab52e 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -804,9 +804,16 @@ Activity LocalMove(Actor self, WPos fromPos, WPos toPos, CPos cell) return above; List path; - using (var search = PathSearch.Search(self.World, Locomotor, self, BlockedByActor.All, - loc => loc.Layer == 0 && CanEnterCell(loc)) - .FromPoint(self.Location)) + var query = new PathQuery( + queryType: PathQueryType.ConditionUnidirectional, + world: self.World, + locomotor: Locomotor, + actor: self, + check: BlockedByActor.All, + fromPosition: self.Location, + isGoal: loc => loc.Layer == 0 && CanEnterCell(loc)); + + using (var search = new PathSearch(query)) path = Pathfinder.FindPath(search); if (path.Count > 0) diff --git a/OpenRA.Mods.Common/Traits/World/ActorMap.cs b/OpenRA.Mods.Common/Traits/World/ActorMap.cs index 11b61b0863f1..d1ce9ba628b7 100644 --- a/OpenRA.Mods.Common/Traits/World/ActorMap.cs +++ b/OpenRA.Mods.Common/Traits/World/ActorMap.cs @@ -261,7 +261,8 @@ public IEnumerable GetActorsAt(CPos a) if (!influence.Contains(uv)) return Enumerable.Empty(); - var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; + var layerIndex = a.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; return new ActorsAtEnumerable(layer[uv]); } @@ -271,7 +272,8 @@ public IEnumerable GetActorsAt(CPos a, SubCell sub) if (!influence.Contains(uv)) yield break; - var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; + var layerIndex = a.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; for (var i = layer[uv]; i != null; i = i.Next) if (!i.Actor.Disposed && (i.SubCell == sub || i.SubCell == SubCell.FullCell || sub == SubCell.FullCell || sub == SubCell.Any)) yield return i.Actor; @@ -318,7 +320,8 @@ public bool AnyActorsAt(CPos a) if (!influence.Contains(uv)) return false; - var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; + var layerIndex = a.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; return layer[uv] != null; } @@ -330,7 +333,8 @@ public bool AnyActorsAt(CPos a, SubCell sub, bool checkTransient = true) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; - var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; + var layerIndex = a.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; for (var i = layer[uv]; i != null; i = i.Next) { if (always || i.SubCell == sub || i.SubCell == SubCell.FullCell) @@ -355,7 +359,8 @@ public bool AnyActorsAt(CPos a, SubCell sub, Func withCondition) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; - var layer = a.Layer == 0 ? influence : customInfluence[a.Layer]; + var layerIndex = a.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; for (var i = layer[uv]; i != null; i = i.Next) if ((always || i.SubCell == sub || i.SubCell == SubCell.FullCell) && !i.Actor.Disposed && withCondition(i.Actor)) return true; @@ -371,7 +376,8 @@ public void AddInfluence(Actor self, IOccupySpace ios) if (!influence.Contains(uv)) continue; - var layer = c.Cell.Layer == 0 ? influence : customInfluence[c.Cell.Layer]; + var layerIndex = c.Cell.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; layer[uv] = new InfluenceNode { Next = layer[uv], SubCell = c.SubCell, Actor = self }; if (cellTriggerInfluence.TryGetValue(c.Cell, out var triggers)) @@ -390,7 +396,8 @@ public void RemoveInfluence(Actor self, IOccupySpace ios) if (!influence.Contains(uv)) continue; - var layer = c.Cell.Layer == 0 ? influence : customInfluence[c.Cell.Layer]; + var layerIndex = c.Cell.Layer; + var layer = layerIndex == 0 ? influence : customInfluence[layerIndex]; var temp = layer[uv]; RemoveInfluenceInner(ref temp, self); layer[uv] = temp; diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs index db36c49a9919..8caee8511273 100644 --- a/OpenRA.Mods.Common/Traits/World/Locomotor.cs +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -175,7 +175,8 @@ public short MovementCostForCell(CPos cell) if (!world.Map.Contains(cell)) return short.MaxValue; - return cell.Layer == 0 ? cellsCost[cell] : customLayerCellsCost[cell.Layer][cell]; + var layer = cell.Layer; + return layer == 0 ? cellsCost[cell] : customLayerCellsCost[layer][cell]; } public int MovementSpeedForCell(CPos cell) @@ -191,7 +192,8 @@ public short MovementCostToEnterCell(Actor actor, CPos destNode, BlockedByActor if (!world.Map.Contains(destNode)) return short.MaxValue; - var cellCost = destNode.Layer == 0 ? cellsCost[destNode] : customLayerCellsCost[destNode.Layer][destNode]; + var destLayer = destNode.Layer; + var cellCost = destLayer == 0 ? cellsCost[destNode] : customLayerCellsCost[destLayer][destNode]; if (cellCost == short.MaxValue || !CanMoveFreelyInto(actor, destNode, check, ignoreActor)) @@ -399,7 +401,8 @@ CellCache GetCache(CPos cell) dirtyCells.Remove(cell); } - var cache = cell.Layer == 0 ? blockingCache : customLayerBlockingCache[cell.Layer]; + var layer = cell.Layer; + var cache = layer == 0 ? blockingCache : customLayerBlockingCache[layer]; return cache[cell]; } diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index b90cc5b8d7db..b9b25096b497 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -38,14 +38,14 @@ public interface IPathFinder /// /// Calculates a path given a search specification /// - List FindPath(IPathSearch search); + List FindPath(BasePathSearch search); /// /// Calculates a path given two search specifications, and /// then returns a path when both search intersect each other /// TODO: This should eventually disappear /// - List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest); + List FindBidiPath(BasePathSearch fromSrc, BasePathSearch fromDest); } public class PathFinder : IPathFinder @@ -78,15 +78,24 @@ public List FindUnitPath(CPos source, CPos target, Actor self, Actor ignor var distance = source - target; var canMoveFreely = locomotor.CanMoveFreelyInto(self, target, check, null); if (distance.LengthSquared < 3 && !canMoveFreely) - return new List { }; + return EmptyPath; if (source.Layer == target.Layer && distance.LengthSquared < 3 && canMoveFreely) return new List { target }; List pb; - - using (var fromSrc = PathSearch.FromPoint(world, locomotor, self, target, source, check).WithIgnoredActor(ignoreActor)) - using (var fromDest = PathSearch.FromPoint(world, locomotor, self, source, target, check).WithIgnoredActor(ignoreActor).Reverse()) + var fromSrcQuery = new PathQuery( + queryType: PathQueryType.PositionBidirectional, + world: world, + locomotor: locomotor, + actor: self, + fromPosition: target, + toPosition: source, + check: check, + ignoreActor: ignoreActor); + + using (var fromSrc = new PathSearch(fromSrcQuery)) + using (var fromDest = new PathSearch(fromSrcQuery.CreateReverse())) pb = FindBidiPath(fromSrc, fromDest); return pb; @@ -111,9 +120,12 @@ public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, // 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 = world.Map.FindTilesInCircle(targetCell, range.Length / 1024 + 1) - .Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= range.LengthSquared - && mobile.Info.CanEnterCell(self.World, self, t)); + var rangeLengthSquared = range.LengthSquared; + var mobileInfo = mobile.Info; + var map = world.Map; + var tilesInRange = map.FindTilesInCircle(targetCell, range.Length / 1024 + 1) + .Where(t => (map.CenterOfCell(t) - target).LengthSquared <= rangeLengthSquared + && mobileInfo.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 @@ -124,19 +136,38 @@ public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, 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()) + var fromSrcQuery = new PathQuery( + queryType: PathQueryType.PositionBidirectional, + world: world, + locomotor: locomotor, + actor: self, + fromPositions: tilesInRange, + toPosition: source, + check: check); + + var fromDestQuery = new PathQuery( + queryType: PathQueryType.PositionBidirectional, + world: world, + locomotor: locomotor, + actor: self, + fromPosition: source, + toPosition: targetCell, + check: check, + reverse: true); + + using (var fromSrc = new PathSearch(fromSrcQuery)) + using (var fromDest = new PathSearch(fromDestQuery)) return FindBidiPath(fromSrc, fromDest); } - public List FindPath(IPathSearch search) + public List FindPath(BasePathSearch search) { - List path = null; + List path = EmptyPath; - while (search.CanExpand) + var isGoal = search.IsGoal; + while (search.TryExpand(out var p)) { - var p = search.Expand(); - if (search.IsTarget(p)) + if (isGoal(p)) { path = MakePath(search.Graph, p); break; @@ -145,34 +176,30 @@ public List FindPath(IPathSearch search) search.Graph.Dispose(); - if (path != null) - return path; - - // no path exists - return EmptyPath; + return path; } // Searches from both ends toward each other. This is used to prevent blockings in case we find // units in the middle of the path that prevent us to continue. - public List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) + public List FindBidiPath(BasePathSearch fromSrc, BasePathSearch fromDest) { - List path = null; + List path = EmptyPath; - while (fromSrc.CanExpand && fromDest.CanExpand) + while (fromSrc.TryExpand(out var p) && fromDest.TryExpand(out var q)) { // make some progress on the first search - var p = fromSrc.Expand(); - if (fromDest.Graph[p].Status == CellStatus.Closed && - fromDest.Graph[p].CostSoFar < int.MaxValue) + var pci = fromDest.Graph[p]; + if (pci.Status == CellStatus.Closed && + pci.CostSoFar < int.MaxValue) { 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 < int.MaxValue) + var qci = fromSrc.Graph[q]; + if (qci.Status == CellStatus.Closed && + qci.CostSoFar < int.MaxValue) { path = MakeBidiPath(fromSrc, fromDest, q); break; @@ -182,30 +209,29 @@ public List FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest) fromSrc.Graph.Dispose(); fromDest.Graph.Dispose(); - if (path != null) - return path; - - return EmptyPath; + return path; } // 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(IGraph cellInfo, CPos destination) + static List MakePath(PathGraph cellInfo, CPos destination) { var ret = new List(); var currentNode = destination; + var prevNode = cellInfo[currentNode].PreviousPos; - while (cellInfo[currentNode].PreviousPos != currentNode) + while (prevNode != currentNode) { ret.Add(currentNode); - currentNode = cellInfo[currentNode].PreviousPos; + currentNode = prevNode; + prevNode = cellInfo[currentNode].PreviousPos; } ret.Add(currentNode); return ret; } - static List MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode) + static List MakeBidiPath(BasePathSearch a, BasePathSearch b, CPos confluenceNode) { var ca = a.Graph; var cb = b.Graph; @@ -213,10 +239,12 @@ static List MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode var ret = new List(); var q = confluenceNode; - while (ca[q].PreviousPos != q) + var prevQ = ca[q].PreviousPos; + while (prevQ != q) { ret.Add(q); - q = ca[q].PreviousPos; + q = prevQ; + prevQ = ca[q].PreviousPos; } ret.Add(q); @@ -224,9 +252,11 @@ static List MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode ret.Reverse(); q = confluenceNode; - while (cb[q].PreviousPos != q) + prevQ = cb[q].PreviousPos; + while (prevQ != q) { - q = cb[q].PreviousPos; + q = prevQ; + prevQ = cb[q].PreviousPos; ret.Add(q); } diff --git a/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs b/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs index e04b0fafc7f2..c2b3ce024503 100644 --- a/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs @@ -72,10 +72,9 @@ WPos ICustomMovementLayer.CenterOfCell(CPos cell) return pos + new WVec(0, 0, height[cell] - pos.Z); } - bool ValidTransitionCell(CPos cell, LocomotorInfo li) + bool ValidTransitionCell(CPos cell, SubterraneanLocomotorInfo sli) { var terrainType = map.GetTerrainInfo(cell).Type; - var sli = (SubterraneanLocomotorInfo)li; if (!sli.SubterraneanTransitionTerrainTypes.Contains(terrainType) && sli.SubterraneanTransitionTerrainTypes.Any()) return false;