Add a hierarchical path finder to improve pathfinding performance.
Replaces the existing bi-directional search between points used by the pathfinder with a guided hierarchical search. The old search was a standard A* search with a heuristic of advancing in straight line towards the target. This heuristic performs well if a mostly direct path to the target exists, it performs poorly it the path has to navigate around blockages in the terrain. The hierarchical path finder maintains a simplified, abstract graph. When a path search is performed it uses this abstract graph to inform the heuristic. Instead of moving blindly towards the target, it will instead steer around major obstacles, almost as if it had been provided a map which ensures it can move in roughly the right direction. This allows it to explore less of the area overall, improving performance. When a path needs to steer around terrain on the map, the hierarchical path finder is able to greatly improve on the previous performance. When a path is able to proceed in a straight line, no performance benefit will be seen. If the path needs to steer around actors on the map instead of terrain (e.g. trees, buildings, units) then the same poor pathfinding performance as before will be observed.
This commit is contained in:
committed by
Matthias Mailänder
parent
cea9ceb72e
commit
5a8f91aa21
@@ -151,6 +151,7 @@ namespace OpenRA
|
||||
public class Map : IReadOnlyFileSystem
|
||||
{
|
||||
public const int SupportedMapFormat = 11;
|
||||
const short InvalidCachedTerrainIndex = -1;
|
||||
|
||||
/// <summary>Defines the order of the fields in map.yaml</summary>
|
||||
static readonly MapField[] YamlFields =
|
||||
@@ -448,6 +449,19 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
AllEdgeCells = UpdateEdgeCells();
|
||||
|
||||
// Invalidate the entry for a cell if anything could cause the terrain index to change.
|
||||
Action<CPos> invalidateTerrainIndex = c =>
|
||||
{
|
||||
if (cachedTerrainIndexes != null)
|
||||
cachedTerrainIndexes[c] = InvalidCachedTerrainIndex;
|
||||
};
|
||||
|
||||
// Even though the cache is lazily initialized, we must attach these event handlers on init.
|
||||
// This ensures our handler to invalidate the cache runs first,
|
||||
// so other listeners to these same events will get correct data when calling GetTerrainIndex.
|
||||
CustomTerrain.CellEntryChanged += invalidateTerrainIndex;
|
||||
Tiles.CellEntryChanged += invalidateTerrainIndex;
|
||||
}
|
||||
|
||||
void UpdateRamp(CPos cell)
|
||||
@@ -1069,18 +1083,11 @@ namespace OpenRA
|
||||
|
||||
public byte GetTerrainIndex(CPos cell)
|
||||
{
|
||||
const short InvalidCachedTerrainIndex = -1;
|
||||
|
||||
// Lazily initialize a cache for terrain indexes.
|
||||
if (cachedTerrainIndexes == null)
|
||||
{
|
||||
cachedTerrainIndexes = new CellLayer<short>(this);
|
||||
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
|
||||
|
||||
// Invalidate the entry for a cell if anything could cause the terrain index to change.
|
||||
Action<CPos> invalidateTerrainIndex = c => cachedTerrainIndexes[c] = InvalidCachedTerrainIndex;
|
||||
CustomTerrain.CellEntryChanged += invalidateTerrainIndex;
|
||||
Tiles.CellEntryChanged += invalidateTerrainIndex;
|
||||
}
|
||||
|
||||
var uv = cell.ToMPos(this);
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var harvPos = self.CenterPosition;
|
||||
|
||||
// Find any harvestable resources:
|
||||
var path = mobile.PathFinder.FindUnitPathToTargetCellByPredicate(
|
||||
var path = mobile.PathFinder.FindPathToTargetCellByPredicate(
|
||||
self,
|
||||
new[] { searchFromLoc, self.Location },
|
||||
loc =>
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
getPath = check =>
|
||||
{
|
||||
return mobile.PathFinder.FindUnitPathToTargetCell(
|
||||
return mobile.PathFinder.FindPathToTargetCell(
|
||||
self, new[] { mobile.ToCell }, destination, check, laneBias: false);
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
if (!this.destination.HasValue)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
return mobile.PathFinder.FindUnitPathToTargetCell(
|
||||
return mobile.PathFinder.FindPathToTargetCell(
|
||||
self, new[] { mobile.ToCell }, this.destination.Value, check, ignoreActor: ignoreActor);
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
if (searchCells.Count == 0)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
var path = Mobile.PathFinder.FindUnitPathToTargetCell(self, searchCells, loc, check);
|
||||
var path = Mobile.PathFinder.FindPathToTargetCell(self, searchCells, loc, check);
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
/// allowable to be returned in a <see cref="GraphConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="neighbor">The candidate cell. This might not lie within map bounds.</param>
|
||||
protected virtual bool NeighborAllowable(CPos neighbor)
|
||||
protected virtual bool IsValidNeighbor(CPos neighbor)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
var dir = directions[i];
|
||||
var neighbor = position + dir;
|
||||
if (!NeighborAllowable(neighbor))
|
||||
if (!IsValidNeighbor(neighbor))
|
||||
continue;
|
||||
|
||||
var pathCost = GetPathCostToNode(position, neighbor, dir);
|
||||
@@ -142,7 +142,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
continue;
|
||||
|
||||
var layerPosition = new CPos(position.X, position.Y, cml.Index);
|
||||
if (!NeighborAllowable(layerPosition))
|
||||
if (!IsValidNeighbor(layerPosition))
|
||||
continue;
|
||||
|
||||
var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition);
|
||||
@@ -155,7 +155,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
else
|
||||
{
|
||||
var groundPosition = new CPos(position.X, position.Y, 0);
|
||||
if (NeighborAllowable(groundPosition))
|
||||
if (IsValidNeighbor(groundPosition))
|
||||
{
|
||||
var exitCost = CustomMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
|
||||
if (exitCost != PathGraph.MovementCostForUnreachableCell &&
|
||||
|
||||
94
OpenRA.Mods.Common/Pathfinder/Grid.cs
Normal file
94
OpenRA.Mods.Common/Pathfinder/Grid.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a simplistic grid of cells, where everything in the
|
||||
/// top-to-bottom and left-to-right range is within the grid.
|
||||
/// The grid can be restricted to a single layer, or allowed to span all layers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This means in <see cref="MapGridType.RectangularIsometric"/> some cells within a grid may lay off the map.
|
||||
/// Contrast this with <see cref="CellRegion"/> which maintains the simplistic grid in map space -
|
||||
/// ensuring the cells are therefore always within the map area.
|
||||
/// The advantage of Grid is that it has straight edges, making logic for adjacent grids easy.
|
||||
/// A CellRegion has jagged edges in RectangularIsometric, which makes that more difficult.
|
||||
/// </remarks>
|
||||
public readonly struct Grid
|
||||
{
|
||||
/// <summary>
|
||||
/// Inclusive.
|
||||
/// </summary>
|
||||
public readonly CPos TopLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Exclusive.
|
||||
/// </summary>
|
||||
public readonly CPos BottomRight;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the grid spans only the single layer given by the cells. When false, it spans all layers.
|
||||
/// </summary>
|
||||
public readonly bool SingleLayer;
|
||||
|
||||
public Grid(CPos topLeft, CPos bottomRight, bool singleLayer)
|
||||
{
|
||||
if (topLeft.Layer != bottomRight.Layer)
|
||||
throw new ArgumentException($"{nameof(topLeft)} and {nameof(bottomRight)} must have the same {nameof(CPos.Layer)}");
|
||||
|
||||
TopLeft = topLeft;
|
||||
BottomRight = bottomRight;
|
||||
SingleLayer = singleLayer;
|
||||
}
|
||||
|
||||
public int Width => BottomRight.X - TopLeft.X;
|
||||
public int Height => BottomRight.Y - TopLeft.Y;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the cell X and Y lie within the grid bounds. The cell layer must also match.
|
||||
/// </summary>
|
||||
public bool Contains(CPos cell)
|
||||
{
|
||||
return
|
||||
cell.X >= TopLeft.X && cell.X < BottomRight.X &&
|
||||
cell.Y >= TopLeft.Y && cell.Y < BottomRight.Y &&
|
||||
(!SingleLayer || cell.Layer == TopLeft.Layer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the line segment from <paramref name="start"/> to <paramref name="end"/>
|
||||
/// passes through the grid boundary. The cell layers are ignored.
|
||||
/// A line contained wholly within the grid that doesn't cross the boundary is not counted as intersecting.
|
||||
/// </summary>
|
||||
public bool IntersectsLine(CPos start, CPos end)
|
||||
{
|
||||
var s = new int2(start.X, start.Y);
|
||||
var e = new int2(end.X, end.Y);
|
||||
var tl = new int2(TopLeft.X, TopLeft.Y);
|
||||
var tr = new int2(BottomRight.X, TopLeft.Y);
|
||||
var bl = new int2(TopLeft.X, BottomRight.Y);
|
||||
var br = new int2(BottomRight.X, BottomRight.Y);
|
||||
return
|
||||
Exts.LinesIntersect(s, e, tl, tr) ||
|
||||
Exts.LinesIntersect(s, e, tl, bl) ||
|
||||
Exts.LinesIntersect(s, e, bl, br) ||
|
||||
Exts.LinesIntersect(s, e, tr, br);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{TopLeft}->{BottomRight}";
|
||||
}
|
||||
}
|
||||
}
|
||||
54
OpenRA.Mods.Common/Pathfinder/GridPathGraph.cs
Normal file
54
OpenRA.Mods.Common/Pathfinder/GridPathGraph.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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 OpenRA.Mods.Common.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// A dense pathfinding graph that supports a search over all cells within a <see cref="Grid"/>.
|
||||
/// Cells outside the grid area are deemed unreachable and will not be considered.
|
||||
/// It implements the ability to cost and get connections for cells, and supports <see cref="ICustomMovementLayer"/>.
|
||||
/// </summary>
|
||||
sealed class GridPathGraph : DensePathGraph
|
||||
{
|
||||
readonly CellInfo[] infos;
|
||||
readonly Grid grid;
|
||||
|
||||
public GridPathGraph(Locomotor locomotor, Actor actor, World world, BlockedByActor check,
|
||||
Func<CPos, int> customCost, Actor ignoreActor, bool laneBias, bool inReverse, Grid grid)
|
||||
: base(locomotor, actor, world, check, customCost, ignoreActor, laneBias, inReverse)
|
||||
{
|
||||
infos = new CellInfo[grid.Width * grid.Height];
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
protected override bool IsValidNeighbor(CPos neighbor)
|
||||
{
|
||||
// Enforce that we only search within the grid bounds.
|
||||
return grid.Contains(neighbor);
|
||||
}
|
||||
|
||||
int InfoIndex(CPos pos)
|
||||
{
|
||||
return
|
||||
(pos.Y - grid.TopLeft.Y) * grid.Width +
|
||||
(pos.X - grid.TopLeft.X);
|
||||
}
|
||||
|
||||
public override CellInfo this[CPos pos]
|
||||
{
|
||||
get => infos[InfoIndex(pos)];
|
||||
set => infos[InfoIndex(pos)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
904
OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs
Normal file
904
OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs
Normal file
@@ -0,0 +1,904 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides pathfinding abilities for actors that use a specific <see cref="Locomotor"/>.
|
||||
/// Maintains a hierarchy of abstract graphs that provide a more accurate heuristic function during
|
||||
/// A* pathfinding than the one available from <see cref="PathSearch.DefaultCostEstimator(Locomotor)"/>.
|
||||
/// This allows for faster pathfinding.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The goal of this pathfinder is to increase performance of path searches. <see cref="PathSearch"/> 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.</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
///
|
||||
/// <para><see cref="PathSearch.DefaultCostEstimator(Locomotor)"/> 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.</para>
|
||||
///
|
||||
/// <para>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.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>The <see cref="HierarchicalPathFinder"/> 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.</para>
|
||||
///
|
||||
/// <para>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).</para>
|
||||
///
|
||||
/// <para>When a path search is to be performed, we first perform a A* search on the abstract graph with the
|
||||
/// <see cref="PathSearch.DefaultCostEstimator(Locomotor)"/>. 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.</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
///
|
||||
/// <para>This implementation is aware of movement costs over terrain given by
|
||||
/// <see cref="Locomotor.MovementCostToEnterCell"/>. 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
|
||||
/// <see cref="BlockedByActor.None"/> had been specified. It is not aware of actors on the map. So blocking actors
|
||||
/// will not be accounted for in the heuristic.</para>
|
||||
///
|
||||
/// <para>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 a blocking actor (trees, walls, buildings, units) 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
|
||||
/// been walled off. 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.</para>
|
||||
///
|
||||
/// <para>In summary, the <see cref="HierarchicalPathFinder"/> reduces the performance impact of path searches that
|
||||
/// must go around terrain, but does not improve performance of searches that must go around actors.</para>
|
||||
/// </remarks>
|
||||
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 Func<CPos, CPos, int> costEstimator;
|
||||
readonly HashSet<int> dirtyGridIndexes = new HashSet<int>();
|
||||
Grid mapBounds;
|
||||
int gridXs;
|
||||
int gridYs;
|
||||
|
||||
/// <summary>
|
||||
/// Index by a <see cref="GridIndex"/>.
|
||||
/// </summary>
|
||||
GridInfo[] gridInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Dictionary<CPos, List<GraphConnection>> abstractGraph;
|
||||
|
||||
/// <summary>
|
||||
/// Knows about the abstract nodes within a grid. Can map a local cell to its abstract node.
|
||||
/// </summary>
|
||||
readonly struct GridInfo
|
||||
{
|
||||
readonly CPos?[] singleAbstractCellForLayer;
|
||||
readonly Dictionary<CPos, CPos> localCellToAbstractCell;
|
||||
|
||||
public GridInfo(CPos?[] singleAbstractCellForLayer, Dictionary<CPos, CPos> localCellToAbstractCell)
|
||||
{
|
||||
this.singleAbstractCellForLayer = singleAbstractCellForLayer;
|
||||
this.localCellToAbstractCell = localCellToAbstractCell;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a local cell to a abstract node in the graph.
|
||||
/// Returns null when the local cell is unreachable.
|
||||
/// </summary>
|
||||
public CPos? AbstractCellForLocalCell(CPos localCell)
|
||||
{
|
||||
var abstractCell = singleAbstractCellForLayer[localCell.Layer];
|
||||
if (abstractCell != null)
|
||||
return abstractCell;
|
||||
if (localCellToAbstractCell.TryGetValue(localCell, out var abstractCellFromMap))
|
||||
return abstractCellFromMap;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void CopyAbstractCellsInto(HashSet<CPos> set)
|
||||
{
|
||||
foreach (var single in singleAbstractCellForLayer)
|
||||
if (single != null)
|
||||
set.Add(single.Value);
|
||||
foreach (var cell in localCellToAbstractCell.Values)
|
||||
set.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
sealed class AbstractGraphWithInsertedEdges
|
||||
{
|
||||
readonly Dictionary<CPos, List<GraphConnection>> abstractEdges;
|
||||
readonly Dictionary<CPos, IEnumerable<GraphConnection>> changedEdges;
|
||||
|
||||
public AbstractGraphWithInsertedEdges(
|
||||
Dictionary<CPos, List<GraphConnection>> abstractEdges,
|
||||
IList<GraphEdge> sourceEdges,
|
||||
GraphEdge? targetEdge,
|
||||
Func<CPos, CPos, int> costEstimator)
|
||||
{
|
||||
this.abstractEdges = abstractEdges;
|
||||
changedEdges = new Dictionary<CPos, IEnumerable<GraphConnection>>(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<CPos, CPos, int> costEstimator)
|
||||
{
|
||||
InsertConnections(edge.Source, edge.Destination, costEstimator);
|
||||
}
|
||||
|
||||
void InsertConnections(CPos localCell, CPos abstractCell, Func<CPos, CPos, int> costEstimator)
|
||||
{
|
||||
if (!abstractEdges.TryGetValue(abstractCell, out var edges))
|
||||
edges = new List<GraphConnection>();
|
||||
changedEdges[localCell] = edges
|
||||
.Select(e => new GraphConnection(e.Destination, costEstimator(localCell, e.Destination)))
|
||||
.Append(new GraphConnection(abstractCell, costEstimator(localCell, abstractCell)));
|
||||
|
||||
IEnumerable<GraphConnection> 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<GraphConnection> 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<GraphConnection> 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<GraphConnection>();
|
||||
}
|
||||
}
|
||||
|
||||
public HierarchicalPathFinder(World world, Locomotor locomotor)
|
||||
{
|
||||
this.world = world;
|
||||
this.locomotor = locomotor;
|
||||
if (locomotor.Info.TerrainSpeeds.Count == 0)
|
||||
return;
|
||||
|
||||
costEstimator = PathSearch.DefaultCostEstimator(locomotor);
|
||||
|
||||
BuildGrids();
|
||||
BuildCostTable();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<CPos, List<GraphConnection>> GetOverlayData()
|
||||
{
|
||||
if (costEstimator == null)
|
||||
return default;
|
||||
|
||||
// Ensure the abstract graph is up to date when using the overlay.
|
||||
RebuildDirtyGrids();
|
||||
return new ReadOnlyDictionary<CPos, List<GraphConnection>>(abstractGraph);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the map area up into a series of grids.
|
||||
/// </summary>
|
||||
void BuildGrids()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
GridInfo BuildGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLayers)
|
||||
{
|
||||
var singleAbstractCellForLayer = new CPos?[customMovementLayers.Length];
|
||||
var localCellToAbstractCell = new Dictionary<CPos, CPos>();
|
||||
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);
|
||||
var accessibleCells = new HashSet<CPos>();
|
||||
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 (locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell)
|
||||
accessibleCells.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
CPos AbstractCellForLocalCells(List<CPos> 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, null, 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the abstract graph in entirety. The abstract graph contains edges between all the abstract nodes
|
||||
/// that represent the costs to move between them.
|
||||
/// </summary>
|
||||
void BuildCostTable()
|
||||
{
|
||||
abstractGraph = new Dictionary<CPos, List<GraphConnection>>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
IEnumerable<KeyValuePair<CPos, List<GraphConnection>>> 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;
|
||||
|
||||
// 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 (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell)
|
||||
continue;
|
||||
|
||||
var adjacentCell = cell + adjacentVec;
|
||||
for (var i = -1; i <= 1; i++)
|
||||
{
|
||||
var candidateCell = adjacentCell + i * new CVec(adjacentVec.Y, adjacentVec.X);
|
||||
if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) !=
|
||||
PathGraph.MovementCostForUnreachableCell)
|
||||
{
|
||||
var gridInfo = gridInfos[GridIndex(cell)];
|
||||
var abstractCell = gridInfo.AbstractCellForLocalCell(cell);
|
||||
if (abstractCell == null)
|
||||
continue;
|
||||
|
||||
var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)];
|
||||
var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell);
|
||||
if (abstractCellAdjacent == null)
|
||||
continue;
|
||||
|
||||
abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell)
|
||||
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;
|
||||
}
|
||||
|
||||
if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) ==
|
||||
PathGraph.MovementCostForUnreachableCell)
|
||||
continue;
|
||||
|
||||
var gridInfo = gridInfos[GridIndex(cell)];
|
||||
var abstractCell = gridInfo.AbstractCellForLocalCell(cell);
|
||||
if (abstractCell == null)
|
||||
continue;
|
||||
|
||||
var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)];
|
||||
var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell);
|
||||
if (abstractCellAdjacent == null)
|
||||
continue;
|
||||
|
||||
abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When reachability changes for a cell, marks the grid it belongs to as out of date.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
|
||||
/// </summary>
|
||||
public List<CPos> FindPath(Actor self, IReadOnlyCollection<CPos> sources, CPos target,
|
||||
BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
|
||||
Actor ignoreActor, bool laneBias)
|
||||
{
|
||||
if (costEstimator == null)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
RebuildDirtyGrids();
|
||||
|
||||
var targetAbstractCell = AbstractCellForLocalCell(target);
|
||||
if (targetAbstractCell == null)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
var sourcesWithReachableNodes = new List<CPos>(sources.Count);
|
||||
var sourceEdges = new List<GraphEdge>(sources.Count);
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var sourceAbstractCell = AbstractCellForLocalCell(source);
|
||||
if (sourceAbstractCell == null)
|
||||
continue;
|
||||
|
||||
sourcesWithReachableNodes.Add(source);
|
||||
var sourceEdge = EdgeFromLocalToAbstract(source, sourceAbstractCell.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))
|
||||
{
|
||||
var sourcesWithPathableNodes = new List<CPos>(sourcesWithReachableNodes.Count);
|
||||
foreach (var source in sourcesWithReachableNodes)
|
||||
{
|
||||
reverseAbstractSearch.TargetPredicate = cell => cell == source;
|
||||
if (reverseAbstractSearch.ExpandToTarget())
|
||||
sourcesWithPathableNodes.Add(source);
|
||||
}
|
||||
|
||||
if (sourcesWithPathableNodes.Count == 0)
|
||||
return PathFinder.NoPath;
|
||||
|
||||
using (var fromSrc = GetLocalPathSearch(
|
||||
self, sourcesWithPathableNodes, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize)))
|
||||
return fromSrc.FindPath();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
|
||||
/// </summary>
|
||||
public List<CPos> FindPath(Actor self, CPos source, CPos target,
|
||||
BlockedByActor check, int heuristicWeightPercentage, Func<CPos, int> customCost,
|
||||
Actor ignoreActor, bool laneBias)
|
||||
{
|
||||
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);
|
||||
|
||||
List<CPos> localPath;
|
||||
using (var search = GetLocalPathSearch(
|
||||
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, gridToSearch, heuristicWeightPercentage))
|
||||
localPath = search.FindPath();
|
||||
|
||||
if (localPath.Count > 0)
|
||||
return localPath;
|
||||
}
|
||||
|
||||
RebuildDirtyGrids();
|
||||
|
||||
var targetAbstractCell = AbstractCellForLocalCell(target);
|
||||
if (targetAbstractCell == null)
|
||||
return PathFinder.NoPath;
|
||||
var sourceAbstractCell = AbstractCellForLocalCell(source);
|
||||
if (sourceAbstractCell == null)
|
||||
return PathFinder.NoPath;
|
||||
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<GraphEdge>(), 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))
|
||||
{
|
||||
if (!forwardAbstractSearch.ExpandToTarget())
|
||||
return PathFinder.NoPath;
|
||||
|
||||
using (var reverseAbstractSearch = PathSearch.ToTargetCellOverGraph(
|
||||
fullGraph.GetConnections, locomotor, target, source, estimatedSearchSize))
|
||||
{
|
||||
reverseAbstractSearch.ExpandToTarget();
|
||||
|
||||
using (var fromSrc = GetLocalPathSearch(
|
||||
self, new[] { source }, target, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(reverseAbstractSearch, estimatedSearchSize)))
|
||||
using (var fromDest = GetLocalPathSearch(
|
||||
self, new[] { target }, source, customCost, ignoreActor, check, laneBias, null, heuristicWeightPercentage,
|
||||
heuristic: Heuristic(forwardAbstractSearch, estimatedSearchSize),
|
||||
inReverse: true))
|
||||
return PathSearch.FindBidiPath(fromDest, fromSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
void RebuildDirtyGrids()
|
||||
{
|
||||
if (dirtyGridIndexes.Count == 0)
|
||||
return;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<CPos>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a local cell to a abstract node in the graph.
|
||||
/// Returns null when the local cell is unreachable.
|
||||
/// </summary>
|
||||
CPos? AbstractCellForLocalCell(CPos localCell) =>
|
||||
gridInfos[GridIndex(localCell)].AbstractCellForLocalCell(localCell);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="GraphEdge"/> from the <paramref name="localCell"/> to the <paramref name="abstractCell"/>.
|
||||
/// Return null when no edge is required, because the cells match.
|
||||
/// </summary>
|
||||
GraphEdge? EdgeFromLocalToAbstract(CPos localCell, CPos abstractCell)
|
||||
{
|
||||
if (localCell == abstractCell)
|
||||
return null;
|
||||
|
||||
return new GraphEdge(localCell, abstractCell, costEstimator(localCell, abstractCell));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Func<CPos, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize)
|
||||
{
|
||||
var nodeForCostLookup = new Dictionary<CPos, CPos>(estimatedSearchSize);
|
||||
var graph = (SparsePathGraph)abstractSearch.Graph;
|
||||
return cell =>
|
||||
{
|
||||
// All 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 are filtered out if they aren't reachable.
|
||||
// The search only explores accessible cells from then on.
|
||||
var gridInfo = gridInfos[GridIndex(cell)];
|
||||
var abstractCell = gridInfo.AbstractCellForLocalCell(cell).Value;
|
||||
var info = graph[abstractCell];
|
||||
|
||||
// Expand the abstract search if we have not visited the abstract cell.
|
||||
if (info.Status == CellStatus.Unvisited)
|
||||
{
|
||||
abstractSearch.TargetPredicate = c => c == abstractCell;
|
||||
if (!abstractSearch.ExpandToTarget())
|
||||
throw new Exception("The abstract path should never be searched for an unreachable point.");
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<CPos>();
|
||||
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<CPos> srcs, CPos dst, Func<CPos, int> customCost,
|
||||
Actor ignoreActor, BlockedByActor check, bool laneBias, Grid? grid, int heuristicWeightPercentage,
|
||||
Func<CPos, int> heuristic = null,
|
||||
bool inReverse = false)
|
||||
{
|
||||
return PathSearch.ToTargetCell(
|
||||
world, locomotor, self, srcs, dst, check, heuristicWeightPercentage,
|
||||
customCost, ignoreActor, laneBias, inReverse, heuristic, grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
/// <summary>
|
||||
/// Given a source node, returns connections to all reachable destination nodes with their cost.
|
||||
/// </summary>
|
||||
/// <remarks>PERF: Returns a <see cref="List{T}"/> rather than an <see cref="IEnumerable{T}"/> as enumerating
|
||||
/// this efficiently is important for pathfinding performance. Callers should interact with this as an
|
||||
/// <see cref="IEnumerable{T}"/> and not mutate the result.</remarks>
|
||||
List<GraphConnection> GetConnections(CPos source);
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +41,37 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
public const short MovementCostForUnreachableCell = short.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a full edge in a graph, giving the cost to traverse between two nodes.
|
||||
/// </summary>
|
||||
public readonly struct GraphEdge
|
||||
{
|
||||
public readonly CPos Source;
|
||||
public readonly CPos Destination;
|
||||
public readonly int Cost;
|
||||
|
||||
public GraphEdge(CPos source, CPos destination, int cost)
|
||||
{
|
||||
if (source == destination)
|
||||
throw new ArgumentException($"{nameof(source)} and {nameof(destination)} must refer to different cells");
|
||||
if (cost < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be negative");
|
||||
if (cost == PathGraph.PathCostForInvalidPath)
|
||||
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be used for an unreachable path");
|
||||
|
||||
Source = source;
|
||||
Destination = destination;
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public GraphConnection ToConnection()
|
||||
{
|
||||
return new GraphConnection(Destination, Cost);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Source} -> {Destination} = {Cost}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents part of an edge in a graph, giving the cost to traverse to a node.
|
||||
/// </summary>
|
||||
@@ -69,6 +103,11 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public GraphEdge ToEdge(CPos source)
|
||||
{
|
||||
return new GraphEdge(source, Destination, Cost);
|
||||
}
|
||||
|
||||
public override string ToString() => $"-> {Destination} = {Cost}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,6 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public sealed class PathSearch : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// When searching for paths, use a default weight of 125% to reduce
|
||||
/// computation effort - even if this means paths may be sub-optimal.
|
||||
/// </summary>
|
||||
public const int DefaultHeuristicWeightPercentage = 125;
|
||||
|
||||
// PERF: Maintain a pool of layers used for paths searches for each world. These searches are performed often
|
||||
// so we wish to avoid the high cost of initializing a new search space every time by reusing the old ones.
|
||||
static readonly ConditionalWeakTable<World, CellInfoLayerPool> LayerPoolTable = new ConditionalWeakTable<World, CellInfoLayerPool>();
|
||||
@@ -37,13 +31,14 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCellByPredicate(
|
||||
World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
||||
World world, Locomotor locomotor, Actor self,
|
||||
IEnumerable<CPos> froms, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
bool laneBias = true)
|
||||
{
|
||||
var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false);
|
||||
var search = new PathSearch(graph, loc => 0, DefaultHeuristicWeightPercentage, targetPredicate);
|
||||
var search = new PathSearch(graph, loc => 0, 0, targetPredicate);
|
||||
|
||||
foreach (var sl in froms)
|
||||
if (world.Map.Contains(sl))
|
||||
@@ -53,15 +48,20 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCell(
|
||||
World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms, CPos target, BlockedByActor check,
|
||||
World world, Locomotor locomotor, Actor self,
|
||||
IEnumerable<CPos> froms, CPos target, BlockedByActor check, int heuristicWeightPercentage,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
bool laneBias = true,
|
||||
bool inReverse = false,
|
||||
Func<CPos, int> heuristic = null,
|
||||
int heuristicWeightPercentage = DefaultHeuristicWeightPercentage)
|
||||
Grid? grid = null)
|
||||
{
|
||||
var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse);
|
||||
IPathGraph graph;
|
||||
if (grid != null)
|
||||
graph = new GridPathGraph(locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse, grid.Value);
|
||||
else
|
||||
graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, inReverse);
|
||||
|
||||
heuristic = heuristic ?? DefaultCostEstimator(locomotor, target);
|
||||
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target);
|
||||
@@ -73,6 +73,18 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
return search;
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCellOverGraph(
|
||||
Func<CPos, List<GraphConnection>> edges, Locomotor locomotor, CPos from, CPos target,
|
||||
int estimatedSearchSize = 0)
|
||||
{
|
||||
var graph = new SparsePathGraph(edges, estimatedSearchSize);
|
||||
var search = new PathSearch(graph, DefaultCostEstimator(locomotor, target), 100, loc => loc == target);
|
||||
|
||||
search.AddInitialCell(from);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default: Diagonal distance heuristic. More information:
|
||||
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
||||
@@ -113,9 +125,9 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
}
|
||||
|
||||
public IPathGraph Graph { get; }
|
||||
public Func<CPos, bool> TargetPredicate { get; set; }
|
||||
readonly Func<CPos, int> heuristic;
|
||||
readonly int heuristicWeightPercentage;
|
||||
readonly Func<CPos, bool> targetPredicate;
|
||||
readonly IPriorityQueue<GraphConnection> openQueue;
|
||||
|
||||
/// <summary>
|
||||
@@ -137,7 +149,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
Graph = graph;
|
||||
this.heuristic = heuristic;
|
||||
this.heuristicWeightPercentage = heuristicWeightPercentage;
|
||||
this.targetPredicate = targetPredicate;
|
||||
TargetPredicate = targetPredicate;
|
||||
openQueue = new PriorityQueue<GraphConnection>(GraphConnection.ConnectionCostComparer);
|
||||
}
|
||||
|
||||
@@ -215,6 +227,30 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
return currentMinNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the path search until a path is found, and returns whether a path is found successfully.
|
||||
/// </summary>
|
||||
public bool ExpandToTarget()
|
||||
{
|
||||
while (CanExpand())
|
||||
if (TargetPredicate(Expand()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the path search over the whole search space.
|
||||
/// Returns the cells that were visited during the search.
|
||||
/// </summary>
|
||||
public List<CPos> ExpandAll()
|
||||
{
|
||||
var consideredCells = new List<CPos>();
|
||||
while (CanExpand())
|
||||
consideredCells.Add(Expand());
|
||||
return consideredCells;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the path search until a path is found, and returns that path.
|
||||
/// Returned path is *reversed* and given target to source.
|
||||
@@ -224,7 +260,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
while (CanExpand())
|
||||
{
|
||||
var p = Expand();
|
||||
if (targetPredicate(p))
|
||||
if (TargetPredicate(p))
|
||||
return MakePath(Graph, p);
|
||||
}
|
||||
|
||||
|
||||
53
OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs
Normal file
53
OpenRA.Mods.Common/Pathfinder/SparsePathGraph.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// A sparse pathfinding graph that supports a search over provided cells.
|
||||
/// This is a classic graph that supports an arbitrary graph of nodes and edges,
|
||||
/// and does not require a dense grid of cells.
|
||||
/// Costs and any desired connections to a <see cref="Traits.ICustomMovementLayer"/>
|
||||
/// must be provided as input.
|
||||
/// </summary>
|
||||
sealed class SparsePathGraph : IPathGraph
|
||||
{
|
||||
readonly Func<CPos, List<GraphConnection>> edges;
|
||||
readonly Dictionary<CPos, CellInfo> info;
|
||||
|
||||
public SparsePathGraph(Func<CPos, List<GraphConnection>> edges, int estimatedSearchSize = 0)
|
||||
{
|
||||
this.edges = edges;
|
||||
info = new Dictionary<CPos, CellInfo>(estimatedSearchSize);
|
||||
}
|
||||
|
||||
public List<GraphConnection> GetConnections(CPos position)
|
||||
{
|
||||
return edges(position) ?? new List<GraphConnection>();
|
||||
}
|
||||
|
||||
public CellInfo this[CPos pos]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (info.TryGetValue(pos, out var cellInfo))
|
||||
return cellInfo;
|
||||
return default;
|
||||
}
|
||||
set => info[pos] = value;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
harv.Harvester.CanHarvestCell(cell) &&
|
||||
claimLayer.CanClaimCell(actor, cell);
|
||||
|
||||
var path = harv.Mobile.PathFinder.FindUnitPathToTargetCellByPredicate(
|
||||
var path = harv.Mobile.PathFinder.FindPathToTargetCellByPredicate(
|
||||
actor, new[] { actor.Location }, isValidResource, BlockedByActor.Stationary,
|
||||
loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), Info.HarvesterEnemyAvoidanceRadius)
|
||||
.Where(u => !u.IsDead && actor.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy)
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}).ToLookup(r => r.Location);
|
||||
|
||||
// Start a search from each refinery's delivery location:
|
||||
var path = mobile.PathFinder.FindUnitPathToTargetCell(
|
||||
var path = mobile.PathFinder.FindPathToTargetCell(
|
||||
self, refineries.Select(r => r.Key), self.Location, BlockedByActor.None,
|
||||
location =>
|
||||
{
|
||||
|
||||
@@ -825,7 +825,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
if (CanEnterCell(above))
|
||||
return above;
|
||||
|
||||
var path = PathFinder.FindUnitPathToTargetCellByPredicate(
|
||||
var path = PathFinder.FindPathToTargetCellByPredicate(
|
||||
self, new[] { self.Location }, loc => loc.Layer == 0 && CanEnterCell(loc), BlockedByActor.All);
|
||||
|
||||
if (path.Count > 0)
|
||||
|
||||
137
OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs
Normal file
137
OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Commands;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.World)]
|
||||
[Desc("Renders a debug overlay showing the abstract graph of the hierarchical pathfinder. Attach this to the world actor.")]
|
||||
public class HierarchicalPathFinderOverlayInfo : TraitInfo, Requires<PathFinderInfo>
|
||||
{
|
||||
public readonly string Font = "TinyBold";
|
||||
public readonly Color GroundLayerColor = Color.DarkOrange;
|
||||
public readonly Color CustomLayerColor = Color.Blue;
|
||||
public readonly Color GroundToCustomLayerColor = Color.Purple;
|
||||
public readonly Color AbstractNodeColor = Color.Red;
|
||||
|
||||
public override object Create(ActorInitializer init) { return new HierarchicalPathFinderOverlay(this); }
|
||||
}
|
||||
|
||||
public class HierarchicalPathFinderOverlay : IRenderAnnotations, IWorldLoaded, IChatCommand
|
||||
{
|
||||
const string CommandName = "hpf";
|
||||
const string CommandDesc = "toggles the hierarchical pathfinder overlay.";
|
||||
|
||||
readonly HierarchicalPathFinderOverlayInfo info;
|
||||
readonly SpriteFont font;
|
||||
|
||||
public bool Enabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Locomotor selected in the UI which the overlay will display.
|
||||
/// If null, will show the overlays for the currently selected units.
|
||||
/// </summary>
|
||||
public Locomotor Locomotor { get; set; }
|
||||
|
||||
public HierarchicalPathFinderOverlay(HierarchicalPathFinderOverlayInfo info)
|
||||
{
|
||||
this.info = info;
|
||||
font = Game.Renderer.Fonts[info.Font];
|
||||
}
|
||||
|
||||
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
var console = w.WorldActor.TraitOrDefault<ChatCommands>();
|
||||
var help = w.WorldActor.TraitOrDefault<HelpCommand>();
|
||||
|
||||
if (console == null || help == null)
|
||||
return;
|
||||
|
||||
console.RegisterCommand(CommandName, this);
|
||||
help.RegisterHelp(CommandName, CommandDesc);
|
||||
}
|
||||
|
||||
void IChatCommand.InvokeCommand(string name, string arg)
|
||||
{
|
||||
if (name == CommandName)
|
||||
Enabled ^= true;
|
||||
}
|
||||
|
||||
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr)
|
||||
{
|
||||
if (!Enabled)
|
||||
yield break;
|
||||
|
||||
var pathFinder = self.Trait<PathFinder>();
|
||||
var visibleRegion = wr.Viewport.AllVisibleCells;
|
||||
var locomotors = Locomotor == null
|
||||
? self.World.Selection.Actors
|
||||
.Where(a => !a.Disposed)
|
||||
.Select(a => a.TraitOrDefault<Mobile>()?.Locomotor)
|
||||
.Where(l => l != null)
|
||||
.Distinct()
|
||||
: new[] { Locomotor };
|
||||
foreach (var locomotor in locomotors)
|
||||
{
|
||||
var abstractGraph = pathFinder.GetOverlayDataForLocomotor(locomotor);
|
||||
|
||||
// Locomotor doesn't allow movement, nothing to display.
|
||||
if (abstractGraph == null)
|
||||
continue;
|
||||
|
||||
foreach (var connectionsFromOneNode in abstractGraph)
|
||||
{
|
||||
var nodeCell = connectionsFromOneNode.Key;
|
||||
var srcUv = (PPos)nodeCell.ToMPos(self.World.Map);
|
||||
foreach (var cost in connectionsFromOneNode.Value)
|
||||
{
|
||||
var destUv = (PPos)cost.Destination.ToMPos(self.World.Map);
|
||||
if (!visibleRegion.Contains(destUv) && !visibleRegion.Contains(srcUv))
|
||||
continue;
|
||||
|
||||
var connection = new WPos[]
|
||||
{
|
||||
self.World.Map.CenterOfSubCell(cost.Destination, SubCell.FullCell),
|
||||
self.World.Map.CenterOfSubCell(nodeCell, SubCell.FullCell),
|
||||
};
|
||||
|
||||
// Connections on the ground layer given in ground color.
|
||||
// Connections on any custom layers given in custom color.
|
||||
// Connections that allow a transition between layers given in transition color.
|
||||
Color lineColor;
|
||||
if (nodeCell.Layer == 0 && cost.Destination.Layer == 0)
|
||||
lineColor = info.GroundLayerColor;
|
||||
else if (nodeCell.Layer == cost.Destination.Layer)
|
||||
lineColor = info.CustomLayerColor;
|
||||
else
|
||||
lineColor = info.GroundToCustomLayerColor;
|
||||
yield return new TargetLineRenderable(connection, lineColor, 1, 2);
|
||||
|
||||
var centerCell = new CPos(
|
||||
(cost.Destination.X + nodeCell.X) / 2,
|
||||
(cost.Destination.Y + nodeCell.Y) / 2);
|
||||
var centerPos = self.World.Map.CenterOfSubCell(centerCell, SubCell.FullCell);
|
||||
yield return new TextAnnotationRenderable(font, centerPos, 0, lineColor, cost.Cost.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IRenderAnnotations.SpatiallyPartitionable => false;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -23,19 +24,39 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
public override object Create(ActorInitializer init)
|
||||
{
|
||||
return new PathFinder(init.World);
|
||||
return new PathFinder(init.Self);
|
||||
}
|
||||
}
|
||||
|
||||
public class PathFinder : IPathFinder
|
||||
public class PathFinder : IPathFinder, IWorldLoaded
|
||||
{
|
||||
public static readonly List<CPos> NoPath = new List<CPos>(0);
|
||||
|
||||
readonly World world;
|
||||
/// <summary>
|
||||
/// When searching for paths, use a default weight of 125% to reduce
|
||||
/// computation effort - even if this means paths may be sub-optimal.
|
||||
/// </summary>
|
||||
const int DefaultHeuristicWeightPercentage = 125;
|
||||
|
||||
public PathFinder(World world)
|
||||
readonly World world;
|
||||
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersByLocomotor;
|
||||
|
||||
public PathFinder(Actor self)
|
||||
{
|
||||
this.world = world;
|
||||
world = self.World;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<CPos, List<GraphConnection>> GetOverlayDataForLocomotor(Locomotor locomotor)
|
||||
{
|
||||
return hierarchicalPathFindersByLocomotor[locomotor].GetOverlayData();
|
||||
}
|
||||
|
||||
public void WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
// Requires<LocomotorInfo> ensures all Locomotors have been initialized.
|
||||
hierarchicalPathFindersByLocomotor = w.WorldActor.TraitsImplementing<Locomotor>().ToDictionary(
|
||||
locomotor => locomotor,
|
||||
locomotor => new HierarchicalPathFinder(world, locomotor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,7 +69,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
/// as optimizations are possible for the single source case. Use searches from multiple source cells
|
||||
/// sparingly.
|
||||
/// </remarks>
|
||||
public List<CPos> FindUnitPathToTargetCell(
|
||||
public List<CPos> FindPathToTargetCell(
|
||||
Actor self, IEnumerable<CPos> sources, CPos target, BlockedByActor check,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
@@ -58,13 +79,13 @@ namespace OpenRA.Mods.Common.Traits
|
||||
if (sourcesList.Count == 0)
|
||||
return NoPath;
|
||||
|
||||
var locomotor = GetLocomotor(self);
|
||||
var locomotor = GetActorLocomotor(self);
|
||||
|
||||
// If the target cell is inaccessible, bail early.
|
||||
var inaccessible =
|
||||
!world.Map.Contains(target) ||
|
||||
!locomotor.CanMoveFreelyInto(self, target, check, ignoreActor) ||
|
||||
(!(customCost is null) && customCost(target) == PathGraph.PathCostForInvalidPath);
|
||||
(customCost != null && customCost(target) == PathGraph.PathCostForInvalidPath);
|
||||
if (inaccessible)
|
||||
return NoPath;
|
||||
|
||||
@@ -81,18 +102,14 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return new List<CPos>(2) { target, source };
|
||||
}
|
||||
|
||||
// With one starting point, we can use a bidirectional search.
|
||||
using (var fromTarget = PathSearch.ToTargetCell(
|
||||
world, locomotor, self, new[] { target }, source, check, ignoreActor: ignoreActor))
|
||||
using (var fromSource = PathSearch.ToTargetCell(
|
||||
world, locomotor, self, new[] { source }, target, check, ignoreActor: ignoreActor, inReverse: true))
|
||||
return PathSearch.FindBidiPath(fromTarget, fromSource);
|
||||
// Use a hierarchical path search, which performs a guided bidirectional search.
|
||||
return hierarchicalPathFindersByLocomotor[locomotor].FindPath(
|
||||
self, source, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias);
|
||||
}
|
||||
|
||||
// With multiple starting points, we can only use a unidirectional search.
|
||||
using (var search = PathSearch.ToTargetCell(
|
||||
world, locomotor, self, sourcesList, target, check, customCost, ignoreActor, laneBias))
|
||||
return search.FindPath();
|
||||
// Use a hierarchical path search, which performs a guided unidirectional search.
|
||||
return hierarchicalPathFindersByLocomotor[locomotor].FindPath(
|
||||
self, sourcesList, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,10 +118,10 @@ namespace OpenRA.Mods.Common.Traits
|
||||
/// The shortest path between a source and a discovered target is returned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Searches with this method are slower than <see cref="FindUnitPathToTargetCell"/> due to the need to search for
|
||||
/// Searches with this method are slower than <see cref="FindPathToTargetCell"/> due to the need to search for
|
||||
/// and discover an acceptable target cell. Use this search sparingly.
|
||||
/// </remarks>
|
||||
public List<CPos> FindUnitPathToTargetCellByPredicate(
|
||||
public List<CPos> FindPathToTargetCellByPredicate(
|
||||
Actor self, IEnumerable<CPos> sources, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
@@ -112,11 +129,11 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
// With no pre-specified target location, we can only use a unidirectional search.
|
||||
using (var search = PathSearch.ToTargetCellByPredicate(
|
||||
world, GetLocomotor(self), self, sources, targetPredicate, check, customCost, ignoreActor, laneBias))
|
||||
world, GetActorLocomotor(self), self, sources, targetPredicate, check, customCost, ignoreActor, laneBias))
|
||||
return search.FindPath();
|
||||
}
|
||||
|
||||
static Locomotor GetLocomotor(Actor self)
|
||||
static Locomotor GetActorLocomotor(Actor self)
|
||||
{
|
||||
// PERF: This PathFinder trait requires the use of Mobile, so we can be sure that is in use.
|
||||
// We can save some performance by avoiding querying for the Locomotor trait and retrieving it from Mobile.
|
||||
|
||||
@@ -799,7 +799,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
/// Returned path is *reversed* and given target to source.
|
||||
/// The shortest path between a source and the target is returned.
|
||||
/// </summary>
|
||||
List<CPos> FindUnitPathToTargetCell(
|
||||
List<CPos> FindPathToTargetCell(
|
||||
Actor self, IEnumerable<CPos> sources, CPos target, BlockedByActor check,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
@@ -810,7 +810,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
/// Returned path is *reversed* and given target to source.
|
||||
/// The shortest path between a source and a discovered target is returned.
|
||||
/// </summary>
|
||||
List<CPos> FindUnitPathToTargetCellByPredicate(
|
||||
List<CPos> FindPathToTargetCellByPredicate(
|
||||
Actor self, IEnumerable<CPos> sources, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
||||
Func<CPos, int> customCost = null,
|
||||
Actor ignoreActor = null,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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.Linq;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
{
|
||||
public class HierarchicalPathFinderOverlayLogic : ChromeLogic
|
||||
{
|
||||
[ObjectCreator.UseCtor]
|
||||
public HierarchicalPathFinderOverlayLogic(Widget widget, World world)
|
||||
{
|
||||
var hpfOverlay = world.WorldActor.Trait<HierarchicalPathFinderOverlay>();
|
||||
widget.IsVisible = () => hpfOverlay.Enabled;
|
||||
|
||||
var locomotors = new Locomotor[] { null }.Concat(
|
||||
world.WorldActor.TraitsImplementing<Locomotor>().OrderBy(l => l.Info.Name))
|
||||
.ToArray();
|
||||
var locomotorSelector = widget.Get<DropDownButtonWidget>("HPF_OVERLAY_LOCOMOTOR");
|
||||
locomotorSelector.OnMouseDown = _ =>
|
||||
{
|
||||
Func<Locomotor, ScrollItemWidget, ScrollItemWidget> setupItem = (option, template) =>
|
||||
{
|
||||
var item = ScrollItemWidget.Setup(
|
||||
template,
|
||||
() => hpfOverlay.Locomotor == option,
|
||||
() => hpfOverlay.Locomotor = option);
|
||||
item.Get<LabelWidget>("LABEL").GetText = () => option?.Info.Name ?? "(Selected Units)";
|
||||
return item;
|
||||
};
|
||||
|
||||
locomotorSelector.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", locomotors.Length * 30, locomotors, setupItem);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1860,6 +1860,18 @@ Container@PLAYER_WIDGETS:
|
||||
Y: 299
|
||||
Width: 194
|
||||
Height: 20
|
||||
Container@HPF_OVERLAY:
|
||||
Logic: HierarchicalPathFinderOverlayLogic
|
||||
X: WINDOW_RIGHT - WIDTH - 240
|
||||
Y: 40
|
||||
Width: 150
|
||||
Height: 25
|
||||
Children:
|
||||
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
|
||||
Width: PARENT_RIGHT
|
||||
Height: PARENT_BOTTOM
|
||||
Text: Select Locomotor
|
||||
Font: Regular
|
||||
|
||||
Background@FMVPLAYER:
|
||||
Width: WINDOW_RIGHT
|
||||
|
||||
@@ -153,6 +153,7 @@ World:
|
||||
ChatCommands:
|
||||
DevCommands:
|
||||
DebugVisualizationCommands:
|
||||
HierarchicalPathFinderOverlay:
|
||||
PlayerCommands:
|
||||
HelpCommand:
|
||||
ScreenShaker:
|
||||
|
||||
@@ -641,3 +641,15 @@ Container@PLAYER_WIDGETS:
|
||||
Y: 5
|
||||
ImageCollection: scrollpanel-decorations
|
||||
ImageName: down
|
||||
Container@HPF_OVERLAY:
|
||||
Logic: HierarchicalPathFinderOverlayLogic
|
||||
X: WINDOW_RIGHT - WIDTH - 231
|
||||
Y: 40
|
||||
Width: 150
|
||||
Height: 25
|
||||
Children:
|
||||
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
|
||||
Width: PARENT_RIGHT
|
||||
Height: PARENT_BOTTOM
|
||||
Text: Select Locomotor
|
||||
Font: Regular
|
||||
|
||||
@@ -120,6 +120,7 @@ World:
|
||||
ChatCommands:
|
||||
DevCommands:
|
||||
DebugVisualizationCommands:
|
||||
HierarchicalPathFinderOverlay:
|
||||
PlayerCommands:
|
||||
HelpCommand:
|
||||
ScreenShaker:
|
||||
|
||||
@@ -649,3 +649,16 @@ Container@PLAYER_WIDGETS:
|
||||
Y: 4
|
||||
ImageCollection: power-icons
|
||||
ImageName: power-normal
|
||||
Container@HPF_OVERLAY:
|
||||
Logic: HierarchicalPathFinderOverlayLogic
|
||||
X: WINDOW_RIGHT - WIDTH - 260
|
||||
Y: 40
|
||||
Width: 150
|
||||
Height: 25
|
||||
Children:
|
||||
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
|
||||
Width: PARENT_RIGHT
|
||||
Height: PARENT_BOTTOM
|
||||
Text: Select Locomotor
|
||||
Font: Regular
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ World:
|
||||
ChatCommands:
|
||||
DevCommands:
|
||||
DebugVisualizationCommands:
|
||||
HierarchicalPathFinderOverlay:
|
||||
PlayerCommands:
|
||||
HelpCommand:
|
||||
ScreenShaker:
|
||||
|
||||
@@ -619,3 +619,15 @@ Container@PLAYER_WIDGETS:
|
||||
Background: scrolldown-buttons
|
||||
TooltipText: Scroll down
|
||||
TooltipContainer: TOOLTIP_CONTAINER
|
||||
Container@HPF_OVERLAY:
|
||||
Logic: HierarchicalPathFinderOverlayLogic
|
||||
X: WINDOW_RIGHT - WIDTH - 245
|
||||
Y: 40
|
||||
Width: 150
|
||||
Height: 25
|
||||
Children:
|
||||
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
|
||||
Width: PARENT_RIGHT
|
||||
Height: PARENT_BOTTOM
|
||||
Text: Select Locomotor
|
||||
Font: Regular
|
||||
|
||||
@@ -235,6 +235,7 @@ World:
|
||||
ChatCommands:
|
||||
DevCommands:
|
||||
DebugVisualizationCommands:
|
||||
HierarchicalPathFinderOverlay:
|
||||
PlayerCommands:
|
||||
HelpCommand:
|
||||
BuildingInfluence:
|
||||
|
||||
Reference in New Issue
Block a user