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); } } }