ProceduralGridMover.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. using UnityEngine;
  2. using System.Collections;
  3. namespace Pathfinding {
  4. /// <summary>
  5. /// Moves a grid graph to follow a target.
  6. ///
  7. /// Attach this to some object in the scene and assign the target to e.g the player.
  8. /// Then the graph will follow that object around as it moves.
  9. ///
  10. /// [Open online documentation to see videos]
  11. ///
  12. /// This is useful if pathfinding is only necessary in a small region around an object (for example the player).
  13. /// It makes it possible to have vast open worlds (maybe procedurally generated) and still be able to use pathfinding on them.
  14. ///
  15. /// When the graph is moved you may notice an fps drop.
  16. /// If this grows too large you can try a few things:
  17. /// - Reduce the <see cref="updateDistance"/>. This will make the updates smaller but more frequent.
  18. /// This only works to some degree however since an update has an inherent overhead.
  19. /// - Reduce the grid size.
  20. /// - Turn on multithreading (A* Inspector -> Settings)
  21. /// - Disable Height Testing or Collision Testing in the grid graph. This can give a performance boost
  22. /// since fewer calls to the physics engine need to be done.
  23. /// - Avoid using any erosion in the grid graph settings. This is relatively slow.
  24. ///
  25. /// This script has a built-in constant called <see cref="MaxMillisPerFrame"/> and it tries to not use any more
  26. /// cpu time than that per frame.
  27. ///
  28. /// Make sure you have 'Show Graphs' disabled in the A* inspector since gizmos in the scene view can take some
  29. /// time to update when the graph moves and thus make it seem like this script is slower than it actually is.
  30. ///
  31. /// See: Take a look at the example scene called "Procedural" for an example of how to use this script
  32. ///
  33. /// Note: Using erosion on grid graphs can significantly lower the performance when updating graphs.
  34. /// Each erosion iteration requires expanding the region that is updated by 1 node.
  35. /// </summary>
  36. [HelpURL("http://arongranberg.com/astar/documentation/stable/class_pathfinding_1_1_procedural_grid_mover.php")]
  37. public class ProceduralGridMover : VersionedMonoBehaviour {
  38. /// <summary>
  39. /// Graph will be updated if the target is more than this number of nodes from the graph center.
  40. /// Note that this is in nodes, not world units.
  41. ///
  42. /// Version: The unit was changed to nodes instead of world units in 3.6.8.
  43. /// </summary>
  44. public float updateDistance = 10;
  45. /// <summary>Graph will be moved to follow this target</summary>
  46. public Transform target;
  47. /// <summary>Temporary buffer</summary>
  48. GridNodeBase[] buffer;
  49. /// <summary>True while the graph is being updated by this script</summary>
  50. public bool updatingGraph { get; private set; }
  51. /// <summary>
  52. /// Grid graph to update.
  53. /// This will be set at Start based on <see cref="graphIndex"/>.
  54. /// During runtime you may set this to any graph or to null to disable updates.
  55. /// </summary>
  56. public GridGraph graph;
  57. /// <summary>
  58. /// Index for the graph to update.
  59. /// This will be used at Start to set <see cref="graph"/>.
  60. ///
  61. /// This is an index into the AstarPath.active.data.graphs array.
  62. /// </summary>
  63. [HideInInspector]
  64. public int graphIndex;
  65. void Start () {
  66. if (AstarPath.active == null) throw new System.Exception("There is no AstarPath object in the scene");
  67. // If one creates this component via a script then they may have already set the graph field.
  68. // In that case don't replace it.
  69. if (graph == null) {
  70. if (graphIndex < 0) throw new System.Exception("Graph index should not be negative");
  71. if (graphIndex >= AstarPath.active.data.graphs.Length) throw new System.Exception("The ProceduralGridMover was configured to use graph index " + graphIndex + ", but only " + AstarPath.active.data.graphs.Length + " graphs exist");
  72. graph = AstarPath.active.data.graphs[graphIndex] as GridGraph;
  73. if (graph == null) throw new System.Exception("The ProceduralGridMover was configured to use graph index " + graphIndex + " but that graph either does not exist or is not a GridGraph or LayerGridGraph");
  74. }
  75. UpdateGraph();
  76. }
  77. /// <summary>Update is called once per frame</summary>
  78. void Update () {
  79. if (graph == null) return;
  80. // Calculate where the graph center and the target position is in graph space
  81. var graphCenterInGraphSpace = PointToGraphSpace(graph.center);
  82. var targetPositionInGraphSpace = PointToGraphSpace(target.position);
  83. // Check the distance in graph space
  84. // We only care about the X and Z axes since the Y axis is the "height" coordinate of the nodes (in graph space)
  85. // We only care about the plane that the nodes are placed in
  86. if (VectorMath.SqrDistanceXZ(graphCenterInGraphSpace, targetPositionInGraphSpace) > updateDistance*updateDistance) {
  87. UpdateGraph();
  88. }
  89. }
  90. /// <summary>
  91. /// Transforms a point from world space to graph space.
  92. /// In graph space, (0,0,0) is bottom left corner of the graph
  93. /// and one unit along the X and Z axes equals distance between two nodes
  94. /// the Y axis still uses world units
  95. /// </summary>
  96. Vector3 PointToGraphSpace (Vector3 p) {
  97. // Multiply with the inverse matrix of the graph
  98. // to get the point in graph space
  99. return graph.transform.InverseTransform(p);
  100. }
  101. /// <summary>
  102. /// Updates the graph asynchronously.
  103. /// This will move the graph so that the target's position is the center of the graph.
  104. /// If the graph is already being updated, the call will be ignored.
  105. ///
  106. /// The image below shows which nodes will be updated when the graph moves.
  107. /// The whole graph is not recalculated each time it is moved, but only those
  108. /// nodes that have to be updated, the rest will keep their old values.
  109. /// The image is a bit simplified but it shows the main idea.
  110. /// [Open online documentation to see images]
  111. ///
  112. /// If you want to move the graph synchronously then call
  113. /// <code> AstarPath.active.FlushWorkItems(); </code>
  114. /// Immediately after you have called this method.
  115. /// </summary>
  116. public void UpdateGraph () {
  117. if (updatingGraph) {
  118. // We are already updating the graph
  119. // so ignore this call
  120. return;
  121. }
  122. updatingGraph = true;
  123. // Start a work item for updating the graph
  124. // This will pause the pathfinding threads
  125. // so that it is safe to update the graph
  126. // and then do it over several frames
  127. // (hence the IEnumerator coroutine)
  128. // to avoid too large FPS drops
  129. IEnumerator ie = UpdateGraphCoroutine();
  130. AstarPath.active.AddWorkItem(new AstarWorkItem(
  131. (context, force) => {
  132. // If force is true we need to calculate all steps at once
  133. if (force) while (ie.MoveNext()) {}
  134. // Calculate one step. False will be returned when there are no more steps
  135. bool done;
  136. try {
  137. done = !ie.MoveNext();
  138. } catch (System.Exception e) {
  139. // The code MAY throw an exception in rare circumstances if for example the user
  140. // changes the width of the graph in the inspector while an update is being performed
  141. // at the same time. So lets just fail in that case and retry later.
  142. Debug.LogException(e, this);
  143. done = true;
  144. }
  145. if (done) {
  146. updatingGraph = false;
  147. }
  148. return done;
  149. }));
  150. }
  151. /// <summary>Async method for moving the graph</summary>
  152. IEnumerator UpdateGraphCoroutine () {
  153. // Find the direction that we want to move the graph in.
  154. // Calcuculate this in graph space (where a distance of one is the size of one node)
  155. Vector3 dir = PointToGraphSpace(target.position) - PointToGraphSpace(graph.center);
  156. // Snap to a whole number of nodes
  157. dir.x = Mathf.Round(dir.x);
  158. dir.z = Mathf.Round(dir.z);
  159. dir.y = 0;
  160. // Nothing do to
  161. if (dir == Vector3.zero) yield break;
  162. // Number of nodes to offset in each direction
  163. Int2 offset = new Int2(-Mathf.RoundToInt(dir.x), -Mathf.RoundToInt(dir.z));
  164. // Move the center (this is in world units, so we need to convert it back from graph space)
  165. graph.center += graph.transform.TransformVector(dir);
  166. graph.UpdateTransform();
  167. // Cache some variables for easier access
  168. int width = graph.width;
  169. int depth = graph.depth;
  170. GridNodeBase[] nodes;
  171. // Layers are required when handling LayeredGridGraphs
  172. int layers = graph.LayerCount;
  173. var layeredGraph = graph as LayerGridGraph;
  174. if (layeredGraph != null) {
  175. nodes = layeredGraph.nodes;
  176. } else {
  177. nodes = graph.nodes;
  178. }
  179. // Create a temporary buffer required for the calculations
  180. if (buffer == null || buffer.Length != width*depth) {
  181. buffer = new GridNodeBase[width*depth];
  182. }
  183. // Check if we have moved less than a whole graph width all directions
  184. // If we have moved more than this we can just as well recalculate the whole graph
  185. if (Mathf.Abs(offset.x) <= width && Mathf.Abs(offset.y) <= depth) {
  186. IntRect recalculateRect = new IntRect(0, 0, offset.x, offset.y);
  187. // If offset.x < 0, adjust the rect
  188. if (recalculateRect.xmin > recalculateRect.xmax) {
  189. int tmp2 = recalculateRect.xmax;
  190. recalculateRect.xmax = width + recalculateRect.xmin;
  191. recalculateRect.xmin = width + tmp2;
  192. }
  193. // If offset.y < 0, adjust the rect
  194. if (recalculateRect.ymin > recalculateRect.ymax) {
  195. int tmp2 = recalculateRect.ymax;
  196. recalculateRect.ymax = depth + recalculateRect.ymin;
  197. recalculateRect.ymin = depth + tmp2;
  198. }
  199. // Connections need to be recalculated for the neighbours as well, so we need to expand the rect by 1
  200. var connectionRect = recalculateRect.Expand(1);
  201. // Makes sure the rect stays inside the grid
  202. connectionRect = IntRect.Intersection(connectionRect, new IntRect(0, 0, width, depth));
  203. // Offset each node by the #offset variable
  204. // nodes which would end up outside the graph
  205. // will wrap around to the other side of it
  206. for (int l = 0; l < layers; l++) {
  207. int layerOffset = l*width*depth;
  208. for (int z = 0; z < depth; z++) {
  209. int pz = z*width;
  210. int tz = ((z+offset.y + depth)%depth)*width;
  211. for (int x = 0; x < width; x++) {
  212. buffer[tz + ((x+offset.x + width) % width)] = nodes[layerOffset + pz + x];
  213. }
  214. }
  215. yield return null;
  216. // Copy the nodes back to the graph
  217. // and set the correct indices
  218. for (int z = 0; z < depth; z++) {
  219. int pz = z*width;
  220. for (int x = 0; x < width; x++) {
  221. int newIndex = pz + x;
  222. var node = buffer[newIndex];
  223. if (node != null) node.NodeInGridIndex = newIndex;
  224. nodes[layerOffset + newIndex] = node;
  225. }
  226. // Calculate the limits for the region that has been wrapped
  227. // to the other side of the graph
  228. int xmin, xmax;
  229. if (z >= recalculateRect.ymin && z < recalculateRect.ymax) {
  230. xmin = 0;
  231. xmax = depth;
  232. } else {
  233. xmin = recalculateRect.xmin;
  234. xmax = recalculateRect.xmax;
  235. }
  236. for (int x = xmin; x < xmax; x++) {
  237. var node = buffer[pz + x];
  238. if (node != null) {
  239. // Clear connections on all nodes that are wrapped and placed on the other side of the graph.
  240. // This is both to clear any custom connections (which do not really make sense after moving the node)
  241. // and to prevent possible exceptions when the node will later (possibly) be destroyed because it was
  242. // not needed anymore (only for layered grid graphs).
  243. node.ClearConnections(false);
  244. }
  245. }
  246. }
  247. yield return null;
  248. }
  249. // The calculation will only update approximately this number of
  250. // nodes per frame. This is used to keep CPU load per frame low
  251. int yieldEvery = 1000;
  252. // To avoid the update taking too long, make yieldEvery somewhat proportional to the number of nodes that we are going to update
  253. int approxNumNodesToUpdate = Mathf.Max(Mathf.Abs(offset.x), Mathf.Abs(offset.y)) * Mathf.Max(width, depth);
  254. yieldEvery = Mathf.Max(yieldEvery, approxNumNodesToUpdate/10);
  255. int counter = 0;
  256. // Recalculate the nodes
  257. // Take a look at the image in the docs for the UpdateGraph method
  258. // to see which nodes are being recalculated.
  259. for (int z = 0; z < depth; z++) {
  260. int xmin, xmax;
  261. if (z >= recalculateRect.ymin && z < recalculateRect.ymax) {
  262. xmin = 0;
  263. xmax = width;
  264. } else {
  265. xmin = recalculateRect.xmin;
  266. xmax = recalculateRect.xmax;
  267. }
  268. for (int x = xmin; x < xmax; x++) {
  269. graph.RecalculateCell(x, z, false, false);
  270. }
  271. counter += (xmax - xmin);
  272. if (counter > yieldEvery) {
  273. counter = 0;
  274. yield return null;
  275. }
  276. }
  277. for (int z = 0; z < depth; z++) {
  278. int xmin, xmax;
  279. if (z >= connectionRect.ymin && z < connectionRect.ymax) {
  280. xmin = 0;
  281. xmax = width;
  282. } else {
  283. xmin = connectionRect.xmin;
  284. xmax = connectionRect.xmax;
  285. }
  286. for (int x = xmin; x < xmax; x++) {
  287. graph.CalculateConnections(x, z);
  288. }
  289. counter += (xmax - xmin);
  290. if (counter > yieldEvery) {
  291. counter = 0;
  292. yield return null;
  293. }
  294. }
  295. yield return null;
  296. // Calculate all connections for the nodes along the boundary
  297. // of the graph, these always need to be updated
  298. /// <summary>TODO: Optimize to not traverse all nodes in the graph, only those at the edges</summary>
  299. for (int z = 0; z < depth; z++) {
  300. for (int x = 0; x < width; x++) {
  301. if (x == 0 || z == 0 || x == width-1 || z == depth-1) graph.CalculateConnections(x, z);
  302. }
  303. }
  304. } else {
  305. // The calculation will only update approximately this number of
  306. // nodes per frame. This is used to keep CPU load per frame low
  307. int yieldEvery = Mathf.Max(depth*width / 20, 1000);
  308. int counter = 0;
  309. // Just update all nodes
  310. for (int z = 0; z < depth; z++) {
  311. for (int x = 0; x < width; x++) {
  312. graph.RecalculateCell(x, z);
  313. }
  314. counter += width;
  315. if (counter > yieldEvery) {
  316. counter = 0;
  317. yield return null;
  318. }
  319. }
  320. // Recalculate the connections of all nodes
  321. for (int z = 0; z < depth; z++) {
  322. for (int x = 0; x < width; x++) {
  323. graph.CalculateConnections(x, z);
  324. }
  325. counter += width;
  326. if (counter > yieldEvery) {
  327. counter = 0;
  328. yield return null;
  329. }
  330. }
  331. }
  332. }
  333. }
  334. }