PathfindingSystem.AStar.cs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. using Content.Shared.NPC;
  2. using Robust.Shared.Map;
  3. using Robust.Shared.Utility;
  4. namespace Content.Server.NPC.Pathfinding;
  5. public sealed partial class PathfindingSystem
  6. {
  7. private PathResult UpdateAStarPath(AStarPathRequest request)
  8. {
  9. if (request.Start.Equals(request.End))
  10. {
  11. return PathResult.Path;
  12. }
  13. if (request.Task.IsCanceled)
  14. {
  15. return PathResult.NoPath;
  16. }
  17. // TODO: Need partial planning that uses best node.
  18. PathPoly? currentNode = null;
  19. // First run
  20. if (!request.Started)
  21. {
  22. request.Frontier = new PriorityQueue<(float, PathPoly)>(PathPolyComparer);
  23. request.Started = true;
  24. }
  25. // Re-validate nodes
  26. else
  27. {
  28. // Theoretically this shouldn't be happening, but practically...
  29. if (request.Frontier.Count == 0)
  30. {
  31. return PathResult.NoPath;
  32. }
  33. (_, currentNode) = request.Frontier.Peek();
  34. if (!currentNode.IsValid())
  35. {
  36. return PathResult.NoPath;
  37. }
  38. // Re-validate parents too.
  39. if (request.CameFrom.TryGetValue(currentNode, out var parentNode) && !parentNode.IsValid())
  40. {
  41. return PathResult.NoPath;
  42. }
  43. }
  44. DebugTools.Assert(!request.Task.IsCompleted);
  45. request.Stopwatch.Restart();
  46. var startNode = GetPoly(request.Start);
  47. var endNode = GetPoly(request.End);
  48. if (startNode == null || endNode == null)
  49. {
  50. return PathResult.NoPath;
  51. }
  52. currentNode = startNode;
  53. request.Frontier.Add((0.0f, startNode));
  54. request.CostSoFar[startNode] = 0.0f;
  55. var count = 0;
  56. var arrived = false;
  57. while (request.Frontier.Count > 0 && count < NodeLimit)
  58. {
  59. // Handle whether we need to pause if we've taken too long
  60. if (count % 20 == 0 && count > 0 && request.Stopwatch.Elapsed > PathTime)
  61. {
  62. // I had this happen once in testing but I don't think it should be possible?
  63. DebugTools.Assert(request.Frontier.Count > 0);
  64. return PathResult.Continuing;
  65. }
  66. count++;
  67. // Actual pathfinding here
  68. (_, currentNode) = request.Frontier.Take();
  69. // If we're inside the required distance OR we're at the end node.
  70. if ((request.Distance > 0f &&
  71. currentNode.Coordinates.TryDistance(EntityManager, request.End, out var distance) &&
  72. distance <= request.Distance) ||
  73. currentNode.Equals(endNode))
  74. {
  75. arrived = true;
  76. break;
  77. }
  78. foreach (var neighbor in currentNode.Neighbors)
  79. {
  80. var tileCost = GetTileCost(request, currentNode, neighbor);
  81. if (tileCost.Equals(0f))
  82. {
  83. continue;
  84. }
  85. // f = g + h
  86. // gScore is distance to the start node
  87. // hScore is distance to the end node
  88. var gScore = request.CostSoFar[currentNode] + tileCost;
  89. if (request.CostSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
  90. {
  91. continue;
  92. }
  93. request.CameFrom[neighbor] = currentNode;
  94. request.CostSoFar[neighbor] = gScore;
  95. // pFactor is tie-breaker where the fscore is otherwise equal.
  96. // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
  97. // There's other ways to do it but future consideration
  98. // The closer the fScore is to the actual distance then the better the pathfinder will be
  99. // (i.e. somewhere between 1 and infinite)
  100. // Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
  101. var hScore = OctileDistance(endNode, neighbor) * (1.0f + 1.0f / 1000.0f);
  102. var fScore = gScore + hScore;
  103. request.Frontier.Add((fScore, neighbor));
  104. }
  105. }
  106. if (!arrived)
  107. {
  108. return PathResult.NoPath;
  109. }
  110. var route = ReconstructPath(request.CameFrom, currentNode);
  111. var path = new Queue<EntityCoordinates>(route.Count);
  112. foreach (var node in route)
  113. {
  114. // Due to partial planning some nodes may have been invalidated.
  115. if (!node.IsValid())
  116. {
  117. return PathResult.NoPath;
  118. }
  119. path.Enqueue(node.Coordinates);
  120. }
  121. DebugTools.Assert(route.Count > 0);
  122. request.Polys = route;
  123. return PathResult.Path;
  124. }
  125. }