151 lines
4.8 KiB
C#
151 lines
4.8 KiB
C#
using Content.Shared.NPC;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.NPC.Pathfinding;
|
|
|
|
public sealed partial class PathfindingSystem
|
|
{
|
|
private PathResult UpdateAStarPath(AStarPathRequest request)
|
|
{
|
|
if (request.Start.Equals(request.End))
|
|
{
|
|
return PathResult.Path;
|
|
}
|
|
|
|
if (request.Task.IsCanceled)
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
// TODO: Need partial planning that uses best node.
|
|
PathPoly? currentNode = null;
|
|
|
|
// First run
|
|
if (!request.Started)
|
|
{
|
|
request.Frontier = new PriorityQueue<(float, PathPoly)>(PathPolyComparer);
|
|
request.Started = true;
|
|
}
|
|
// Re-validate nodes
|
|
else
|
|
{
|
|
// Theoretically this shouldn't be happening, but practically...
|
|
if (request.Frontier.Count == 0)
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
(_, currentNode) = request.Frontier.Peek();
|
|
|
|
if (!currentNode.IsValid())
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
// Re-validate parents too.
|
|
if (request.CameFrom.TryGetValue(currentNode, out var parentNode) && !parentNode.IsValid())
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
}
|
|
|
|
DebugTools.Assert(!request.Task.IsCompleted);
|
|
request.Stopwatch.Restart();
|
|
|
|
var startNode = GetPoly(request.Start);
|
|
var endNode = GetPoly(request.End);
|
|
|
|
if (startNode == null || endNode == null)
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
currentNode = startNode;
|
|
request.Frontier.Add((0.0f, startNode));
|
|
request.CostSoFar[startNode] = 0.0f;
|
|
var count = 0;
|
|
var arrived = false;
|
|
|
|
while (request.Frontier.Count > 0 && count < NodeLimit)
|
|
{
|
|
// Handle whether we need to pause if we've taken too long
|
|
if (count % 20 == 0 && count > 0 && request.Stopwatch.Elapsed > PathTime)
|
|
{
|
|
// I had this happen once in testing but I don't think it should be possible?
|
|
DebugTools.Assert(request.Frontier.Count > 0);
|
|
return PathResult.Continuing;
|
|
}
|
|
|
|
count++;
|
|
|
|
// Actual pathfinding here
|
|
(_, currentNode) = request.Frontier.Take();
|
|
|
|
// If we're inside the required distance OR we're at the end node.
|
|
if ((request.Distance > 0f &&
|
|
currentNode.Coordinates.TryDistance(EntityManager, request.End, out var distance) &&
|
|
distance <= request.Distance) ||
|
|
currentNode.Equals(endNode))
|
|
{
|
|
arrived = true;
|
|
break;
|
|
}
|
|
|
|
foreach (var neighbor in currentNode.Neighbors)
|
|
{
|
|
var tileCost = GetTileCost(request, currentNode, neighbor);
|
|
|
|
if (tileCost.Equals(0f))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// f = g + h
|
|
// gScore is distance to the start node
|
|
// hScore is distance to the end node
|
|
var gScore = request.CostSoFar[currentNode] + tileCost;
|
|
if (request.CostSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
request.CameFrom[neighbor] = currentNode;
|
|
request.CostSoFar[neighbor] = gScore;
|
|
// pFactor is tie-breaker where the fscore is otherwise equal.
|
|
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
|
|
// There's other ways to do it but future consideration
|
|
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
|
// (i.e. somewhere between 1 and infinite)
|
|
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
|
var hScore = OctileDistance(endNode, neighbor) * (1.0f + 1.0f / 1000.0f);
|
|
var fScore = gScore + hScore;
|
|
request.Frontier.Add((fScore, neighbor));
|
|
}
|
|
}
|
|
|
|
if (!arrived)
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
var route = ReconstructPath(request.CameFrom, currentNode);
|
|
var path = new Queue<EntityCoordinates>(route.Count);
|
|
|
|
foreach (var node in route)
|
|
{
|
|
// Due to partial planning some nodes may have been invalidated.
|
|
if (!node.IsValid())
|
|
{
|
|
return PathResult.NoPath;
|
|
}
|
|
|
|
path.Enqueue(node.Coordinates);
|
|
}
|
|
|
|
DebugTools.Assert(route.Count > 0);
|
|
request.Polys = route;
|
|
return PathResult.Path;
|
|
}
|
|
}
|