PathUtilities.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. using Pathfinding.Util;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Pathfinding {
  5. /// <summary>
  6. /// Contains useful functions for working with paths and nodes.
  7. /// This class works a lot with the <see cref="Pathfinding.GraphNode"/> class, a useful function to get nodes is AstarPath.GetNearest.
  8. /// See: <see cref="AstarPath.GetNearest"/>
  9. /// See: <see cref="Pathfinding.GraphUpdateUtilities"/>
  10. /// See: <see cref="Pathfinding.GraphUtilities"/>
  11. /// </summary>
  12. public static class PathUtilities {
  13. /// <summary>
  14. /// Returns if there is a walkable path from node1 to node2.
  15. /// This method is extremely fast because it only uses precalculated information.
  16. ///
  17. /// <code>
  18. /// GraphNode node1 = AstarPath.active.GetNearest(point1, NNConstraint.Default).node;
  19. /// GraphNode node2 = AstarPath.active.GetNearest(point2, NNConstraint.Default).node;
  20. ///
  21. /// if (PathUtilities.IsPathPossible(node1, node2)) {
  22. /// // Yay, there is a path between those two nodes
  23. /// }
  24. /// </code>
  25. ///
  26. /// Equivalent to calling <see cref="IsPathPossible(List<GraphNode>)"/> with a list containing node1 and node2.
  27. ///
  28. /// See: graph-updates (view in online documentation for working links)
  29. /// See: <see cref="AstarPath.GetNearest"/>
  30. /// </summary>
  31. public static bool IsPathPossible (GraphNode node1, GraphNode node2) {
  32. return node1.Walkable && node2.Walkable && node1.Area == node2.Area;
  33. }
  34. /// <summary>
  35. /// Returns if there are walkable paths between all nodes.
  36. ///
  37. /// See: graph-updates (view in online documentation for working links)
  38. ///
  39. /// Returns true for empty lists.
  40. ///
  41. /// See: <see cref="AstarPath.GetNearest"/>
  42. /// </summary>
  43. public static bool IsPathPossible (List<GraphNode> nodes) {
  44. if (nodes.Count == 0) return true;
  45. uint area = nodes[0].Area;
  46. for (int i = 0; i < nodes.Count; i++) if (!nodes[i].Walkable || nodes[i].Area != area) return false;
  47. return true;
  48. }
  49. /// <summary>
  50. /// Returns if there are walkable paths between all nodes.
  51. /// See: graph-updates (view in online documentation for working links)
  52. ///
  53. /// This method will actually only check if the first node can reach all other nodes. However this is
  54. /// equivalent in 99% of the cases since almost always the graph connections are bidirectional.
  55. /// If you are not aware of any cases where you explicitly create unidirectional connections
  56. /// this method can be used without worries.
  57. ///
  58. /// Returns true for empty lists
  59. ///
  60. /// Warning: This method is significantly slower than the IsPathPossible method which does not take a tagMask
  61. ///
  62. /// See: <see cref="AstarPath.GetNearest"/>
  63. /// </summary>
  64. public static bool IsPathPossible (List<GraphNode> nodes, int tagMask) {
  65. if (nodes.Count == 0) return true;
  66. // Make sure that the first node has a valid tag
  67. if (((tagMask >> (int)nodes[0].Tag) & 1) == 0) return false;
  68. // Fast check first
  69. if (!IsPathPossible(nodes)) return false;
  70. // Make sure that the first node can reach all other nodes
  71. var reachable = GetReachableNodes(nodes[0], tagMask);
  72. bool result = true;
  73. // Make sure that the first node can reach all other nodes
  74. for (int i = 1; i < nodes.Count; i++) {
  75. if (!reachable.Contains(nodes[i])) {
  76. result = false;
  77. break;
  78. }
  79. }
  80. // Pool the temporary list
  81. ListPool<GraphNode>.Release(ref reachable);
  82. return result;
  83. }
  84. /// <summary>
  85. /// Returns all nodes reachable from the seed node.
  86. /// This function performs a DFS (depth-first-search) or flood fill of the graph and returns all nodes which can be reached from
  87. /// the seed node. In almost all cases this will be identical to returning all nodes which have the same area as the seed node.
  88. /// In the editor areas are displayed as different colors of the nodes.
  89. /// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
  90. /// but no path from the seed node to that part of the graph.
  91. ///
  92. /// The returned list is not sorted in any particular way.
  93. ///
  94. /// Depending on the number of reachable nodes, this function can take quite some time to calculate
  95. /// so don't use it too often or it might affect the framerate of your game.
  96. ///
  97. /// See: bitmasks (view in online documentation for working links).
  98. ///
  99. /// Returns: A List<Node> containing all nodes reachable from the seed node.
  100. /// For better memory management the returned list should be pooled, see Pathfinding.Util.ListPool.
  101. /// </summary>
  102. /// <param name="seed">The node to start the search from.</param>
  103. /// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
  104. /// <param name="filter">Optional filter for which nodes to search. You can combine this with tagMask = -1 to make the filter determine everything.
  105. /// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
  106. public static List<GraphNode> GetReachableNodes (GraphNode seed, int tagMask = -1, System.Func<GraphNode, bool> filter = null) {
  107. Stack<GraphNode> dfsStack = StackPool<GraphNode>.Claim();
  108. List<GraphNode> reachable = ListPool<GraphNode>.Claim();
  109. /// <summary>TODO: Pool</summary>
  110. var map = new HashSet<GraphNode>();
  111. System.Action<GraphNode> callback;
  112. // Check if we can use the fast path
  113. if (tagMask == -1 && filter == null) {
  114. callback = (GraphNode node) => {
  115. if (node.Walkable && map.Add(node)) {
  116. reachable.Add(node);
  117. dfsStack.Push(node);
  118. }
  119. };
  120. } else {
  121. callback = (GraphNode node) => {
  122. if (node.Walkable && ((tagMask >> (int)node.Tag) & 0x1) != 0 && map.Add(node)) {
  123. if (filter != null && !filter(node)) return;
  124. reachable.Add(node);
  125. dfsStack.Push(node);
  126. }
  127. };
  128. }
  129. callback(seed);
  130. while (dfsStack.Count > 0) {
  131. dfsStack.Pop().GetConnections(callback);
  132. }
  133. StackPool<GraphNode>.Release(dfsStack);
  134. return reachable;
  135. }
  136. static Queue<GraphNode> BFSQueue;
  137. static Dictionary<GraphNode, int> BFSMap;
  138. /// <summary>
  139. /// Returns all nodes up to a given node-distance from the seed node.
  140. /// This function performs a BFS (<a href="https://en.wikipedia.org/wiki/Breadth-first_search">breadth-first search</a>) or flood fill of the graph and returns all nodes within a specified node distance which can be reached from
  141. /// the seed node. In almost all cases when depth is large enough this will be identical to returning all nodes which have the same area as the seed node.
  142. /// In the editor areas are displayed as different colors of the nodes.
  143. /// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
  144. /// but no path from the seed node to that part of the graph.
  145. ///
  146. /// The returned list is sorted by node distance from the seed node
  147. /// i.e distance is measured in the number of nodes the shortest path from seed to that node would pass through.
  148. /// Note that the distance measurement does not take heuristics, penalties or tag penalties.
  149. ///
  150. /// Depending on the number of nodes, this function can take quite some time to calculate
  151. /// so don't use it too often or it might affect the framerate of your game.
  152. ///
  153. /// Returns: A List<GraphNode> containing all nodes reachable up to a specified node distance from the seed node.
  154. /// For better memory management the returned list should be pooled, see Pathfinding.Util.ListPool
  155. ///
  156. /// Warning: This method is not thread safe. Only use it from the Unity thread (i.e normal game code).
  157. ///
  158. /// The video below shows the BFS result with varying values of depth. Points are sampled on the nodes using <see cref="GetPointsOnNodes"/>.
  159. /// [Open online documentation to see videos]
  160. ///
  161. /// <code>
  162. /// var seed = AstarPath.active.GetNearest(transform.position, NNConstraint.Default).node;
  163. /// var nodes = PathUtilities.BFS(seed, 10);
  164. /// foreach (var node in nodes) {
  165. /// Debug.DrawRay((Vector3)node.position, Vector3.up, Color.red, 10);
  166. /// }
  167. /// </code>
  168. /// </summary>
  169. /// <param name="seed">The node to start the search from.</param>
  170. /// <param name="depth">The maximum node-distance from the seed node.</param>
  171. /// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
  172. /// <param name="filter">Optional filter for which nodes to search. You can combine this with depth = int.MaxValue and tagMask = -1 to make the filter determine everything.
  173. /// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
  174. public static List<GraphNode> BFS (GraphNode seed, int depth, int tagMask = -1, System.Func<GraphNode, bool> filter = null) {
  175. #if ASTAR_PROFILE
  176. System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
  177. watch.Start();
  178. #endif
  179. BFSQueue = BFSQueue ?? new Queue<GraphNode>();
  180. var que = BFSQueue;
  181. BFSMap = BFSMap ?? new Dictionary<GraphNode, int>();
  182. var map = BFSMap;
  183. // Even though we clear at the end of this function, it is good to
  184. // do it here as well in case the previous invocation of the method
  185. // threw an exception for some reason
  186. // and didn't clear the que and map
  187. que.Clear();
  188. map.Clear();
  189. List<GraphNode> result = ListPool<GraphNode>.Claim();
  190. int currentDist = -1;
  191. System.Action<GraphNode> callback;
  192. if (tagMask == -1) {
  193. callback = node => {
  194. if (node.Walkable && !map.ContainsKey(node)) {
  195. if (filter != null && !filter(node)) return;
  196. map.Add(node, currentDist+1);
  197. result.Add(node);
  198. que.Enqueue(node);
  199. }
  200. };
  201. } else {
  202. callback = node => {
  203. if (node.Walkable && ((tagMask >> (int)node.Tag) & 0x1) != 0 && !map.ContainsKey(node)) {
  204. if (filter != null && !filter(node)) return;
  205. map.Add(node, currentDist+1);
  206. result.Add(node);
  207. que.Enqueue(node);
  208. }
  209. };
  210. }
  211. callback(seed);
  212. while (que.Count > 0) {
  213. GraphNode n = que.Dequeue();
  214. currentDist = map[n];
  215. if (currentDist >= depth) break;
  216. n.GetConnections(callback);
  217. }
  218. que.Clear();
  219. map.Clear();
  220. #if ASTAR_PROFILE
  221. watch.Stop();
  222. Debug.Log((1000*watch.Elapsed.TotalSeconds).ToString("0.0 ms"));
  223. #endif
  224. return result;
  225. }
  226. /// <summary>
  227. /// Returns points in a spiral centered around the origin with a minimum clearance from other points.
  228. /// The points are laid out on the involute of a circle
  229. /// See: http://en.wikipedia.org/wiki/Involute
  230. /// Which has some nice properties.
  231. /// All points are separated by clearance world units.
  232. /// This method is O(n), yes if you read the code you will see a binary search, but that binary search
  233. /// has an upper bound on the number of steps, so it does not yield a log factor.
  234. ///
  235. /// Note: Consider recycling the list after usage to reduce allocations.
  236. /// See: Pathfinding.Util.ListPool
  237. /// </summary>
  238. public static List<Vector3> GetSpiralPoints (int count, float clearance) {
  239. List<Vector3> pts = ListPool<Vector3>.Claim(count);
  240. // The radius of the smaller circle used for generating the involute of a circle
  241. // Calculated from the separation distance between the turns
  242. float a = clearance/(2*Mathf.PI);
  243. float t = 0;
  244. pts.Add(InvoluteOfCircle(a, t));
  245. for (int i = 0; i < count; i++) {
  246. Vector3 prev = pts[pts.Count-1];
  247. // d = -t0/2 + sqrt( t0^2/4 + 2d/a )
  248. // Minimum angle (radians) which would create an arc distance greater than clearance
  249. float d = -t/2 + Mathf.Sqrt(t*t/4 + 2*clearance/a);
  250. // Binary search for separating this point and the previous one
  251. float mn = t + d;
  252. float mx = t + 2*d;
  253. while (mx - mn > 0.01f) {
  254. float mid = (mn + mx)/2;
  255. Vector3 p = InvoluteOfCircle(a, mid);
  256. if ((p - prev).sqrMagnitude < clearance*clearance) {
  257. mn = mid;
  258. } else {
  259. mx = mid;
  260. }
  261. }
  262. pts.Add(InvoluteOfCircle(a, mx));
  263. t = mx;
  264. }
  265. return pts;
  266. }
  267. /// <summary>
  268. /// Returns the XZ coordinate of the involute of circle.
  269. /// See: http://en.wikipedia.org/wiki/Involute
  270. /// </summary>
  271. private static Vector3 InvoluteOfCircle (float a, float t) {
  272. return new Vector3(a*(Mathf.Cos(t) + t*Mathf.Sin(t)), 0, a*(Mathf.Sin(t) - t*Mathf.Cos(t)));
  273. }
  274. /// <summary>
  275. /// Will calculate a number of points around p which are on the graph and are separated by clearance from each other.
  276. /// This is like GetPointsAroundPoint except that previousPoints are treated as being in world space.
  277. /// The average of the points will be found and then that will be treated as the group center.
  278. /// </summary>
  279. /// <param name="p">The point to generate points around</param>
  280. /// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
  281. /// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
  282. /// <param name="previousPoints">The points to use for reference. Note that these are in world space.
  283. /// The new points will overwrite the existing points in the list. The result will be in world space.</param>
  284. /// <param name="radius">The final points will be at most this distance from p.</param>
  285. /// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
  286. public static void GetPointsAroundPointWorld (Vector3 p, IRaycastableGraph g, List<Vector3> previousPoints, float radius, float clearanceRadius) {
  287. if (previousPoints.Count == 0) return;
  288. Vector3 avg = Vector3.zero;
  289. for (int i = 0; i < previousPoints.Count; i++) avg += previousPoints[i];
  290. avg /= previousPoints.Count;
  291. for (int i = 0; i < previousPoints.Count; i++) previousPoints[i] -= avg;
  292. GetPointsAroundPoint(p, g, previousPoints, radius, clearanceRadius);
  293. }
  294. /// <summary>
  295. /// Will calculate a number of points around center which are on the graph and are separated by clearance from each other.
  296. /// The maximum distance from center to any point will be radius.
  297. /// Points will first be tried to be laid out as previousPoints and if that fails, random points will be selected.
  298. /// This is great if you want to pick a number of target points for group movement. If you pass all current agent points from e.g the group's average position
  299. /// this method will return target points so that the units move very little within the group, this is often aesthetically pleasing and reduces jitter if using
  300. /// some kind of local avoidance.
  301. ///
  302. /// TODO: Write unit tests
  303. /// </summary>
  304. /// <param name="center">The point to generate points around</param>
  305. /// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
  306. /// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
  307. /// <param name="previousPoints">The points to use for reference. Note that these should not be in world space. They are treated as relative to center.
  308. /// The new points will overwrite the existing points in the list. The result will be in world space, not relative to center.</param>
  309. /// <param name="radius">The final points will be at most this distance from center.</param>
  310. /// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
  311. public static void GetPointsAroundPoint (Vector3 center, IRaycastableGraph g, List<Vector3> previousPoints, float radius, float clearanceRadius) {
  312. if (g == null) throw new System.ArgumentNullException("g");
  313. var graph = g as NavGraph;
  314. if (graph == null) throw new System.ArgumentException("g is not a NavGraph");
  315. NNInfoInternal nn = graph.GetNearestForce(center, NNConstraint.Default);
  316. center = nn.clampedPosition;
  317. if (nn.node == null) {
  318. // No valid point to start from
  319. return;
  320. }
  321. // Make sure the enclosing circle has a radius which can pack circles with packing density 0.5
  322. radius = Mathf.Max(radius, 1.4142f*clearanceRadius*Mathf.Sqrt(previousPoints.Count)); //Mathf.Sqrt(previousPoints.Count*clearanceRadius*2));
  323. clearanceRadius *= clearanceRadius;
  324. for (int i = 0; i < previousPoints.Count; i++) {
  325. Vector3 dir = previousPoints[i];
  326. float magn = dir.magnitude;
  327. if (magn > 0) dir /= magn;
  328. float newMagn = radius;//magn > radius ? radius : magn;
  329. dir *= newMagn;
  330. GraphHitInfo hit;
  331. int tests = 0;
  332. while (true) {
  333. Vector3 pt = center + dir;
  334. if (g.Linecast(center, pt, out hit)) {
  335. if (hit.point == Vector3.zero) {
  336. // Oops, linecast actually failed completely
  337. // try again unless we have tried lots of times
  338. // then we just continue anyway
  339. tests++;
  340. if (tests > 8) {
  341. previousPoints[i] = pt;
  342. break;
  343. }
  344. } else {
  345. pt = hit.point;
  346. }
  347. }
  348. bool worked = false;
  349. for (float q = 0.1f; q <= 1.0f; q += 0.05f) {
  350. Vector3 qt = Vector3.Lerp(center, pt, q);
  351. worked = true;
  352. for (int j = 0; j < i; j++) {
  353. if ((previousPoints[j] - qt).sqrMagnitude < clearanceRadius) {
  354. worked = false;
  355. break;
  356. }
  357. }
  358. // Abort after 8 tests or when we have found a valid point
  359. if (worked || tests > 8) {
  360. worked = true;
  361. previousPoints[i] = qt;
  362. break;
  363. }
  364. }
  365. // Break out of nested loop
  366. if (worked) {
  367. break;
  368. }
  369. // If we could not find a valid point, reduce the clearance radius slightly to improve
  370. // the chances next time
  371. clearanceRadius *= 0.9f;
  372. // This will pick points in 2D closer to the edge of the circle with a higher probability
  373. dir = Random.onUnitSphere * Mathf.Lerp(newMagn, radius, tests / 5);
  374. dir.y = 0;
  375. tests++;
  376. }
  377. }
  378. }
  379. /// <summary>
  380. /// Returns randomly selected points on the specified nodes with each point being separated by clearanceRadius from each other.
  381. /// Selecting points ON the nodes only works for TriangleMeshNode (used by Recast Graph and Navmesh Graph) and GridNode (used by GridGraph).
  382. /// For other node types, only the positions of the nodes will be used.
  383. ///
  384. /// clearanceRadius will be reduced if no valid points can be found.
  385. ///
  386. /// Note: This method assumes that the nodes in the list have the same type for some special cases.
  387. /// More specifically if the first node is not a TriangleMeshNode or a GridNode, it will use a fast path
  388. /// which assumes that all nodes in the list have the same surface area (which usually is a surface area of zero and the
  389. /// nodes are all PointNodes).
  390. /// </summary>
  391. public static List<Vector3> GetPointsOnNodes (List<GraphNode> nodes, int count, float clearanceRadius = 0) {
  392. if (nodes == null) throw new System.ArgumentNullException("nodes");
  393. if (nodes.Count == 0) throw new System.ArgumentException("no nodes passed");
  394. List<Vector3> pts = ListPool<Vector3>.Claim(count);
  395. // Square
  396. clearanceRadius *= clearanceRadius;
  397. if (clearanceRadius > 0 || nodes[0] is TriangleMeshNode
  398. #if !ASTAR_NO_GRID_GRAPH
  399. || nodes[0] is GridNode
  400. #endif
  401. ) {
  402. // Accumulated area of all nodes
  403. List<float> accs = ListPool<float>.Claim(nodes.Count);
  404. // Total area of all nodes so far
  405. float tot = 0;
  406. for (int i = 0; i < nodes.Count; i++) {
  407. var surfaceArea = nodes[i].SurfaceArea();
  408. // Ensures that even if the nodes have a surface area of 0, a random one will still be picked
  409. // instead of e.g always picking the first or the last one.
  410. surfaceArea += 0.001f;
  411. tot += surfaceArea;
  412. accs.Add(tot);
  413. }
  414. for (int i = 0; i < count; i++) {
  415. //Pick point
  416. int testCount = 0;
  417. int testLimit = 10;
  418. bool worked = false;
  419. while (!worked) {
  420. worked = true;
  421. // If no valid points could be found, progressively lower the clearance radius until such a point is found
  422. if (testCount >= testLimit) {
  423. // Note that clearanceRadius is a squared radius
  424. clearanceRadius *= 0.9f*0.9f;
  425. testLimit += 10;
  426. if (testLimit > 100) clearanceRadius = 0;
  427. }
  428. // Pick a random node among the ones in the list weighted by their area
  429. float tg = Random.value*tot;
  430. int v = accs.BinarySearch(tg);
  431. if (v < 0) v = ~v;
  432. if (v >= nodes.Count) {
  433. // Cover edge cases
  434. worked = false;
  435. continue;
  436. }
  437. var node = nodes[v];
  438. var p = node.RandomPointOnSurface();
  439. // Test if it is some distance away from the other points
  440. if (clearanceRadius > 0) {
  441. for (int j = 0; j < pts.Count; j++) {
  442. if ((pts[j]-p).sqrMagnitude < clearanceRadius) {
  443. worked = false;
  444. break;
  445. }
  446. }
  447. }
  448. if (worked) {
  449. pts.Add(p);
  450. break;
  451. }
  452. testCount++;
  453. }
  454. }
  455. ListPool<float>.Release(ref accs);
  456. } else {
  457. // Fast path, assumes all nodes have the same area (usually zero)
  458. for (int i = 0; i < count; i++) {
  459. pts.Add((Vector3)nodes[Random.Range(0, nodes.Count)].RandomPointOnSurface());
  460. }
  461. }
  462. return pts;
  463. }
  464. }
  465. }