#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Common.Pathfinder { /// /// Provides pathfinding abilities for actors that use a specific . /// Maintains a hierarchy of abstract graphs that provide a more accurate heuristic function during /// A* pathfinding than the one available from . /// This allows for faster pathfinding. /// /// /// The goal of this pathfinder is to increase performance of path searches. is used /// to perform a path search as usual, but a different heuristic function is provided that is more accurate. This /// means fewer nodes have to be explored during the search, resulting in a performance increase. /// /// When an A* path search is performed, the search expands outwards from the source location until the /// target is found. The heuristic controls how this expansion occurs. When the heuristic of h(n) = 0 is given, we /// get Dijkstra's algorithm. The search grows outwards from the source node in an expanding circle with no sense /// of direction. This will find the shortest path by brute force. It will explore many nodes during the search, /// including lots of nodes in the opposite direction to the target. /// /// provides heuristic for searching a 2D grid. It /// estimates the cost as the straight-line distance between the source and target nodes. The search grows in a /// straight line towards the target node. This is a vast improvement over Dijkstra's algorithm as we now /// prioritize exploring nodes that lie closer to the target, rather than exploring nodes that take us away from /// the target. /// /// This default straight-line heuristic still has drawbacks - it is unaware of the obstacles on the grid. If /// the route to be found requires steering around obstacles then this heuristic can perform badly. Imagine a path /// that must steer around a lake, or move back on itself to get out of a dead end. In these cases the straight-line /// heuristic moves blindly towards the target, when actually the path requires that we move sidewards or even /// backwards to find a route. When this occurs then the straight-line heuristic ends up exploring nodes that /// aren't useful - they lead us into dead ends or directly into an obstacle that we need to go around instead. /// /// /// The improves the heuristic by making it aware of unreachable map /// terrain. A "low-resolution" version of the map is maintained, and used to provide an initial route. When the /// search is conducted it explores along this initial route. This allows the search to "know" it needs to go /// sideways around the lake or backwards out of the dead-end, meaning we can explore even fewer nodes. /// /// The "low-resolution" version of the map is referred to as the abstract graph. The abstract graph is /// created by dividing the map up into a series of grids, of say 10x10 nodes. Within each grid, we determine the /// connected regions of nodes within that grid. If all the nodes within the grid connect to each other, we have /// one such region. If they are split up by impassable terrain then we may have two or more regions within the /// grid. Every region will be represented by one node in the abstract graph (an abstract node, for short). /// /// When a path search is to be performed, we first perform a A* search on the abstract graph with the /// . This graph is much smaller than the full map, so /// this search is quick. The resulting path gives us the initial route between each abstract node. We can then use /// this to create the improved heuristic for use on the path search on the full resolution map. When determining /// the cost for the node, we can use the straight-line distance towards the next abstract node as our estimate. /// Our search is therefore guided along the initial route. /// /// This implementation only maintains one level of abstract graph, but a hierarchy of such graphs is /// possible. This allows the top-level and lowest resolution graph to be as small as possible - important because /// it will be searched using the dumbest heuristic. Each level underneath is higher-resolution and contains more /// nodes, but uses a heuristic informed from the previous level to guide the search in the right direction. /// /// This implementation is aware of movement costs over terrain given by /// . It is aware of /// changes to the costs in terrain and able to update the abstract graph when this occurs. It is able to search /// the abstract graph as if had been specified. If /// is given in the constructor, the abstract graph will additionally /// account for a subset of immovable actors using the same rules as /// . It will be aware /// of changes to actors on the map and update the abstract graph when this occurs. Other types of blocking actors /// will not be accounted for in the heuristic. /// /// If the obstacle on the map is from terrain (e.g. a cliff or lake) the heuristic will work well. If the /// obstacle is from the subset of immovable actors (e.g. trees, walls, buildings) and /// was given, the heuristic will work well. If the obstacle is from other /// actors (e.g. units) then the heuristic is unaware of these. Therefore the same problem where the search goes in /// the wrong direction is possible, e.g. through a choke-point that has units blocking it. In this scenario the /// performance benefit will be lost, as the search will have to explore more nodes until it can get around the /// obstacle. /// /// In summary, the reduces the performance impact of path searches that /// must go around terrain, and some types of actor, but does not improve performance of searches that must go /// around the remaining types of actor. /// public sealed class HierarchicalPathFinder { // This value determined via empiric testing as the best performance trade-off. const int GridSize = 10; readonly World world; readonly Locomotor locomotor; readonly IActorMap actorMap; readonly Func costEstimator; readonly HashSet dirtyGridIndexes = new(); readonly HashSet cellsWithBlockingActor; Grid mapBounds; int gridXs; int gridYs; /// /// Index by a . /// GridInfo[] gridInfos; /// /// The abstract graph is represented here. /// An abstract node is the key, and costs to other abstract nodes are then available. /// Abstract nodes with no connections are NOT present in the graph. /// A lookup will fail, rather than return an empty list. /// Dictionary> abstractGraph; /// /// The abstract domains are represented here. /// An abstract node is the key, and a domain index is given. /// If the domain index of two nodes is equal, a path exists between them (ignoring all blocking actors). /// If unequal, no path is possible. /// readonly Dictionary abstractDomains; /// /// Knows about the abstract nodes within a grid. Can map a local cell to its abstract node. /// readonly struct GridInfo { readonly CPos?[] singleAbstractCellForLayer; readonly Dictionary localCellToAbstractCell; public GridInfo(CPos?[] singleAbstractCellForLayer, Dictionary localCellToAbstractCell) { this.singleAbstractCellForLayer = singleAbstractCellForLayer; this.localCellToAbstractCell = localCellToAbstractCell; } /// /// Maps a local cell to a abstract node in the graph. /// Returns null when the local cell is unreachable. /// Pass a null to skip cost checks if the caller already checked. /// public CPos? AbstractCellForLocalCell(CPos localCell, HierarchicalPathFinder hpf) { var abstractCell = singleAbstractCellForLayer[localCell.Layer]; if (abstractCell != null) { // All reachable cells in the grid are joined together so only a single abstract cell was needed, // but there may be unreachable cells in the grid which we must exclude. if (hpf != null && !hpf.CellIsAccessible(localCell)) return null; return abstractCell; } // Only reachable cells would be populated in the lookup, so no need to check their cost. if (localCellToAbstractCell.TryGetValue(localCell, out var abstractCellFromMap)) return abstractCellFromMap; return null; } public void CopyAbstractCellsInto(HashSet set) { foreach (var single in singleAbstractCellForLayer) if (single != null) set.Add(single.Value); foreach (var cell in localCellToAbstractCell.Values) set.Add(cell); } } /// /// Represents an abstract graph with some extra edges inserted. /// Instead of building a new dictionary with the edges added, we build a supplemental dictionary of changes. /// This is to avoid copying the entire abstract graph. /// sealed class AbstractGraphWithInsertedEdges { readonly Dictionary> abstractEdges; readonly Dictionary> changedEdges; public AbstractGraphWithInsertedEdges( Dictionary> abstractEdges, IList sourceEdges, GraphEdge? targetEdge, Func costEstimator) { this.abstractEdges = abstractEdges; changedEdges = new Dictionary>(sourceEdges.Count * 9 + (targetEdge != null ? 9 : 0)); foreach (var sourceEdge in sourceEdges) InsertEdgeAsBidirectional(sourceEdge, costEstimator); if (targetEdge != null) InsertEdgeAsBidirectional(targetEdge.Value, costEstimator); } void InsertEdgeAsBidirectional(GraphEdge edge, Func costEstimator) { InsertConnections(edge.Source, edge.Destination, costEstimator); } void InsertConnections(CPos localCell, CPos abstractCell, Func costEstimator) { if (!abstractEdges.TryGetValue(abstractCell, out var edges)) edges = new List(); changedEdges[localCell] = edges .Select(e => new GraphConnection(e.Destination, costEstimator(localCell, e.Destination))) .Append(new GraphConnection(abstractCell, costEstimator(localCell, abstractCell))); IEnumerable abstractChangedEdges = edges; if (changedEdges.TryGetValue(abstractCell, out var existingEdges)) abstractChangedEdges = existingEdges; changedEdges[abstractCell] = abstractChangedEdges .Append(new GraphConnection(localCell, costEstimator(abstractCell, localCell))); foreach (var conn in edges) { IEnumerable connChangedEdges; if (changedEdges.TryGetValue(conn.Destination, out var existingConnEdges)) connChangedEdges = existingConnEdges; else connChangedEdges = abstractEdges[conn.Destination]; changedEdges[conn.Destination] = connChangedEdges .Append(new GraphConnection(localCell, costEstimator(conn.Destination, localCell))); } } public List GetConnections(CPos position) { if (changedEdges.TryGetValue(position, out var changedEdge)) return changedEdge.ToList(); if (abstractEdges.TryGetValue(position, out var abstractEdge)) return abstractEdge; return new List(); } } public HierarchicalPathFinder(World world, Locomotor locomotor, IActorMap actorMap, BlockedByActor check) { this.world = world; this.locomotor = locomotor; this.actorMap = actorMap; if (locomotor.Info.TerrainSpeeds.Count == 0) return; if (check == BlockedByActor.Immovable) { // When we account for immovable actors, it depends on the actors on the map. // When this changes, we must update the cost table. actorMap.CellUpdated += RequireBlockingRefreshInCell; // Determine immovable cells from actors already on the map. cellsWithBlockingActor = actorMap.AllActors() .Where(ActorIsBlocking) .SelectMany(a => a.OccupiesSpace.OccupiedCells() .Select(oc => oc.Cell) .Where(c => ActorCellIsBlocking(a, c))) .ToHashSet(); } else if (check != BlockedByActor.None) throw new System.ComponentModel.InvalidEnumArgumentException( $"{nameof(HierarchicalPathFinder)} supports {nameof(BlockedByActor.None)} " + $"and {nameof(BlockedByActor.Immovable)} only for {nameof(check)}"); costEstimator = PathSearch.DefaultCostEstimator(locomotor); BuildGrids(); BuildCostTable(); abstractDomains = new Dictionary(gridXs * gridYs); RebuildDomains(); // When we build the cost table, it depends on the movement costs of the cells at that time. // When this changes, we must update the cost table. locomotor.CellCostChanged += RequireCostRefreshInCell; // If the map projection changes, the result of Map.Contains(CPos) may change. // We need to rebuild grids to account for this possibility. this.world.Map.CellProjectionChanged += RequireProjectionRefreshInCell; } public ( IReadOnlyDictionary> AbstractGraph, IReadOnlyDictionary AbstractDomains) GetOverlayData() { if (costEstimator == null) return default; // Ensure the abstract graph and domains are up to date when using the overlay. RebuildDirtyGrids(); RebuildDomains(); return ( new ReadOnlyDictionary>(abstractGraph), new ReadOnlyDictionary(abstractDomains)); } /// /// Divides the map area up into a series of grids. /// void BuildGrids() { static Grid GetCPosBounds(Map map) { if (map.Grid.Type == MapGridType.RectangularIsometric) { var bottomRight = map.AllCells.BottomRight; var bottomRightU = bottomRight.ToMPos(map).U; return new Grid( new CPos(0, -bottomRightU), new CPos(bottomRight.X + 1, bottomRight.Y + bottomRightU + 1), false); } return new Grid(CPos.Zero, (CPos)map.MapSize, false); } mapBounds = GetCPosBounds(world.Map); gridXs = Exts.IntegerDivisionRoundingAwayFromZero(mapBounds.Width, GridSize); gridYs = Exts.IntegerDivisionRoundingAwayFromZero(mapBounds.Height, GridSize); var customMovementLayers = world.GetCustomMovementLayers(); gridInfos = new GridInfo[gridXs * gridYs]; for (var gridX = mapBounds.TopLeft.X; gridX < mapBounds.BottomRight.X; gridX += GridSize) for (var gridY = mapBounds.TopLeft.Y; gridY < mapBounds.BottomRight.Y; gridY += GridSize) gridInfos[GridIndex(new CPos(gridX, gridY))] = BuildGrid(gridX, gridY, customMovementLayers); } /// /// Determines the abstract nodes within a single grid. One abstract node will be created for each set of cells /// that are reachable from each other within the grid area. A grid with open terrain will commonly have one /// abstract node. If impassable terrain such as cliffs or water divides the cells into 2 or more distinct /// regions, one abstract node is created for each region. We also remember which cells belong to which /// abstract node. Given a local cell, this allows us to determine which abstract node it belongs to. /// GridInfo BuildGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLayers) { var singleAbstractCellForLayer = new CPos?[customMovementLayers.Length]; var localCellToAbstractCell = new Dictionary(); // When accounting for immovable actors, use a custom cost so those cells become invalid paths. var customCost = cellsWithBlockingActor == null ? (Func)null : c => cellsWithBlockingActor.Contains(c) ? PathGraph.PathCostForInvalidPath : 0; var accessibleCells = new HashSet(GridSize * GridSize); for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++) { if (gridLayer != 0 && (customMovementLayers[gridLayer] == null || !customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info))) continue; var grid = GetGrid(new CPos(gridX, gridY, gridLayer), mapBounds); for (var y = gridY; y < grid.BottomRight.Y; y++) { for (var x = gridX; x < grid.BottomRight.X; x++) { var cell = new CPos(x, y, gridLayer); if (CellIsAccessible(cell)) accessibleCells.Add(cell); } } static CPos AbstractCellForLocalCells(List cells, byte layer) { var minX = int.MaxValue; var minY = int.MaxValue; var maxX = int.MinValue; var maxY = int.MinValue; foreach (var cell in cells) { minX = Math.Min(cell.X, minX); minY = Math.Min(cell.Y, minY); maxX = Math.Max(cell.X, maxX); maxY = Math.Max(cell.Y, maxY); } var regionWidth = maxX - minX; var regionHeight = maxY - minY; var desired = new CPos(minX + regionWidth / 2, minY + regionHeight / 2, layer); // Make sure the abstract cell is one of the available local cells. // This ensures each abstract cell we generate is unique. // We'll choose an abstract node as close to the middle of the region as possible. var abstractCell = desired; var distance = int.MaxValue; foreach (var cell in cells) { var newDistance = (cell - desired).LengthSquared; if (distance > newDistance) { distance = newDistance; abstractCell = cell; } } return abstractCell; } // Flood fill the search area from one of the accessible cells. // We can use this to determine the connected regions within the grid. // Each region we discover will be represented by an abstract node. // Repeat this process until all disjoint regions are discovered. var hasPopulatedAbstractCellForLayer = false; while (accessibleCells.Count > 0) { var src = accessibleCells.First(); using (var search = GetLocalPathSearch( null, new[] { src }, src, customCost, null, BlockedByActor.None, false, grid, 100)) { var localCellsInRegion = search.ExpandAll(); var abstractCell = AbstractCellForLocalCells(localCellsInRegion, gridLayer); accessibleCells.ExceptWith(localCellsInRegion); // PERF: If there is only one distinct region of cells, // we can represent this grid with one abstract cell. // We don't need to remember how to map back from a local cell to an abstract cell. if (!hasPopulatedAbstractCellForLayer && accessibleCells.Count == 0) singleAbstractCellForLayer[gridLayer] = abstractCell; else { // When there is more than one region within the grid // (imagine a wall or stream separating the grid into disjoint areas) // then we will remember a mapping from local cells to each of their abstract cells. hasPopulatedAbstractCellForLayer = true; foreach (var localCell in localCellsInRegion) localCellToAbstractCell.Add(localCell, abstractCell); } } } } return new GridInfo(singleAbstractCellForLayer, localCellToAbstractCell); } /// /// Builds the abstract graph in entirety. The abstract graph contains edges between all the abstract nodes /// that represent the costs to move between them. /// void BuildCostTable() { abstractGraph = new Dictionary>(gridXs * gridYs); var customMovementLayers = world.GetCustomMovementLayers(); for (var gridX = mapBounds.TopLeft.X; gridX < mapBounds.BottomRight.X; gridX += GridSize) for (var gridY = mapBounds.TopLeft.Y; gridY < mapBounds.BottomRight.Y; gridY += GridSize) foreach (var edges in GetAbstractEdgesForGrid(gridX, gridY, customMovementLayers)) abstractGraph.Add(edges.Key, edges.Value); } /// /// For a given grid, determines the edges between the abstract nodes within the grid and the abstract nodes /// within adjacent grids on the same layer. Also determines any edges available to grids on other layers via /// custom movement layers. /// IEnumerable>> GetAbstractEdgesForGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLayers) { var abstractEdges = new HashSet<(CPos Src, CPos Dst)>(); for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++) { if (gridLayer != 0 && (customMovementLayers[gridLayer] == null || !customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info))) continue; void AddEdgesIfMovementAllowedBetweenCells(CPos cell, CPos candidateCell) { if (!MovementAllowedBetweenCells(cell, candidateCell)) return; var abstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell); if (abstractCell == null) return; var abstractCellAdjacent = AbstractCellForLocalCellNoAccessibleCheck(candidateCell); if (abstractCellAdjacent == null) return; abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value)); } // Searches along edges of all grids within a layer. // Checks for the local edge cell if we can traverse to any of the three adjacent cells in the next grid. // Builds connections in the abstract graph when any local cells have connections. void AddAbstractEdges(int xIncrement, int yIncrement, CVec adjacentVec, int2 offset) { var startY = gridY + offset.Y; var startX = gridX + offset.X; for (var y = startY; y < startY + GridSize; y += yIncrement) { for (var x = startX; x < startX + GridSize; x += xIncrement) { var cell = new CPos(x, y, gridLayer); if (!CellIsAccessible(cell)) continue; var adjacentCell = cell + adjacentVec; for (var i = -1; i <= 1; i++) { var candidateCell = adjacentCell + i * new CVec(adjacentVec.Y, adjacentVec.X); AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell); } } } } // Searches all cells within a layer. // Checks for the local cell if we can traverse from/to a custom movement layer. // Builds connections in the abstract graph when any local cells have connections. void AddAbstractCustomLayerEdges() { var gridCml = customMovementLayers[gridLayer]; for (byte candidateLayer = 0; candidateLayer < customMovementLayers.Length; candidateLayer++) { if (gridLayer == candidateLayer) continue; var candidateCml = customMovementLayers[candidateLayer]; if (candidateLayer != 0 && (candidateCml == null || !candidateCml.EnabledForLocomotor(locomotor.Info))) continue; for (var y = gridY; y < gridY + GridSize; y++) { for (var x = gridX; x < gridX + GridSize; x++) { var cell = new CPos(x, y, gridLayer); if (!CellIsAccessible(cell)) continue; CPos candidateCell; if (gridLayer == 0) { candidateCell = new CPos(cell.X, cell.Y, candidateLayer); if (candidateCml.EntryMovementCost(locomotor.Info, candidateCell) == PathGraph.MovementCostForUnreachableCell) continue; } else { candidateCell = new CPos(cell.X, cell.Y, 0); if (gridCml.ExitMovementCost(locomotor.Info, candidateCell) == PathGraph.MovementCostForUnreachableCell) continue; } AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell); } } } } // Top, Left, Bottom, Right AddAbstractEdges(1, GridSize, new CVec(0, -1), new int2(0, 0)); AddAbstractEdges(GridSize, 1, new CVec(-1, 0), new int2(0, 0)); AddAbstractEdges(1, GridSize, new CVec(0, 1), new int2(0, GridSize - 1)); AddAbstractEdges(GridSize, 1, new CVec(1, 0), new int2(GridSize - 1, 0)); AddAbstractCustomLayerEdges(); } return abstractEdges .GroupBy(edge => edge.Src) .Select(group => KeyValuePair.Create( group.Key, group.Select(edge => new GraphConnection(edge.Dst, costEstimator(edge.Src, edge.Dst))).ToList())); } /// /// When reachability changes for a cell, marks the grid it belongs to as out of date. /// void RequireCostRefreshInCell(CPos cell, short oldCost, short newCost) { // We don't care about the specific cost of the cell, just whether it is reachable or not. // This is good because updating the table is expensive, so only having to update it when // the reachability changes rather than for all costs changes saves us a lot of time. if (oldCost == PathGraph.MovementCostForUnreachableCell ^ newCost == PathGraph.MovementCostForUnreachableCell) dirtyGridIndexes.Add(GridIndex(cell)); } bool CellIsAccessible(CPos cell) { return locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell && (cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(cell)); } bool MovementAllowedBetweenCells(CPos accessibleSrcCell, CPos destCell) { return locomotor.MovementCostToEnterCell( null, accessibleSrcCell, destCell, BlockedByActor.None, null) != PathGraph.MovementCostForUnreachableCell && (cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(destCell)); } /// /// When actors change for a cell, marks the grid it belongs to as out of date. /// void RequireBlockingRefreshInCell(CPos cell) { var cellHasBlockingActor = false; var actors = actorMap.GetActorsAt(cell); foreach (var actor in actors) { if (ActorIsBlocking(actor) && ActorCellIsBlocking(actor, cell)) { cellHasBlockingActor = true; break; } } if (cellHasBlockingActor) { if (cellsWithBlockingActor.Add(cell)) dirtyGridIndexes.Add(GridIndex(cell)); } else { if (cellsWithBlockingActor.Remove(cell)) dirtyGridIndexes.Add(GridIndex(cell)); } } /// /// When map projection changes for a cell, marks the grid it belongs to as out of date. /// void RequireProjectionRefreshInCell(CPos cell) { dirtyGridIndexes.Add(GridIndex(cell)); } /// /// defines immovability based on the mobile trait. The blocking rules /// in allow units /// to pass these immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor. /// Since our abstract graph must work for any actor, we have to be conservative and can only consider a subset /// of the immovable actors in the graph - ones we know cannot be passed by some actors due to these rules. /// Both this and must be true for a cell to be blocked. /// /// This method is dependant on the logic in /// and /// . This method must be kept in sync with changes in the locomotor /// rules. /// bool ActorIsBlocking(Actor actor) { var isMovable = actor.OccupiesSpace is Mobile mobile && !mobile.IsTraitDisabled && !mobile.IsTraitPaused && !mobile.IsImmovable; if (isMovable) return false; var isTemporaryBlocker = world.RulesContainTemporaryBlocker && actor.TraitOrDefault() != null; if (isTemporaryBlocker) return false; var crushables = actor.TraitsImplementing(); foreach (var crushable in crushables) if (world.NoPlayersMask != crushable.CrushableBy(actor, locomotor.Info.Crushes)) return false; return true; } /// /// The blocking rules additionally allow some cells to be considered passable even if the actor is blocking. /// A cell is passable if the locomotor can share the cell and a subcell is available. It is also passable if /// it is a transit only cell of a . We cannot consider these cells to be blocked. /// Both this and must be true for a cell to be blocked. /// bool ActorCellIsBlocking(Actor actor, CPos cell) { var canShareCell = locomotor.Info.SharesCell && actorMap.HasFreeSubCell(cell); if (canShareCell) return false; var isTransitOnly = actor.OccupiesSpace is Building building && building.TransitOnlyCells().Contains(cell); if (isTransitOnly) return false; return true; } int GridIndex(CPos cellInGrid) { return (cellInGrid.Y - mapBounds.TopLeft.Y) / GridSize * gridXs + (cellInGrid.X - mapBounds.TopLeft.X) / GridSize; } CPos GetGridTopLeft(int gridIndex, byte layer) { return new CPos( gridIndex % gridXs * GridSize + mapBounds.TopLeft.X, gridIndex / gridXs * GridSize + mapBounds.TopLeft.Y, layer); } static CPos GetGridTopLeft(CPos cellInGrid, Grid mapBounds) { return new CPos( (cellInGrid.X - mapBounds.TopLeft.X) / GridSize * GridSize + mapBounds.TopLeft.X, (cellInGrid.Y - mapBounds.TopLeft.Y) / GridSize * GridSize + mapBounds.TopLeft.Y, cellInGrid.Layer); } static Grid GetGrid(CPos cellInGrid, Grid mapBounds) { var gridTopLeft = GetGridTopLeft(cellInGrid, mapBounds); var width = Math.Min(mapBounds.BottomRight.X - gridTopLeft.X, GridSize); var height = Math.Min(mapBounds.BottomRight.Y - gridTopLeft.Y, GridSize); return new Grid( gridTopLeft, gridTopLeft + new CVec(width, height), true); } /// /// Calculates a path for the actor from multiple possible sources to target, using a unidirectional search. /// Returned path is *reversed* and given target to source. /// The actor must use the same as this . /// public List FindPath(Actor self, IReadOnlyCollection sources, CPos target, BlockedByActor check, int heuristicWeightPercentage, Func customCost, Actor ignoreActor, bool laneBias, PathFinderOverlay pathFinderOverlay) { if (costEstimator == null) return PathFinder.NoPath; pathFinderOverlay?.NewRecording(self, sources, target); if (!world.Map.Contains(target)) return PathFinder.NoPath; RebuildDirtyGrids(); var targetAbstractCell = AbstractCellForLocalCell(target); if (targetAbstractCell == null) return PathFinder.NoPath; // Unlike the target cell, the source cell is allowed to be an unreachable location. // Instead, what matters is whether any cell adjacent to the source cell can be reached. var sourcesWithReachableNodes = new List<(CPos Source, CPos AdjacentSource)>(sources.Count); var sourceEdges = new List(sources.Count); foreach (var source in sources) { if (!world.Map.Contains(source)) continue; // The source cell is reachable, we can add an edge from there and have no need to check adjacent cells. var sourceAbstractCell = AbstractCellForLocalCell(source); if (sourceAbstractCell != null) { sourcesWithReachableNodes.Add((source, source)); var sourceEdge = EdgeFromLocalToAbstract(source, sourceAbstractCell.Value); if (sourceEdge != null) sourceEdges.Add(sourceEdge.Value); continue; } // If the source cell is unreachable, we must add edges from any adjacent cells that are reachable instead. foreach (var dir in CVec.Directions) { var adjacentSource = source + dir; if (!MovementAllowedBetweenCells(source, adjacentSource)) continue; var adjacentSourceAbstractCell = AbstractCellForLocalCell(adjacentSource); if (adjacentSourceAbstractCell == null) continue; sourcesWithReachableNodes.Add((source, adjacentSource)); var sourceEdge = EdgeFromLocalToAbstract(adjacentSource, adjacentSourceAbstractCell.Value); if (sourceEdge != null) sourceEdges.Add(sourceEdge.Value); } } if (sourcesWithReachableNodes.Count == 0) return PathFinder.NoPath; var targetEdge = EdgeFromLocalToAbstract(target, targetAbstractCell.Value); // The new edges will be treated as bi-directional. var fullGraph = new AbstractGraphWithInsertedEdges(abstractGraph, sourceEdges, targetEdge, costEstimator); // Determine an abstract path to all sources, for use in a unidirectional search. var estimatedSearchSize = (abstractGraph.Count + 2) / 8; using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph( fullGraph.GetConnections, locomotor, target, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self))) { var sourcesWithPathableNodes = new HashSet(sources.Count); List unpathableNodes = null; foreach (var (source, adjacentSource) in sourcesWithReachableNodes) { // Check if we have already found a route to this node before we attempt to expand the search. var sourceStatus = reverseAbstractSearch.Graph[adjacentSource]; if (sourceStatus.Status == CellStatus.Closed) { if (sourceStatus.CostSoFar != PathGraph.PathCostForInvalidPath) sourcesWithPathableNodes.Add(source); else { unpathableNodes ??= new List(); unpathableNodes.Add(adjacentSource); } } else { reverseAbstractSearch.TargetPredicate = cell => cell == adjacentSource; if (reverseAbstractSearch.ExpandToTarget()) sourcesWithPathableNodes.Add(source); else { unpathableNodes ??= new List(); unpathableNodes.Add(adjacentSource); } } } if (sourcesWithPathableNodes.Count == 0) return PathFinder.NoPath; using (var fromSrc = GetLocalPathSearch( self, sourcesWithPathableNodes, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage, heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, sourcesWithPathableNodes, unpathableNodes), recorder: pathFinderOverlay?.RecordLocalEdges(self))) return fromSrc.FindPath(); } } /// /// Calculates a path for the actor from source to target, using a bidirectional search. /// Returned path is *reversed* and given target to source. /// The actor must use the same as this . /// public List FindPath(Actor self, CPos source, CPos target, BlockedByActor check, int heuristicWeightPercentage, Func customCost, Actor ignoreActor, bool laneBias, PathFinderOverlay pathFinderOverlay) { if (costEstimator == null) return PathFinder.NoPath; // If the source and target are close, see if they can be reached locally. // This avoids the cost of an abstract search unless we need one. const int CloseGridDistance = 2; if ((target - source).LengthSquared < GridSize * GridSize * CloseGridDistance * CloseGridDistance && source.Layer == target.Layer) { var gridToSearch = new Grid( new CPos( Math.Min(source.X, target.X) - GridSize / 2, Math.Min(source.Y, target.Y) - GridSize / 2, source.Layer), new CPos( Math.Max(source.X, target.X) + GridSize / 2, Math.Max(source.Y, target.Y) + GridSize / 2, source.Layer), false); pathFinderOverlay?.NewRecording(self, new[] { source }, target); List localPath; using (var search = GetLocalPathSearch( self, new[] { source }, target, customCost, ignoreActor, check, laneBias, gridToSearch, heuristicWeightPercentage, recorder: pathFinderOverlay?.RecordLocalEdges(self))) localPath = search.FindPath(); if (localPath.Count > 0) return localPath; } pathFinderOverlay?.NewRecording(self, new[] { source }, target); RebuildDirtyGrids(); // If the target cell is unreachable, there is no path. var targetAbstractCell = AbstractCellForLocalCell(target); if (targetAbstractCell == null) return PathFinder.NoPath; // If the source cell is unreachable, there may still be a path. // As long as one of the cells adjacent to the source is reachable, the path can be made. // Call the other overload which can handle this scenario. var sourceAbstractCell = AbstractCellForLocalCell(source); if (sourceAbstractCell == null) return FindPath(self, new[] { source }, target, check, heuristicWeightPercentage, customCost, ignoreActor, laneBias, pathFinderOverlay); var targetEdge = EdgeFromLocalToAbstract(target, targetAbstractCell.Value); var sourceEdge = EdgeFromLocalToAbstract(source, sourceAbstractCell.Value); // The new edges will be treated as bi-directional. var fullGraph = new AbstractGraphWithInsertedEdges( abstractGraph, sourceEdge != null ? new[] { sourceEdge.Value } : Array.Empty(), targetEdge, costEstimator); // Determine an abstract path in both directions, for use in a bidirectional search. var estimatedSearchSize = (abstractGraph.Count + 2) / 8; using (var forwardAbstractSearch = PathSearch.ToTargetCellOverGraph( fullGraph.GetConnections, locomotor, source, target, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self))) { if (!forwardAbstractSearch.ExpandToTarget()) return PathFinder.NoPath; using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph( fullGraph.GetConnections, locomotor, target, source, estimatedSearchSize, pathFinderOverlay?.RecordAbstractEdges(self))) { reverseAbstractSearch.ExpandToTarget(); using (var fromSrc = GetLocalPathSearch( self, new[] { source }, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage, heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize, null, null), recorder: pathFinderOverlay?.RecordLocalEdges(self))) using (var fromDest = GetLocalPathSearch( self, new[] { target }, source, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage, heuristic: Heuristic(forwardAbstractSearch, estimatedSearchSize, null, null), recorder: pathFinderOverlay?.RecordLocalEdges(self), inReverse: true)) return PathSearch.FindBidiPath(fromDest, fromSrc); } } } /// /// Determines if a path exists between source and target. /// When was given, only terrain is taken into account, /// i.e. as if was used when finding a path. /// When was given, a subset of immovable actors are also taken into /// account. If the method returns false, there is definitely no path. If it returns true there could be a /// path, but it is possible that there is no path because of an immovable actor that does not belong to the /// subset of actors that can be accounted for. So be careful. /// This would apply for any actor using the same as this . /// public bool PathExists(CPos source, CPos target) { if (costEstimator == null) return false; if (!world.Map.Contains(source) || !world.Map.Contains(target)) return false; RebuildDomains(); var abstractTarget = AbstractCellForLocalCell(target); if (abstractTarget == null) return false; var targetDomain = abstractDomains[abstractTarget.Value]; // The source cell is reachable, we can compare the domains directly. var abstractSource = AbstractCellForLocalCell(source); if (abstractSource != null) { var sourceDomain = abstractDomains[abstractSource.Value]; return sourceDomain == targetDomain; } // Unlike the target cell, the source cell is allowed to be an unreachable location. // Instead, what matters is whether any cell adjacent to the source cell can be reached. // So we need to compare the domains of reachable cells adjacent to the source location. foreach (var dir in CVec.Directions) { var adjacentSource = source + dir; if (!MovementAllowedBetweenCells(source, adjacentSource)) continue; var abstractAdjacentSource = AbstractCellForLocalCell(adjacentSource); if (abstractAdjacentSource == null) continue; var adjacentSourceDomain = abstractDomains[abstractAdjacentSource.Value]; if (adjacentSourceDomain == targetDomain) return true; } return false; } /// /// The abstract graph can become out of date when reachability costs for terrain change. /// When this occurs, we must rebuild any affected parts of the abstract graph so it remains correct. /// void RebuildDirtyGrids() { if (dirtyGridIndexes.Count == 0) return; // An empty domain indicates it is out of date and will require rebuilding when next accessed. abstractDomains.Clear(); var customMovementLayers = world.GetCustomMovementLayers(); foreach (var gridIndex in dirtyGridIndexes) { var oldGrid = gridInfos[gridIndex]; var gridTopLeft = GetGridTopLeft(gridIndex, 0); gridInfos[gridIndex] = BuildGrid(gridTopLeft.X, gridTopLeft.Y, customMovementLayers); RebuildCostTable(gridTopLeft.X, gridTopLeft.Y, oldGrid, customMovementLayers); } dirtyGridIndexes.Clear(); } /// /// Updates the abstract graph to account for changes in a specific grid. Any nodes and edges related to that /// grid will be removed, new nodes and edges will be determined and then inserted into the graph. /// void RebuildCostTable(int gridX, int gridY, GridInfo oldGrid, ICustomMovementLayer[] customMovementLayers) { // For this grid, it is possible the abstract nodes have changed. // Remove the old abstract nodes for this grid. // This is important as GetAbstractEdgesForGrid will look at the *current* grids. // So it won't be aware of any nodes that disappeared before we updated the grid. var abstractNodes = new HashSet(); oldGrid.CopyAbstractCellsInto(abstractNodes); foreach (var oldAbstractNode in abstractNodes) abstractGraph.Remove(oldAbstractNode); abstractNodes.Clear(); // Add new abstract edges for this grid, since we cleared out the old nodes everything should be new. foreach (var edges in GetAbstractEdgesForGrid(gridX, gridY, customMovementLayers)) abstractGraph.Add(edges.Key, edges.Value); foreach (var direction in CVec.Directions) { var adjacentGrid = new CPos(gridX, gridY) + GridSize * direction; if (!mapBounds.Contains(adjacentGrid)) continue; // For all adjacent grids, their abstract nodes will not have changed, but the connections may have done. // Update the connections, and keep track of which nodes we have updated. gridInfos[GridIndex(adjacentGrid)].CopyAbstractCellsInto(abstractNodes); foreach (var edges in GetAbstractEdgesForGrid(adjacentGrid.X, adjacentGrid.Y, customMovementLayers)) { abstractGraph[edges.Key] = edges.Value; abstractNodes.Remove(edges.Key); } // If any nodes were left over they have no connections now, so we can remove them from the graph. foreach (var unconnectedNode in abstractNodes) abstractGraph.Remove(unconnectedNode); abstractNodes.Clear(); } } /// /// The abstract domains can become out of date when the abstract graph changes. /// When this occurs, we must rebuild the domain cache. /// void RebuildDomains() { // First, rebuild the abstract graph if it is out of date. RebuildDirtyGrids(); // Check if our domain cache is empty, if so this indicates it is out-of-date and needs rebuilding. if (abstractDomains.Count != 0) return; List AbstractEdge(CPos abstractCell) { if (abstractGraph.TryGetValue(abstractCell, out var abstractEdge)) return abstractEdge; return null; } // As in BuildGrid, flood fill the search graph until all disjoint domains are discovered. var domain = 0u; var abstractCells = new HashSet(abstractGraph.Count); foreach (var grid in gridInfos) grid.CopyAbstractCellsInto(abstractCells); while (abstractCells.Count > 0) { var searchCell = abstractCells.First(); var search = PathSearch.ToTargetCellOverGraph( AbstractEdge, locomotor, searchCell, searchCell, abstractGraph.Count / 8); var searched = search.ExpandAll(); foreach (var abstractCell in searched) abstractDomains.Add(abstractCell, domain); abstractCells.ExceptWith(searched); domain++; } } /// /// Maps a local cell to a abstract node in the graph. Returns null when the local cell is unreachable. /// The cell must have been checked to be on the map with . /// CPos? AbstractCellForLocalCell(CPos localCell) { return gridInfos[GridIndex(localCell)].AbstractCellForLocalCell(localCell, this); } /// /// Maps a local cell to a abstract node in the graph. Returns null when the local cell is unreachable. /// Skips the check, if it has already been performed. /// If an accessible check has not been performed, call instead. /// CPos? AbstractCellForLocalCellNoAccessibleCheck(CPos localCell) { return gridInfos[GridIndex(localCell)].AbstractCellForLocalCell(localCell, null); } /// /// Creates a from the to the . /// Return null when no edge is required, because the cells match. /// GraphEdge? EdgeFromLocalToAbstract(CPos localCell, CPos abstractCell) { if (localCell == abstractCell) return null; return new GraphEdge(localCell, abstractCell, costEstimator(localCell, abstractCell)); } /// /// Uses the provided abstract search to provide an estimate of the distance remaining to the target /// (the heuristic) for a local path search. The abstract search must run in the opposite direction to the /// local search. So when searching from source to target, the abstract search must be from target to source. /// Func Heuristic(PathSearch abstractSearch, int estimatedSearchSize, HashSet sources, List unpathableNodes) { var nodeForCostLookup = new Dictionary(estimatedSearchSize); var graph = (SparsePathGraph)abstractSearch.Graph; return (cell, knownAccessible) => { // When dealing with an unreachable source cell, the path search will check adjacent locations. // These cells may be reachable, but may represent jumping into an area cut off from the target. // Searching on the abstract graph would fail to provide a route in this scenario, so bail early. if (unpathableNodes != null && unpathableNodes.Contains(cell)) return PathGraph.PathCostForInvalidPath; // During a search, all other cells searched by the heuristic are guaranteed to be reachable. // So we don't need to handle an abstract cell lookup failing, or the search failing to expand. // Cells added as initial starting points for the search might not be reachable, // so for those we need to perform accessibility checks. // If the exceptions here do fire, they indicate a bug. The abstract graph is considering a cell to be // unreachable, but the local pathfinder thinks it is reachable. We must fix the abstract graph to also // consider the cell to be reachable. CPos? maybeAbstractCell; if (knownAccessible) maybeAbstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell); else maybeAbstractCell = AbstractCellForLocalCell(cell); if (maybeAbstractCell == null) { // If the source cell is unreachable, use one of the adjacent reachable cells instead. if (sources != null && sources.Contains(cell)) { foreach (var dir in CVec.Directions) { var adjacentCell = cell + dir; if (!MovementAllowedBetweenCells(cell, adjacentCell) || (unpathableNodes != null && unpathableNodes.Contains(adjacentCell))) continue; // Ideally we'd choose the cheapest cell rather than just any one of them, // but we're lazy and this is an edge case. maybeAbstractCell = AbstractCellForLocalCell(adjacentCell); if (maybeAbstractCell != null) break; } } if (maybeAbstractCell == null) throw new Exception( "The abstract path should never be searched for an unreachable point. " + $"Cell {cell} failed lookup for an abstract cell."); } var abstractCell = maybeAbstractCell.Value; var info = graph[abstractCell]; // Expand the abstract search only if we have yet to get a route to the abstract cell. if (info.Status != CellStatus.Closed) { abstractSearch.TargetPredicate = c => c == abstractCell; if (!abstractSearch.ExpandToTarget()) throw new Exception( "The abstract path should never be searched for an unreachable point. " + $"Abstract cell {abstractCell} failed to route to abstract cell."); info = graph[abstractCell]; } var abstractNode = info.PreviousNode; // When transitioning between layers, the XY will be the same and only the layer changes. // If we have transitioned layers then we need the next node // along otherwise we're measuring from our current location. if (abstractCell.Layer != abstractNode.Layer) abstractNode = graph[abstractNode].PreviousNode; // Now we have an abstract node to target, determine if there is one further along the path we can use. // This will provide a better estimate. Cache these results as they are expensive to calculate. if (!nodeForCostLookup.TryGetValue(abstractNode, out var abstractNodeForCost)) { abstractNodeForCost = AbstractNodeForCost(graph, abstractCell, abstractNode); nodeForCostLookup.Add(abstractNode, abstractNodeForCost); } var cost = graph[abstractNodeForCost].CostSoFar + costEstimator(cell, abstractNodeForCost); return cost; }; } /// /// Determines an abstract node further along the path which can be reached directly without deviating from the /// abstract path from the abstract cell of the source location. /// As this node can be reached directly we can target it instead /// of the original node to provide a better cost estimate. /// CPos AbstractNodeForCost(SparsePathGraph graph, CPos abstractCell, CPos abstractNode) { // We currently have the next abstract node along our path. // This means we can create a distance estimate for our current // cell to this next abstract node, plus then the cost provided by the abstract path. // | AC --- AN ---------------------------- D | // This means the lowest cost estimate follows the abstract path. // The abstract path becomes a sort of highway, units want to // move to it as soon as possible and follow it. // This causes issues when a unit can move in a straight line to the destination. // Instead of moving in a straight line, they move to join the highway, travel the highway, // then leave the highway at the other end. // We can combat this by delaying them from joining the highway until the abstract paths turns. // | AC ----------------- AN -------------- D | // This means the unit will move in a direct path whilst it can, and only "join the highway" // when the highway is going to navigate it around an obstacle. // When the heuristic weight is >100%, this greatly improves the resulting path. // As when the weight is higher we may never get a chance to check for the shorter direct path. var abstractNodesAlongPath = new List(); while (true) { var previousAbstractNode = graph[abstractNode].PreviousNode; // The whole abstract path has been travelled, can't go further. if (previousAbstractNode == abstractNode) break; // Check if we can move directly to the new node whilst staying // within the boundary of the abstract path so far. var intersectsAllNodes = true; abstractNodesAlongPath.Add(abstractNode); foreach (var node in abstractNodesAlongPath) { if (!GetGrid(node, mapBounds).IntersectsLine(abstractCell, previousAbstractNode)) { intersectsAllNodes = false; break; } } if (!intersectsAllNodes) break; abstractNode = previousAbstractNode; } return abstractNode; } PathSearch GetLocalPathSearch( Actor self, IEnumerable srcs, CPos dst, Func customCost, Actor ignoreActor, BlockedByActor check, bool laneBias, Grid? grid, int heuristicWeightPercentage, Func heuristic = null, bool inReverse = false, PathSearch.IRecorder recorder = null) { return PathSearch.ToTargetCell( world, locomotor, self, srcs, dst, check, heuristicWeightPercentage, customCost, ignoreActor, laneBias, inReverse, heuristic, grid, recorder); } } }