121 lines
4.1 KiB
C#
121 lines
4.1 KiB
C#
|
|
namespace Theriapolis.Core.Util;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 8-directional A* pathfinder on the 1024×1024 world tile grid.
|
|||
|
|
/// Uses pre-allocated arrays and a generation counter to avoid clearing between queries.
|
|||
|
|
/// The cost function receives (fromX, fromY, toX, toY, entryDir) and returns the cost
|
|||
|
|
/// of moving to the 'to' tile, or float.PositiveInfinity if impassable.
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class AStarPathfinder
|
|||
|
|
{
|
|||
|
|
private const int W = C.WORLD_WIDTH_TILES;
|
|||
|
|
private const int H = C.WORLD_HEIGHT_TILES;
|
|||
|
|
private const int TileCount = W * H;
|
|||
|
|
|
|||
|
|
// Persistent arrays reused across queries (generation counter avoids clearing)
|
|||
|
|
private readonly float[] _gScore = new float[TileCount];
|
|||
|
|
private readonly int[] _cameFrom = new int[TileCount]; // -1 = none
|
|||
|
|
private readonly byte[] _generation = new byte[TileCount]; // current run's gen tag
|
|||
|
|
private byte _currentGen;
|
|||
|
|
|
|||
|
|
private readonly BinaryHeap<int> _open = new(4096);
|
|||
|
|
|
|||
|
|
// 8-directional deltas: N, NE, E, SE, S, SW, W, NW
|
|||
|
|
private static readonly (int dx, int dy)[] Dirs =
|
|||
|
|
{
|
|||
|
|
( 0,-1), ( 1,-1), ( 1, 0), ( 1, 1),
|
|||
|
|
( 0, 1), (-1, 1), (-1, 0), (-1,-1),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private static float Diagonal(int dx, int dy)
|
|||
|
|
=> (dx != 0 && dy != 0) ? 1.4142f : 1f; // sqrt(2) for diagonals
|
|||
|
|
|
|||
|
|
private static float OctileHeuristic(int x0, int y0, int x1, int y1)
|
|||
|
|
{
|
|||
|
|
int dx = Math.Abs(x1 - x0);
|
|||
|
|
int dy = Math.Abs(y1 - y0);
|
|||
|
|
return Math.Max(dx, dy) + (1.4142f - 1f) * Math.Min(dx, dy);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Find the lowest-cost path from (sx,sy) to (gx,gy).
|
|||
|
|
/// <paramref name="costFn"/> receives (fromX, fromY, toX, toY, entryDir) and returns
|
|||
|
|
/// the cost of entering the 'to' tile from this direction, or PositiveInfinity if impassable.
|
|||
|
|
/// Returns null if no path exists within the tile grid or if maxExpansions is exceeded.
|
|||
|
|
/// </summary>
|
|||
|
|
public List<(int X, int Y)>? FindPath(
|
|||
|
|
int sx, int sy,
|
|||
|
|
int gx, int gy,
|
|||
|
|
Func<int, int, int, int, byte, float> costFn,
|
|||
|
|
int maxExpansions = 600_000)
|
|||
|
|
{
|
|||
|
|
if (sx == gx && sy == gy) return new List<(int, int)> { (sx, sy) };
|
|||
|
|
int expansions = 0;
|
|||
|
|
|
|||
|
|
// Increment generation (wrap around)
|
|||
|
|
_currentGen = (byte)((_currentGen + 1) == 0 ? 1 : _currentGen + 1);
|
|||
|
|
_open.Clear();
|
|||
|
|
|
|||
|
|
int startIdx = sy * W + sx;
|
|||
|
|
_gScore[startIdx] = 0f;
|
|||
|
|
_cameFrom[startIdx] = -1;
|
|||
|
|
_generation[startIdx] = _currentGen;
|
|||
|
|
|
|||
|
|
float h0 = OctileHeuristic(sx, sy, gx, gy);
|
|||
|
|
_open.Insert(startIdx, h0);
|
|||
|
|
|
|||
|
|
while (!_open.IsEmpty)
|
|||
|
|
{
|
|||
|
|
if (++expansions > maxExpansions) return null; // safety cap
|
|||
|
|
|
|||
|
|
int curIdx = _open.ExtractMin();
|
|||
|
|
int curX = curIdx % W;
|
|||
|
|
int curY = curIdx / W;
|
|||
|
|
|
|||
|
|
if (curX == gx && curY == gy)
|
|||
|
|
return ReconstructPath(curIdx);
|
|||
|
|
|
|||
|
|
float curG = _gScore[curIdx];
|
|||
|
|
|
|||
|
|
for (int d = 0; d < 8; d++)
|
|||
|
|
{
|
|||
|
|
int nx = curX + Dirs[d].dx;
|
|||
|
|
int ny = curY + Dirs[d].dy;
|
|||
|
|
if ((uint)nx >= W || (uint)ny >= H) continue;
|
|||
|
|
|
|||
|
|
byte entryDir = (byte)d;
|
|||
|
|
float moveCost = costFn(curX, curY, nx, ny, entryDir);
|
|||
|
|
if (float.IsPositiveInfinity(moveCost)) continue;
|
|||
|
|
|
|||
|
|
float newG = curG + Diagonal(Dirs[d].dx, Dirs[d].dy) + moveCost;
|
|||
|
|
int nIdx = ny * W + nx;
|
|||
|
|
|
|||
|
|
if (_generation[nIdx] == _currentGen && newG >= _gScore[nIdx])
|
|||
|
|
continue; // already found a better path
|
|||
|
|
|
|||
|
|
_gScore[nIdx] = newG;
|
|||
|
|
_cameFrom[nIdx] = curIdx;
|
|||
|
|
_generation[nIdx] = _currentGen;
|
|||
|
|
|
|||
|
|
float f = newG + OctileHeuristic(nx, ny, gx, gy);
|
|||
|
|
_open.Insert(nIdx, f);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null; // no path found
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private List<(int X, int Y)> ReconstructPath(int goalIdx)
|
|||
|
|
{
|
|||
|
|
var path = new List<(int, int)>();
|
|||
|
|
int cur = goalIdx;
|
|||
|
|
while (cur != -1)
|
|||
|
|
{
|
|||
|
|
path.Add((cur % W, cur / W));
|
|||
|
|
cur = _cameFrom[cur];
|
|||
|
|
}
|
|||
|
|
path.Reverse();
|
|||
|
|
return path;
|
|||
|
|
}
|
|||
|
|
}
|