PathfindingSystem.Breadth.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. namespace Content.Server.NPC.Pathfinding;
  2. public sealed partial class PathfindingSystem
  3. {
  4. /*
  5. * Handle BFS searches from Start->End. Doesn't consider NPC pathfinding.
  6. */
  7. /// <summary>
  8. /// Pathfinding args for a 1-many path.
  9. /// </summary>
  10. public record struct BreadthPathArgs()
  11. {
  12. public required Vector2i Start;
  13. public required List<Vector2i> Ends;
  14. public bool Diagonals = false;
  15. public Func<Vector2i, float>? TileCost;
  16. public int Limit = 10000;
  17. }
  18. /// <summary>
  19. /// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
  20. /// </summary>
  21. public SimplePathResult GetBreadthPath(BreadthPathArgs args)
  22. {
  23. var cameFrom = new Dictionary<Vector2i, Vector2i>();
  24. var costSoFar = new Dictionary<Vector2i, float>();
  25. var frontier = new PriorityQueue<Vector2i, float>();
  26. costSoFar[args.Start] = 0f;
  27. frontier.Enqueue(args.Start, 0f);
  28. var count = 0;
  29. while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
  30. {
  31. count++;
  32. if (args.Ends.Contains(node))
  33. {
  34. // Found target
  35. var path = ReconstructPath(node, cameFrom);
  36. return new SimplePathResult()
  37. {
  38. CameFrom = cameFrom,
  39. Path = path,
  40. };
  41. }
  42. var gCost = costSoFar[node];
  43. if (args.Diagonals)
  44. {
  45. for (var x = -1; x <= 1; x++)
  46. {
  47. for (var y = -1; y <= 1; y++)
  48. {
  49. var neighbor = node + new Vector2i(x, y);
  50. var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
  51. if (neighborCost.Equals(0f))
  52. {
  53. continue;
  54. }
  55. // f = g + h
  56. // gScore is distance to the start node
  57. // hScore is distance to the end node
  58. var gScore = gCost + neighborCost;
  59. // Slower to get here so just ignore it.
  60. if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
  61. {
  62. continue;
  63. }
  64. cameFrom[neighbor] = node;
  65. costSoFar[neighbor] = gScore;
  66. // pFactor is tie-breaker where the fscore is otherwise equal.
  67. // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
  68. // There's other ways to do it but future consideration
  69. // The closer the fScore is to the actual distance then the better the pathfinder will be
  70. // (i.e. somewhere between 1 and infinite)
  71. // Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
  72. frontier.Enqueue(neighbor, gScore);
  73. }
  74. }
  75. }
  76. else
  77. {
  78. for (var x = -1; x <= 1; x++)
  79. {
  80. for (var y = -1; y <= 1; y++)
  81. {
  82. if (x != 0 && y != 0)
  83. continue;
  84. var neighbor = node + new Vector2i(x, y);
  85. var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
  86. if (neighborCost.Equals(0f))
  87. continue;
  88. var gScore = gCost + neighborCost;
  89. if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
  90. continue;
  91. cameFrom[neighbor] = node;
  92. costSoFar[neighbor] = gScore;
  93. frontier.Enqueue(neighbor, gScore);
  94. }
  95. }
  96. }
  97. }
  98. return SimplePathResult.NoPath;
  99. }
  100. }