Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
76
OpenRA.Mods.Common/Pathfinder/CellInfo.cs
Normal file
76
OpenRA.Mods.Common/Pathfinder/CellInfo.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the three states that a node in the graph can have.
|
||||
/// Based on A* algorithm specification.
|
||||
/// </summary>
|
||||
public enum CellStatus : byte
|
||||
{
|
||||
Unvisited,
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about nodes in the pathfinding graph.
|
||||
/// The default value of this struct represents an <see cref="CellStatus.Unvisited"/> location.
|
||||
/// </summary>
|
||||
public readonly struct CellInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The status of this node. Accessing other fields is only valid when the status is not <see cref="CellStatus.Unvisited"/>.
|
||||
/// </summary>
|
||||
public readonly CellStatus Status;
|
||||
|
||||
/// <summary>
|
||||
/// The cost to move from the start up to this node.
|
||||
/// </summary>
|
||||
public readonly int CostSoFar;
|
||||
|
||||
/// <summary>
|
||||
/// The estimation of how far this node is from our target.
|
||||
/// </summary>
|
||||
public readonly int EstimatedTotalCost;
|
||||
|
||||
/// <summary>
|
||||
/// The previous node of this one that follows the shortest path.
|
||||
/// </summary>
|
||||
public readonly CPos PreviousNode;
|
||||
|
||||
public CellInfo(CellStatus status, int costSoFar, int estimatedTotalCost, CPos previousNode)
|
||||
{
|
||||
if (status == CellStatus.Unvisited)
|
||||
throw new ArgumentException(
|
||||
$"The default {nameof(CellInfo)} is the only such {nameof(CellInfo)} allowed for representing an {nameof(CellStatus.Unvisited)} location.",
|
||||
nameof(status));
|
||||
|
||||
Status = status;
|
||||
CostSoFar = costSoFar;
|
||||
EstimatedTotalCost = estimatedTotalCost;
|
||||
PreviousNode = previousNode;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Status == CellStatus.Unvisited)
|
||||
return Status.ToString();
|
||||
|
||||
return
|
||||
$"{Status} {nameof(CostSoFar)}={CostSoFar} " +
|
||||
$"{nameof(EstimatedTotalCost)}={EstimatedTotalCost} {nameof(PreviousNode)}={PreviousNode}";
|
||||
}
|
||||
}
|
||||
}
|
||||
86
OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs
Normal file
86
OpenRA.Mods.Common/Pathfinder/CellInfoLayerPool.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
sealed class CellInfoLayerPool
|
||||
{
|
||||
const int MaxPoolSize = 4;
|
||||
readonly Stack<CellLayer<CellInfo>> pool = new(MaxPoolSize);
|
||||
readonly Map map;
|
||||
|
||||
public CellInfoLayerPool(Map map)
|
||||
{
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public PooledCellInfoLayer Get()
|
||||
{
|
||||
return new PooledCellInfoLayer(this);
|
||||
}
|
||||
|
||||
CellLayer<CellInfo> GetLayer()
|
||||
{
|
||||
CellLayer<CellInfo> layer = null;
|
||||
lock (pool)
|
||||
if (pool.Count > 0)
|
||||
layer = pool.Pop();
|
||||
|
||||
// As the default value of CellInfo represents an Unvisited location,
|
||||
// we don't need to initialize the values in the layer,
|
||||
// we can just clear them to the defaults.
|
||||
if (layer == null)
|
||||
layer = new CellLayer<CellInfo>(map);
|
||||
else
|
||||
layer.Clear();
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
void ReturnLayer(CellLayer<CellInfo> layer)
|
||||
{
|
||||
lock (pool)
|
||||
if (pool.Count < MaxPoolSize)
|
||||
pool.Push(layer);
|
||||
}
|
||||
|
||||
public sealed class PooledCellInfoLayer : IDisposable
|
||||
{
|
||||
CellInfoLayerPool layerPool;
|
||||
List<CellLayer<CellInfo>> layers = [];
|
||||
|
||||
public PooledCellInfoLayer(CellInfoLayerPool layerPool)
|
||||
{
|
||||
this.layerPool = layerPool;
|
||||
}
|
||||
|
||||
public CellLayer<CellInfo> GetLayer()
|
||||
{
|
||||
var layer = layerPool.GetLayer();
|
||||
layers.Add(layer);
|
||||
return layer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (layerPool != null)
|
||||
foreach (var layer in layers)
|
||||
layerPool.ReturnLayer(layer);
|
||||
|
||||
layers = null;
|
||||
layerPool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
235
OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs
Normal file
235
OpenRA.Mods.Common/Pathfinder/DensePathGraph.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// A dense pathfinding graph that implements the ability to cost and get connections for cells,
|
||||
/// and supports <see cref="ICustomMovementLayer"/>. Allows searching over a dense grid of cells.
|
||||
/// Derived classes are required to provide backing storage for the pathfinding information.
|
||||
/// </summary>
|
||||
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<CPos, int> customCost;
|
||||
readonly Actor ignoreActor;
|
||||
readonly bool laneBias;
|
||||
readonly bool inReverse;
|
||||
readonly bool checkTerrainHeight;
|
||||
|
||||
protected DensePathGraph(Locomotor locomotor, Actor actor, World world, BlockedByActor check,
|
||||
Func<CPos, int> 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a candidate neighbouring position is
|
||||
/// 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 IsValidNeighbor(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 CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1)], // TL
|
||||
[new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1)], // T
|
||||
[new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1)], // TR
|
||||
[new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1)], // L
|
||||
CVec.Directions,
|
||||
[new CVec(1, -1), new CVec(1, 0), new CVec(1, 1)], // R
|
||||
[new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1)], // BL
|
||||
[new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1)], // B
|
||||
[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<GraphConnection> GetConnections(CPos position, Func<CPos, bool> targetPredicate)
|
||||
{
|
||||
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<GraphConnection>(directions.Length + (layer == 0 ? customMovementLayersEnabledForLocomotor : 1));
|
||||
for (var i = 0; i < directions.Length; i++)
|
||||
{
|
||||
var dir = directions[i];
|
||||
var neighbor = position + dir;
|
||||
if (!IsValidNeighbor(neighbor))
|
||||
continue;
|
||||
|
||||
var pathCost = GetPathCostToNode(position, neighbor, dir, targetPredicate);
|
||||
if (pathCost != PathGraph.PathCostForInvalidPath &&
|
||||
this[neighbor].Status != CellStatus.Closed)
|
||||
validNeighbors.Add(new GraphConnection(neighbor, pathCost));
|
||||
}
|
||||
|
||||
if (layer == 0)
|
||||
{
|
||||
if (customMovementLayersEnabledForLocomotor > 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 (!IsValidNeighbor(layerPosition))
|
||||
continue;
|
||||
|
||||
var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition);
|
||||
if (entryCost != PathGraph.MovementCostForUnreachableCell &&
|
||||
CanEnterNode(position, layerPosition, targetPredicate) &&
|
||||
this[layerPosition].Status != CellStatus.Closed)
|
||||
validNeighbors.Add(new GraphConnection(layerPosition, entryCost));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var groundPosition = new CPos(position.X, position.Y, 0);
|
||||
if (IsValidNeighbor(groundPosition))
|
||||
{
|
||||
var exitCost = CustomMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
|
||||
if (exitCost != PathGraph.MovementCostForUnreachableCell &&
|
||||
CanEnterNode(position, groundPosition, targetPredicate) &&
|
||||
this[groundPosition].Status != CellStatus.Closed)
|
||||
validNeighbors.Add(new GraphConnection(groundPosition, exitCost));
|
||||
}
|
||||
}
|
||||
|
||||
return validNeighbors;
|
||||
}
|
||||
|
||||
bool CanEnterNode(CPos srcNode, CPos destNode, Func<CPos, bool> targetPredicate)
|
||||
{
|
||||
return
|
||||
locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor)
|
||||
!= PathGraph.MovementCostForUnreachableCell ||
|
||||
(inReverse && targetPredicate(destNode));
|
||||
}
|
||||
|
||||
int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction, Func<CPos, bool> targetPredicate)
|
||||
{
|
||||
var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor);
|
||||
|
||||
// When doing searches in reverse, we must allow movement onto an inaccessible target location.
|
||||
// Because when reversed this is actually the source, and it is allowed to move out from an inaccessible source.
|
||||
if (movementCost == PathGraph.MovementCostForUnreachableCell && inReverse && targetPredicate(destNode))
|
||||
movementCost = 0;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
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 (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1278
OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs
Normal file
1278
OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs
Normal file
File diff suppressed because it is too large
Load Diff
109
OpenRA.Mods.Common/Pathfinder/IPathGraph.cs
Normal file
109
OpenRA.Mods.Common/Pathfinder/IPathGraph.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a pathfinding graph with nodes and edges.
|
||||
/// Nodes are represented as cells, and pathfinding information
|
||||
/// in the form of <see cref="CellInfo"/> is attached to each one.
|
||||
/// </summary>
|
||||
public interface IPathGraph : IDisposable
|
||||
{
|
||||
/// <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, Func<CPos, bool> targetPredicate);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pathfinding information for a given node.
|
||||
/// </summary>
|
||||
CellInfo this[CPos node] { get; set; }
|
||||
}
|
||||
|
||||
public static class PathGraph
|
||||
{
|
||||
public const int PathCostForInvalidPath = int.MaxValue;
|
||||
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>
|
||||
public readonly struct GraphConnection
|
||||
{
|
||||
public readonly struct CostComparer : IComparer<GraphConnection>
|
||||
{
|
||||
public int Compare(GraphConnection x, GraphConnection y)
|
||||
{
|
||||
return x.Cost.CompareTo(y.Cost);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly CPos Destination;
|
||||
public readonly int Cost;
|
||||
|
||||
public GraphConnection(CPos destination, int cost)
|
||||
{
|
||||
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");
|
||||
|
||||
Destination = destination;
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public GraphEdge ToEdge(CPos source)
|
||||
{
|
||||
return new GraphEdge(source, Destination, Cost);
|
||||
}
|
||||
|
||||
public override string ToString() => $"-> {Destination} = {Cost}";
|
||||
}
|
||||
}
|
||||
56
OpenRA.Mods.Common/Pathfinder/MapPathGraph.cs
Normal file
56
OpenRA.Mods.Common/Pathfinder/MapPathGraph.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="ICustomMovementLayer"/>.
|
||||
/// </summary>
|
||||
sealed class MapPathGraph : DensePathGraph
|
||||
{
|
||||
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer;
|
||||
readonly CellLayer<CellInfo>[] cellInfoForLayer;
|
||||
|
||||
public MapPathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check,
|
||||
Func<CPos, int> customCost, Actor ignoreActor, bool laneBias, bool inReverse)
|
||||
: base(locomotor, actor, world, check, customCost, ignoreActor, laneBias, inReverse)
|
||||
{
|
||||
// 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<CellInfo>[CustomMovementLayers.Length];
|
||||
cellInfoForLayer[0] = pooledLayer.GetLayer();
|
||||
foreach (var cml in CustomMovementLayers)
|
||||
if (cml != null && cml.EnabledForLocomotor(locomotor.Info))
|
||||
cellInfoForLayer[cml.Index] = pooledLayer.GetLayer();
|
||||
}
|
||||
|
||||
public override CellInfo this[CPos pos]
|
||||
{
|
||||
get => cellInfoForLayer[pos.Layer][pos];
|
||||
set => cellInfoForLayer[pos.Layer][pos] = value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
pooledLayer.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
421
OpenRA.Mods.Common/Pathfinder/PathSearch.cs
Normal file
421
OpenRA.Mods.Common/Pathfinder/PathSearch.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public sealed class PathSearch : IDisposable
|
||||
{
|
||||
public interface IRecorder
|
||||
{
|
||||
void Add(CPos source, CPos destination, int costSoFar, int estimatedRemainingCost);
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
static readonly ConditionalWeakTable<World, CellInfoLayerPool>.CreateValueCallback CreateLayerPool = world => new CellInfoLayerPool(world.Map);
|
||||
|
||||
static CellInfoLayerPool LayerPoolForWorld(World world)
|
||||
{
|
||||
return LayerPoolTable.GetValue(world, CreateLayerPool);
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCellByPredicate(
|
||||
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,
|
||||
IRecorder recorder = null)
|
||||
{
|
||||
var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false);
|
||||
var search = new PathSearch(graph, (_, _) => 0, 0, targetPredicate, recorder);
|
||||
|
||||
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, false, search);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCell(
|
||||
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, bool, int> heuristic = null,
|
||||
Grid? grid = null,
|
||||
IRecorder recorder = null)
|
||||
{
|
||||
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 ??= DefaultCostEstimator(locomotor, target);
|
||||
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target, recorder);
|
||||
|
||||
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, inReverse, search);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a cell is a valid pathfinding location.
|
||||
/// <list type="bullet">
|
||||
/// <item>It is in the world.</item>
|
||||
/// <item>It is either on the ground layer (0) or on an *enabled* custom movement layer.</item>
|
||||
/// <item>It has not been excluded by the <paramref name="customCost"/>.</item>
|
||||
/// </list>
|
||||
/// If required, follow this with a call to
|
||||
/// <see cref="Locomotor.MovementCostToEnterCell(Actor, CPos, CPos, BlockedByActor, Actor, bool)"/> to
|
||||
/// determine if the cell is accessible.
|
||||
/// </summary>
|
||||
public static bool CellAllowsMovement(World world, Locomotor locomotor, CPos cell, Func<CPos, int> customCost)
|
||||
{
|
||||
return world.Map.Contains(cell) &&
|
||||
(cell.Layer == 0 || world.GetCustomMovementLayers()[cell.Layer].EnabledForLocomotor(locomotor.Info)) &&
|
||||
(customCost == null || customCost(cell) != PathGraph.PathCostForInvalidPath);
|
||||
}
|
||||
|
||||
static void AddInitialCells(World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms,
|
||||
BlockedByActor check, Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, PathSearch search)
|
||||
{
|
||||
// A source cell is allowed to have an unreachable movement cost.
|
||||
// Therefore we don't need to check if the cell is accessible, only that it allows movement.
|
||||
// *Unless* the search is being done in reverse, in this case the source is really a target,
|
||||
// and a target is required to have a reachable cost.
|
||||
// We also need to ignore self, so we don't consider the location blocked by ourselves!
|
||||
foreach (var sl in froms)
|
||||
if (CellAllowsMovement(world, locomotor, sl, customCost) &&
|
||||
(!inReverse || locomotor.MovementCostToEnterCell(self, sl, check, ignoreActor, true)
|
||||
!= PathGraph.MovementCostForUnreachableCell))
|
||||
search.AddInitialCell(sl, customCost);
|
||||
}
|
||||
|
||||
public static PathSearch ToTargetCellOverGraph(
|
||||
Func<CPos, List<GraphConnection>> edges, Locomotor locomotor, CPos from, CPos target,
|
||||
int estimatedSearchSize = 0, IRecorder recorder = null)
|
||||
{
|
||||
var graph = new SparsePathGraph(edges, estimatedSearchSize);
|
||||
var search = new PathSearch(graph, DefaultCostEstimator(locomotor, target), 100, loc => loc == target, recorder);
|
||||
|
||||
search.AddInitialCell(from, null);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default: Diagonal distance heuristic. More information:
|
||||
/// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
||||
/// Layers are ignored and incur no additional cost.
|
||||
/// </summary>
|
||||
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
|
||||
/// <param name="destination">The cell for which costs are to be given by the estimation function.</param>
|
||||
/// <returns>A delegate that calculates the cost estimation between the <paramref name="destination"/> and the given cell.</returns>
|
||||
public static Func<CPos, bool, int> DefaultCostEstimator(Locomotor locomotor, CPos destination)
|
||||
{
|
||||
var estimator = DefaultCostEstimator(locomotor);
|
||||
return (here, _) => estimator(here, destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default: Diagonal distance heuristic. More information:
|
||||
/// https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
||||
/// Layers are ignored and incur no additional cost.
|
||||
/// </summary>
|
||||
/// <param name="locomotor">Locomotor used to provide terrain costs.</param>
|
||||
/// <returns>A delegate that calculates the cost estimation between the given cells.</returns>
|
||||
public static Func<CPos, CPos, int> DefaultCostEstimator(Locomotor locomotor)
|
||||
{
|
||||
// Determine the minimum possible cost for moving horizontally between cells based on terrain speeds.
|
||||
// The minimum possible cost diagonally is then Sqrt(2) times more costly.
|
||||
var cellCost = locomotor.Info.TerrainSpeeds.Values.Min(ti => ti.Cost);
|
||||
var diagonalCellCost = Exts.MultiplyBySqrtTwo(cellCost);
|
||||
return (here, destination) =>
|
||||
{
|
||||
var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y));
|
||||
var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y);
|
||||
|
||||
// According to the information link, this is the shape of the function.
|
||||
// We just extract factors to simplify.
|
||||
// Possible simplification: var h = Constants.CellCost * (straight + (Constants.Sqrt2 - 2) * diag);
|
||||
return cellCost * straight + (diagonalCellCost - 2 * cellCost) * diag;
|
||||
};
|
||||
}
|
||||
|
||||
public IPathGraph Graph { get; }
|
||||
public Func<CPos, bool> TargetPredicate { get; set; }
|
||||
readonly Func<CPos, bool, int> heuristic;
|
||||
readonly int heuristicWeightPercentage;
|
||||
readonly IRecorder recorder;
|
||||
readonly IPriorityQueue<GraphConnection> openQueue;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new search.
|
||||
/// </summary>
|
||||
/// <param name="graph">Graph over which the search is conducted.</param>
|
||||
/// <param name="heuristic">Provides an estimation of the distance between the given cell and the target.
|
||||
/// The Boolean parameter indicates if the cell is known to be accessible.
|
||||
/// When true, it is known accessible as it is being explored by the search.
|
||||
/// When false, the cell is being considered as a starting location and might not be accessible.</param>
|
||||
/// <param name="heuristicWeightPercentage">
|
||||
/// The search will aim for the shortest path when given a weight of 100%.
|
||||
/// We can allow the search to find paths that aren't optimal by changing the weight.
|
||||
/// The weight limits the worst case length of the path,
|
||||
/// e.g. a weight of 110% will find a path no more than 10% longer than the shortest possible.
|
||||
/// The benefit of allowing the search to return suboptimal paths is faster computation time.
|
||||
/// The search can skip some areas of the search space, meaning it has less work to do.
|
||||
/// </param>
|
||||
/// <param name="targetPredicate">Determines if the given cell is the target.</param>
|
||||
/// <param name="recorder">If provided, will record all nodes explored by searches performed.</param>
|
||||
PathSearch(IPathGraph graph, Func<CPos, bool, int> heuristic, int heuristicWeightPercentage, Func<CPos, bool> targetPredicate, IRecorder recorder)
|
||||
{
|
||||
Graph = graph;
|
||||
this.heuristic = heuristic;
|
||||
this.heuristicWeightPercentage = heuristicWeightPercentage;
|
||||
TargetPredicate = targetPredicate;
|
||||
this.recorder = recorder;
|
||||
openQueue = new Primitives.PriorityQueue<GraphConnection, GraphConnection.CostComparer>(default);
|
||||
}
|
||||
|
||||
void AddInitialCell(CPos location, Func<CPos, int> customCost)
|
||||
{
|
||||
var initialCost = 0;
|
||||
if (customCost != null)
|
||||
{
|
||||
initialCost = customCost(location);
|
||||
if (initialCost == PathGraph.PathCostForInvalidPath)
|
||||
return;
|
||||
}
|
||||
|
||||
var heuristicCost = heuristic(location, false);
|
||||
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
||||
return;
|
||||
|
||||
var estimatedCost = heuristicCost * heuristicWeightPercentage / 100;
|
||||
Graph[location] = new CellInfo(CellStatus.Open, initialCost, initialCost + estimatedCost, location);
|
||||
var connection = new GraphConnection(location, estimatedCost);
|
||||
openQueue.Add(connection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there are more reachable cells and the search can be continued.
|
||||
/// If false, <see cref="Expand"/> can no longer be called.
|
||||
/// </summary>
|
||||
bool CanExpand()
|
||||
{
|
||||
// Connections to a cell can appear more than once if a search discovers a lower cost route to the cell.
|
||||
// The lower cost gets processed first and the cell will be Closed.
|
||||
// When checking if we can expand, pop any Closed cells off the queue, so Expand will only see Open cells.
|
||||
CellStatus status;
|
||||
do
|
||||
{
|
||||
if (openQueue.Empty)
|
||||
return false;
|
||||
|
||||
status = Graph[openQueue.Peek().Destination].Status;
|
||||
if (status == CellStatus.Closed)
|
||||
openQueue.Pop();
|
||||
}
|
||||
while (status == CellStatus.Closed);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function analyzes the neighbors of the most promising node in the pathfinding graph
|
||||
/// using the A* algorithm (A-star) and returns that node.
|
||||
/// </summary>
|
||||
/// <returns>The most promising node of the iteration.</returns>
|
||||
CPos Expand()
|
||||
{
|
||||
var currentMinNode = openQueue.Pop().Destination;
|
||||
|
||||
var currentInfo = Graph[currentMinNode];
|
||||
Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode);
|
||||
|
||||
foreach (var connection in Graph.GetConnections(currentMinNode, TargetPredicate))
|
||||
{
|
||||
// Calculate the cost up to that point
|
||||
var costSoFarToNeighbor = currentInfo.CostSoFar + connection.Cost;
|
||||
|
||||
var neighbor = connection.Destination;
|
||||
var neighborInfo = Graph[neighbor];
|
||||
|
||||
// Cost is even higher; next direction:
|
||||
if (neighborInfo.Status == CellStatus.Closed ||
|
||||
(neighborInfo.Status == CellStatus.Open && costSoFarToNeighbor >= neighborInfo.CostSoFar))
|
||||
continue;
|
||||
|
||||
// Now we may seriously consider this direction using heuristics.
|
||||
int estimatedRemainingCostToTarget;
|
||||
if (neighborInfo.Status == CellStatus.Open)
|
||||
{
|
||||
// If the cell has already been processed, we can reuse the result
|
||||
// (just the difference between the estimated total and the cost so far)
|
||||
estimatedRemainingCostToTarget = neighborInfo.EstimatedTotalCost - neighborInfo.CostSoFar;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the heuristic reports the cell is unreachable, we won't consider it.
|
||||
var heuristicCost = heuristic(neighbor, true);
|
||||
if (heuristicCost == PathGraph.PathCostForInvalidPath)
|
||||
continue;
|
||||
estimatedRemainingCostToTarget = heuristicCost * heuristicWeightPercentage / 100;
|
||||
}
|
||||
|
||||
recorder?.Add(currentMinNode, neighbor, costSoFarToNeighbor, estimatedRemainingCostToTarget);
|
||||
|
||||
var estimatedTotalCostToTarget = costSoFarToNeighbor + estimatedRemainingCostToTarget;
|
||||
Graph[neighbor] = new CellInfo(CellStatus.Open, costSoFarToNeighbor, estimatedTotalCostToTarget, currentMinNode);
|
||||
openQueue.Add(new GraphConnection(neighbor, estimatedTotalCostToTarget));
|
||||
}
|
||||
|
||||
return currentMinNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the path search until a path is found, and returns whether a path is found successfully.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the path search has previously been expanded it will only return true if a path can be found during
|
||||
/// *this* expansion of the search. If the search was expanded previously and the target is already
|
||||
/// <see cref="CellStatus.Closed"/> then this method will return false.
|
||||
/// </remarks>
|
||||
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.
|
||||
/// </summary>
|
||||
public List<CPos> FindPath()
|
||||
{
|
||||
while (CanExpand())
|
||||
{
|
||||
var p = Expand();
|
||||
if (TargetPredicate(p))
|
||||
return MakePath(Graph, p);
|
||||
}
|
||||
|
||||
return PathFinder.NoPath;
|
||||
}
|
||||
|
||||
// Build the path from the destination.
|
||||
// When we find a node that has the same previous position than itself, that node is the source node.
|
||||
static List<CPos> MakePath(IPathGraph graph, CPos destination)
|
||||
{
|
||||
var ret = new List<CPos>();
|
||||
var currentNode = destination;
|
||||
|
||||
while (graph[currentNode].PreviousNode != currentNode)
|
||||
{
|
||||
ret.Add(currentNode);
|
||||
currentNode = graph[currentNode].PreviousNode;
|
||||
}
|
||||
|
||||
ret.Add(currentNode);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands both path searches until they intersect, and returns the path.
|
||||
/// Returned path is from the source of the first search to the source of the second search.
|
||||
/// </summary>
|
||||
public static List<CPos> FindBidiPath(PathSearch first, PathSearch second)
|
||||
{
|
||||
while (first.CanExpand() && second.CanExpand())
|
||||
{
|
||||
// make some progress on the first search
|
||||
var p = first.Expand();
|
||||
var pInfo = second.Graph[p];
|
||||
if (pInfo.Status == CellStatus.Closed &&
|
||||
pInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
|
||||
return MakeBidiPath(first, second, p);
|
||||
|
||||
// make some progress on the second search
|
||||
var q = second.Expand();
|
||||
var qInfo = first.Graph[q];
|
||||
if (qInfo.Status == CellStatus.Closed &&
|
||||
qInfo.CostSoFar != PathGraph.PathCostForInvalidPath)
|
||||
return MakeBidiPath(first, second, q);
|
||||
}
|
||||
|
||||
return PathFinder.NoPath;
|
||||
}
|
||||
|
||||
// Build the path from the destination of each search.
|
||||
// When we find a node that has the same previous position than itself, that is the source of that search.
|
||||
static List<CPos> MakeBidiPath(PathSearch first, PathSearch second, CPos confluenceNode)
|
||||
{
|
||||
var ca = first.Graph;
|
||||
var cb = second.Graph;
|
||||
|
||||
var ret = new List<CPos>();
|
||||
|
||||
var q = confluenceNode;
|
||||
var previous = ca[q].PreviousNode;
|
||||
while (previous != q)
|
||||
{
|
||||
ret.Add(q);
|
||||
q = previous;
|
||||
previous = ca[q].PreviousNode;
|
||||
}
|
||||
|
||||
ret.Add(q);
|
||||
|
||||
ret.Reverse();
|
||||
|
||||
q = confluenceNode;
|
||||
previous = cb[q].PreviousNode;
|
||||
while (previous != q)
|
||||
{
|
||||
q = previous;
|
||||
previous = cb[q].PreviousNode;
|
||||
ret.Add(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Graph.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
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, Func<CPos, bool> targetPredicate)
|
||||
{
|
||||
return edges(position) ?? [];
|
||||
}
|
||||
|
||||
public CellInfo this[CPos pos]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (info.TryGetValue(pos, out var cellInfo))
|
||||
return cellInfo;
|
||||
return default;
|
||||
}
|
||||
set => info[pos] = value;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user