diff --git a/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs b/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs
new file mode 100644
index 0000000000..7272f7bf4a
--- /dev/null
+++ b/OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs
@@ -0,0 +1,225 @@
+#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.Linq;
+using OpenRA.Mods.Common.Traits;
+
+namespace OpenRA.Mods.Common.Pathfinder
+{
+ ///
+ /// A dense pathfinding graph that implements the ability to cost and get connections for cells,
+ /// and supports . Allows searching over a dense grid of cells.
+ /// Derived classes are required to provide backing storage for the pathfinding information.
+ ///
+ abstract class DensePathGraph : IPathGraph
+ {
+ const int LaneBiasCost = 1;
+
+ protected readonly ICustomMovementLayer[] CustomMovementLayers;
+ readonly int customMovementLayersEnabledForLocomotor;
+ readonly Locomotor locomotor;
+ readonly Actor actor;
+ readonly World world;
+ readonly BlockedByActor check;
+ readonly Func customCost;
+ readonly Actor ignoreActor;
+ readonly bool laneBias;
+ readonly bool inReverse;
+ readonly bool checkTerrainHeight;
+
+ protected DensePathGraph(Locomotor locomotor, Actor actor, World world, BlockedByActor check,
+ Func customCost, Actor ignoreActor, bool laneBias, bool inReverse)
+ {
+ CustomMovementLayers = world.GetCustomMovementLayers();
+ customMovementLayersEnabledForLocomotor = CustomMovementLayers.Count(cml => cml != null && cml.EnabledForLocomotor(locomotor.Info));
+ this.locomotor = locomotor;
+ this.world = world;
+ this.actor = actor;
+ this.check = check;
+ this.customCost = customCost;
+ this.ignoreActor = ignoreActor;
+ this.laneBias = laneBias;
+ this.inReverse = inReverse;
+ checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
+ }
+
+ public abstract CellInfo this[CPos node] { get; set; }
+
+ ///
+ /// Determines if a candidate neighbouring position is
+ /// allowable to be returned in a .
+ ///
+ /// The candidate cell. This might not lie within map bounds.
+ protected virtual bool NeighborAllowable(CPos neighbor)
+ {
+ return true;
+ }
+
+ // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed
+ // to be reached more cheaply by a path through our parent cell which does not include the current cell.
+ // For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set
+ // is the three cells ahead, plus the two cells to the side. Effectively, these are the cells left over
+ // if you ignore the ones reachable from the parent cell.
+ // We can do this because for any cell in range of both the current and parent location,
+ // if we can reach it from one we are guaranteed to be able to reach it from the other.
+ static readonly CVec[][] DirectedNeighbors =
+ {
+ new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // TL
+ new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) }, // T
+ new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // TR
+ new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // L
+ CVec.Directions,
+ new[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // R
+ new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BL
+ new[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // B
+ new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BR
+ };
+
+ // With height discontinuities between the parent and current cell, we cannot optimize the possible neighbors.
+ // It is no longer true that for any cell in range of both the current and parent location,
+ // if we can reach it from one we are guaranteed to be able to reach it from the other.
+ // This is because a height discontinuity may have prevented the parent location from reaching,
+ // but our current cell on a new height may be able to reach as the height difference may be small enough.
+ // Therefore, we can only exclude the parent cell in each set of directions.
+ static readonly CVec[][] DirectedNeighborsConservative =
+ {
+ CVec.Directions.Exclude(new CVec(1, 1)).ToArray(), // TL
+ CVec.Directions.Exclude(new CVec(0, 1)).ToArray(), // T
+ CVec.Directions.Exclude(new CVec(-1, 1)).ToArray(), // TR
+ CVec.Directions.Exclude(new CVec(1, 0)).ToArray(), // L
+ CVec.Directions,
+ CVec.Directions.Exclude(new CVec(-1, 0)).ToArray(), // R
+ CVec.Directions.Exclude(new CVec(1, -1)).ToArray(), // BL
+ CVec.Directions.Exclude(new CVec(0, -1)).ToArray(), // B
+ CVec.Directions.Exclude(new CVec(-1, -1)).ToArray(), // BR
+ };
+
+ public List GetConnections(CPos position)
+ {
+ var layer = position.Layer;
+ var info = this[position];
+ var previousNode = info.PreviousNode;
+
+ var dx = position.X - previousNode.X;
+ var dy = position.Y - previousNode.Y;
+ var index = dy * 3 + dx + 4;
+
+ var heightLayer = world.Map.Height;
+ var directions =
+ (checkTerrainHeight && layer == 0 && previousNode.Layer == 0 && heightLayer[position] != heightLayer[previousNode]
+ ? DirectedNeighborsConservative
+ : DirectedNeighbors)[index];
+
+ var validNeighbors = new List(directions.Length + (layer == 0 ? customMovementLayersEnabledForLocomotor : 1));
+ for (var i = 0; i < directions.Length; i++)
+ {
+ var dir = directions[i];
+ var neighbor = position + dir;
+ if (!NeighborAllowable(neighbor))
+ continue;
+
+ var pathCost = GetPathCostToNode(position, neighbor, dir);
+ if (pathCost != PathGraph.PathCostForInvalidPath &&
+ this[neighbor].Status != CellStatus.Closed)
+ validNeighbors.Add(new GraphConnection(neighbor, pathCost));
+ }
+
+ if (layer == 0)
+ {
+ foreach (var cml in CustomMovementLayers)
+ {
+ if (cml == null || !cml.EnabledForLocomotor(locomotor.Info))
+ continue;
+
+ var layerPosition = new CPos(position.X, position.Y, cml.Index);
+ if (!NeighborAllowable(layerPosition))
+ continue;
+
+ var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition);
+ if (entryCost != PathGraph.MovementCostForUnreachableCell &&
+ CanEnterNode(position, layerPosition) &&
+ this[layerPosition].Status != CellStatus.Closed)
+ validNeighbors.Add(new GraphConnection(layerPosition, entryCost));
+ }
+ }
+ else
+ {
+ var groundPosition = new CPos(position.X, position.Y, 0);
+ if (NeighborAllowable(groundPosition))
+ {
+ var exitCost = CustomMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
+ if (exitCost != PathGraph.MovementCostForUnreachableCell &&
+ CanEnterNode(position, groundPosition) &&
+ this[groundPosition].Status != CellStatus.Closed)
+ validNeighbors.Add(new GraphConnection(groundPosition, exitCost));
+ }
+ }
+
+ return validNeighbors;
+ }
+
+ bool CanEnterNode(CPos srcNode, CPos destNode)
+ {
+ return
+ locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor)
+ != PathGraph.MovementCostForUnreachableCell;
+ }
+
+ int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction)
+ {
+ var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor);
+ if (movementCost != PathGraph.MovementCostForUnreachableCell)
+ return CalculateCellPathCost(destNode, direction, movementCost);
+
+ return PathGraph.PathCostForInvalidPath;
+ }
+
+ int CalculateCellPathCost(CPos neighborCPos, CVec direction, short movementCost)
+ {
+ var cellCost = direction.X * direction.Y != 0
+ ? Exts.MultiplyBySqrtTwo(movementCost)
+ : movementCost;
+
+ if (customCost != null)
+ {
+ var customCellCost = customCost(neighborCPos);
+ if (customCellCost == PathGraph.PathCostForInvalidPath)
+ return PathGraph.PathCostForInvalidPath;
+
+ cellCost += customCellCost;
+ }
+
+ // Directional bonuses for smoother flow!
+ if (laneBias)
+ {
+ var ux = neighborCPos.X + (inReverse ? 1 : 0) & 1;
+ var uy = neighborCPos.Y + (inReverse ? 1 : 0) & 1;
+
+ if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0))
+ cellCost += LaneBiasCost;
+
+ if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0))
+ cellCost += LaneBiasCost;
+ }
+
+ return cellCost;
+ }
+
+ protected virtual void Dispose(bool disposing) { }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs
index c6f4aa883e..e500e65d35 100644
--- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs
+++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs
@@ -1,6 +1,6 @@
#region Copyright & License Information
/*
- * Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
+ * 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
@@ -10,8 +10,6 @@
#endregion
using System;
-using System.Collections.Generic;
-using System.Linq;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Pathfinder
@@ -20,204 +18,42 @@ namespace OpenRA.Mods.Common.Pathfinder
/// A dense pathfinding graph that supports a search over all cells within a map.
/// It implements the ability to cost and get connections for cells, and supports .
///
- sealed class PathGraph : IPathGraph
+ sealed class PathGraph : DensePathGraph
{
public const int PathCostForInvalidPath = int.MaxValue;
public const short MovementCostForUnreachableCell = short.MaxValue;
- const int LaneBiasCost = 1;
- readonly ICustomMovementLayer[] customMovementLayers;
- readonly int customMovementLayersEnabledForLocomotor;
- readonly Locomotor locomotor;
- readonly Actor actor;
- readonly World world;
- readonly BlockedByActor check;
- readonly Func customCost;
- readonly Actor ignoreActor;
- readonly bool laneBias;
- readonly bool inReverse;
- readonly bool checkTerrainHeight;
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer;
readonly CellLayer[] cellInfoForLayer;
public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check,
Func customCost, Actor ignoreActor, bool laneBias, bool inReverse)
+ : base(locomotor, actor, world, check, customCost, ignoreActor, laneBias, inReverse)
{
- customMovementLayers = world.GetCustomMovementLayers();
- customMovementLayersEnabledForLocomotor = customMovementLayers.Count(cml => cml != null && cml.EnabledForLocomotor(locomotor.Info));
- this.locomotor = locomotor;
- this.world = world;
- this.actor = actor;
- this.check = check;
- this.customCost = customCost;
- this.ignoreActor = ignoreActor;
- this.laneBias = laneBias;
- this.inReverse = inReverse;
- checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
-
// As we support a search over the whole map area,
// use the pool to grab the CellInfos we need to track the graph state.
// This allows us to avoid the cost of allocating large arrays constantly.
// PERF: Avoid LINQ
pooledLayer = layerPool.Get();
- cellInfoForLayer = new CellLayer[customMovementLayers.Length];
+ cellInfoForLayer = new CellLayer[CustomMovementLayers.Length];
cellInfoForLayer[0] = pooledLayer.GetLayer();
- foreach (var cml in customMovementLayers)
+ foreach (var cml in CustomMovementLayers)
if (cml != null && cml.EnabledForLocomotor(locomotor.Info))
cellInfoForLayer[cml.Index] = pooledLayer.GetLayer();
}
- // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed
- // to be reached more cheaply by a path through our parent cell which does not include the current cell.
- // For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set
- // is the three cells ahead, plus the two cells to the side. Effectively, these are the cells left over
- // if you ignore the ones reachable from the parent cell.
- // We can do this because for any cell in range of both the current and parent location,
- // if we can reach it from one we are guaranteed to be able to reach it from the other.
- static readonly CVec[][] DirectedNeighbors =
- {
- new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // TL
- new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) }, // T
- new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // TR
- new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // L
- CVec.Directions,
- new[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // R
- new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BL
- new[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // B
- new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BR
- };
-
- // With height discontinuities between the parent and current cell, we cannot optimize the possible neighbors.
- // It is no longer true that for any cell in range of both the current and parent location,
- // if we can reach it from one we are guaranteed to be able to reach it from the other.
- // This is because a height discontinuity may have prevented the parent location from reaching,
- // but our current cell on a new height may be able to reach as the height difference may be small enough.
- // Therefore, we can only exclude the parent cell in each set of directions.
- static readonly CVec[][] DirectedNeighborsConservative =
- {
- CVec.Directions.Exclude(new CVec(1, 1)).ToArray(), // TL
- CVec.Directions.Exclude(new CVec(0, 1)).ToArray(), // T
- CVec.Directions.Exclude(new CVec(-1, 1)).ToArray(), // TR
- CVec.Directions.Exclude(new CVec(1, 0)).ToArray(), // L
- CVec.Directions,
- CVec.Directions.Exclude(new CVec(-1, 0)).ToArray(), // R
- CVec.Directions.Exclude(new CVec(1, -1)).ToArray(), // BL
- CVec.Directions.Exclude(new CVec(0, -1)).ToArray(), // B
- CVec.Directions.Exclude(new CVec(-1, -1)).ToArray(), // BR
- };
-
- public List GetConnections(CPos position)
- {
- var layer = position.Layer;
- var info = this[position];
- var previousNode = info.PreviousNode;
-
- var dx = position.X - previousNode.X;
- var dy = position.Y - previousNode.Y;
- var index = dy * 3 + dx + 4;
-
- var heightLayer = world.Map.Height;
- var directions =
- (checkTerrainHeight && layer == 0 && previousNode.Layer == 0 && heightLayer[position] != heightLayer[previousNode]
- ? DirectedNeighborsConservative
- : DirectedNeighbors)[index];
-
- var validNeighbors = new List(directions.Length + (layer == 0 ? customMovementLayersEnabledForLocomotor : 1));
- for (var i = 0; i < directions.Length; i++)
- {
- var dir = directions[i];
- var neighbor = position + dir;
-
- var pathCost = GetPathCostToNode(position, neighbor, dir);
- if (pathCost != PathCostForInvalidPath &&
- this[neighbor].Status != CellStatus.Closed)
- validNeighbors.Add(new GraphConnection(neighbor, pathCost));
- }
-
- if (layer == 0)
- {
- foreach (var cml in customMovementLayers)
- {
- if (cml == null || !cml.EnabledForLocomotor(locomotor.Info))
- continue;
-
- var layerPosition = new CPos(position.X, position.Y, cml.Index);
- var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition);
- if (entryCost != MovementCostForUnreachableCell &&
- CanEnterNode(position, layerPosition) &&
- this[layerPosition].Status != CellStatus.Closed)
- validNeighbors.Add(new GraphConnection(layerPosition, entryCost));
- }
- }
- else
- {
- var groundPosition = new CPos(position.X, position.Y, 0);
- var exitCost = customMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
- if (exitCost != MovementCostForUnreachableCell &&
- CanEnterNode(position, groundPosition) &&
- this[groundPosition].Status != CellStatus.Closed)
- validNeighbors.Add(new GraphConnection(groundPosition, exitCost));
- }
-
- return validNeighbors;
- }
-
- bool CanEnterNode(CPos srcNode, CPos destNode)
- {
- return
- locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor)
- != MovementCostForUnreachableCell;
- }
-
- int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction)
- {
- var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor);
- if (movementCost != MovementCostForUnreachableCell)
- return CalculateCellPathCost(destNode, direction, movementCost);
-
- return PathCostForInvalidPath;
- }
-
- int CalculateCellPathCost(CPos neighborCPos, CVec direction, short movementCost)
- {
- var cellCost = direction.X * direction.Y != 0
- ? Exts.MultiplyBySqrtTwo(movementCost)
- : movementCost;
-
- if (customCost != null)
- {
- var customCellCost = customCost(neighborCPos);
- if (customCellCost == PathCostForInvalidPath)
- return PathCostForInvalidPath;
-
- cellCost += customCellCost;
- }
-
- // Directional bonuses for smoother flow!
- if (laneBias)
- {
- var ux = neighborCPos.X + (inReverse ? 1 : 0) & 1;
- var uy = neighborCPos.Y + (inReverse ? 1 : 0) & 1;
-
- if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0))
- cellCost += LaneBiasCost;
-
- if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0))
- cellCost += LaneBiasCost;
- }
-
- return cellCost;
- }
-
- public CellInfo this[CPos pos]
+ public override CellInfo this[CPos pos]
{
get => cellInfoForLayer[pos.Layer][pos];
set => cellInfoForLayer[pos.Layer][pos] = value;
}
- public void Dispose()
+ protected override void Dispose(bool disposing)
{
- pooledLayer.Dispose();
+ if (disposing)
+ pooledLayer.Dispose();
+
+ base.Dispose(disposing);
}
}
}