StartEndModifier.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. namespace Pathfinding {
  4. [System.Serializable]
  5. /// <summary>
  6. /// Adjusts start and end points of a path.
  7. ///
  8. /// This modifier is included in the <see cref="Pathfinding.Seeker"/> component and is always used if you are using a Seeker.
  9. /// When a path is calculated the resulting path will only be the positions of the nodes it passes through.
  10. /// However often you may not want to navigate to the center of a specific node but instead to a point on the surface of a node.
  11. /// This modifier will adjust the endpoints of the path.
  12. ///
  13. /// [Open online documentation to see images]
  14. /// </summary>
  15. public class StartEndModifier : PathModifier {
  16. public override int Order { get { return 0; } }
  17. /// <summary>
  18. /// Add points to the path instead of replacing them.
  19. /// If for example <see cref="exactEndPoint"/> is set to ClosestOnNode then the path will be modified so that
  20. /// the path goes first to the center of the last node in the path and then goes to the closest point
  21. /// on the node to the end point in the path request.
  22. ///
  23. /// If this is false however then the relevant points in the path will simply be replaced.
  24. /// In the above example the path would go directly to the closest point on the node without passing
  25. /// through the center of the node.
  26. /// </summary>
  27. public bool addPoints;
  28. /// <summary>
  29. /// How the start point of the path will be determined.
  30. /// See: <see cref="Exactness"/>
  31. /// </summary>
  32. public Exactness exactStartPoint = Exactness.ClosestOnNode;
  33. /// <summary>
  34. /// How the end point of the path will be determined.
  35. /// See: <see cref="Exactness"/>
  36. /// </summary>
  37. public Exactness exactEndPoint = Exactness.ClosestOnNode;
  38. /// <summary>
  39. /// Will be called when a path is processed.
  40. /// The value which is returned will be used as the start point of the path
  41. /// and potentially clamped depending on the value of the <see cref="exactStartPoint"/> field.
  42. /// Only used for the Original, Interpolate and NodeConnection modes.
  43. /// </summary>
  44. public System.Func<Vector3> adjustStartPoint;
  45. /// <summary>
  46. /// Sets where the start and end points of a path should be placed.
  47. ///
  48. /// Here is a legend showing what the different items in the above images represent.
  49. /// The images above show a path coming in from the top left corner and ending at a node next to an obstacle as well as 2 different possible end points of the path and how they would be modified.
  50. /// [Open online documentation to see images]
  51. /// </summary>
  52. public enum Exactness {
  53. /// <summary>
  54. /// The point is snapped to the position of the first/last node in the path.
  55. /// Use this if your game is very tile based and you want your agents to stop precisely at the center of the nodes.
  56. /// If you recalculate the path while the agent is moving you may want the start point snapping to be ClosestOnNode and the end point snapping to be SnapToNode however
  57. /// as while an agent is moving it will likely not be right at the center of a node.
  58. ///
  59. /// [Open online documentation to see images]
  60. /// </summary>
  61. SnapToNode,
  62. /// <summary>
  63. /// The point is set to the exact point which was passed when creating the path request.
  64. /// Note that if a path was for example requested to a point inside an obstacle, then the last point of the path will be inside that obstacle, which is usually not what you want.
  65. /// Consider using the <see cref="ClosestOnNode"/> option instead.
  66. ///
  67. /// [Open online documentation to see images]
  68. /// </summary>
  69. Original,
  70. /// <summary>
  71. /// The point is set to the closest point on the line between either the two first points or the two last points.
  72. /// Usually you will want to use the NodeConnection mode instead since that is usually the behaviour that you really want.
  73. /// This mode exists mostly for compatibility reasons.
  74. /// [Open online documentation to see images]
  75. /// Deprecated: Use NodeConnection instead.
  76. /// </summary>
  77. Interpolate,
  78. /// <summary>
  79. /// The point is set to the closest point on the surface of the node. Note that some node types (point nodes) do not have a surface, so the "closest point" is simply the node's position which makes this identical to <see cref="Exactness.SnapToNode"/>.
  80. /// This is the mode that you almost always want to use in a free movement 3D world.
  81. /// [Open online documentation to see images]
  82. /// </summary>
  83. ClosestOnNode,
  84. /// <summary>
  85. /// The point is set to the closest point on one of the connections from the start/end node.
  86. /// This mode may be useful in a grid based or point graph based world when using the AILerp script.
  87. ///
  88. /// Note: If you are using this mode with a <see cref="Pathfinding.PointGraph"/> you probably also want to use the <see cref="Pathfinding.PointGraph.NodeDistanceMode Connection"/> for <see cref="Pathfinding.PointGraph.nearestNodeDistanceMode"/>.
  89. ///
  90. /// [Open online documentation to see images]
  91. /// </summary>
  92. NodeConnection,
  93. }
  94. /// <summary>
  95. /// Do a straight line check from the node's center to the point determined by the <see cref="Exactness"/>.
  96. /// There are very few cases where you will want to use this. It is mostly here for
  97. /// backwards compatibility reasons.
  98. ///
  99. /// Version: Since 4.1 this field only has an effect for the <see cref="Exactness"/> mode Original because that's the only one where it makes sense.
  100. /// </summary>
  101. public bool useRaycasting;
  102. public LayerMask mask = -1;
  103. /// <summary>
  104. /// Do a straight line check from the node's center to the point determined by the <see cref="Exactness"/>.
  105. /// See: <see cref="useRaycasting"/>
  106. ///
  107. /// Version: Since 4.1 this field only has an effect for the <see cref="Exactness"/> mode Original because that's the only one where it makes sense.
  108. /// </summary>
  109. public bool useGraphRaycasting;
  110. List<GraphNode> connectionBuffer;
  111. System.Action<GraphNode> connectionBufferAddDelegate;
  112. public override void Apply (Path _p) {
  113. var p = _p as ABPath;
  114. // This modifier only supports ABPaths (doesn't make much sense for other paths anyway)
  115. if (p == null || p.vectorPath.Count == 0) return;
  116. bool singleNode = false;
  117. if (p.vectorPath.Count == 1 && !addPoints) {
  118. // Duplicate first point
  119. p.vectorPath.Add(p.vectorPath[0]);
  120. singleNode = true;
  121. }
  122. // Add instead of replacing points
  123. bool forceAddStartPoint, forceAddEndPoint;
  124. // Which connection the start/end point was on (only used for the Connection mode)
  125. int closestStartConnection, closestEndConnection;
  126. Vector3 pStart = Snap(p, exactStartPoint, true, out forceAddStartPoint, out closestStartConnection);
  127. Vector3 pEnd = Snap(p, exactEndPoint, false, out forceAddEndPoint, out closestEndConnection);
  128. // This is a special case when the path is only a single node and the Connection mode is used.
  129. // (forceAddStartPoint/forceAddEndPoint is only used for the Connection mode)
  130. // In this case the start and end points lie on the connections of the node.
  131. // There are two cases:
  132. // 1. If the start and end points lie on the same connection we do *not* want
  133. // the path to pass through the node center but instead go directly from point to point.
  134. // This is the case of closestStartConnection == closestEndConnection.
  135. // 2. If the start and end points lie on different connections we *want*
  136. // the path to pass through the node center as it goes from one connection to another one.
  137. // However in any case we only want the node center to be added once to the path
  138. // so we set forceAddStartPoint to false anyway.
  139. if (singleNode) {
  140. if (closestStartConnection == closestEndConnection) {
  141. forceAddStartPoint = false;
  142. forceAddEndPoint = false;
  143. } else {
  144. forceAddStartPoint = false;
  145. }
  146. }
  147. // Add or replace the start point
  148. // Disable adding of points if the mode is SnapToNode since then
  149. // the first item in vectorPath will very likely be the same as the
  150. // position of the first node
  151. if ((forceAddStartPoint || addPoints) && exactStartPoint != Exactness.SnapToNode) {
  152. p.vectorPath.Insert(0, pStart);
  153. } else {
  154. p.vectorPath[0] = pStart;
  155. }
  156. if ((forceAddEndPoint || addPoints) && exactEndPoint != Exactness.SnapToNode) {
  157. p.vectorPath.Add(pEnd);
  158. } else {
  159. p.vectorPath[p.vectorPath.Count-1] = pEnd;
  160. }
  161. }
  162. Vector3 Snap (ABPath path, Exactness mode, bool start, out bool forceAddPoint, out int closestConnectionIndex) {
  163. var index = start ? 0 : path.path.Count - 1;
  164. var node = path.path[index];
  165. var nodePos = (Vector3)node.position;
  166. closestConnectionIndex = 0;
  167. forceAddPoint = false;
  168. switch (mode) {
  169. case Exactness.ClosestOnNode:
  170. return start ? path.startPoint : path.endPoint;
  171. case Exactness.SnapToNode:
  172. return nodePos;
  173. case Exactness.Original:
  174. case Exactness.Interpolate:
  175. case Exactness.NodeConnection:
  176. Vector3 relevantPoint;
  177. if (start) {
  178. relevantPoint = adjustStartPoint != null? adjustStartPoint() : path.originalStartPoint;
  179. } else {
  180. relevantPoint = path.originalEndPoint;
  181. }
  182. switch (mode) {
  183. case Exactness.Original:
  184. return GetClampedPoint(nodePos, relevantPoint, node);
  185. case Exactness.Interpolate:
  186. // Adjacent node to either the start node or the end node in the path
  187. var adjacentNode = path.path[Mathf.Clamp(index + (start ? 1 : -1), 0, path.path.Count-1)];
  188. return VectorMath.ClosestPointOnSegment(nodePos, (Vector3)adjacentNode.position, relevantPoint);
  189. case Exactness.NodeConnection:
  190. // This code uses some tricks to avoid allocations
  191. // even though it uses delegates heavily
  192. // The connectionBufferAddDelegate delegate simply adds whatever node
  193. // it is called with to the connectionBuffer
  194. connectionBuffer = connectionBuffer ?? new List<GraphNode>();
  195. connectionBufferAddDelegate = connectionBufferAddDelegate ?? (System.Action<GraphNode>)connectionBuffer.Add;
  196. // Adjacent node to either the start node or the end node in the path
  197. adjacentNode = path.path[Mathf.Clamp(index + (start ? 1 : -1), 0, path.path.Count-1)];
  198. // Add all neighbours of #node to the connectionBuffer
  199. node.GetConnections(connectionBufferAddDelegate);
  200. var bestPos = nodePos;
  201. var bestDist = float.PositiveInfinity;
  202. // Loop through all neighbours
  203. // Do it in reverse order because the length of the connectionBuffer
  204. // will change during iteration
  205. for (int i = connectionBuffer.Count - 1; i >= 0; i--) {
  206. var neighbour = connectionBuffer[i];
  207. if (!path.CanTraverse(neighbour)) continue;
  208. // Find the closest point on the connection between the nodes
  209. // and check if the distance to that point is lower than the previous best
  210. var closest = VectorMath.ClosestPointOnSegment(nodePos, (Vector3)neighbour.position, relevantPoint);
  211. var dist = (closest - relevantPoint).sqrMagnitude;
  212. if (dist < bestDist) {
  213. bestPos = closest;
  214. bestDist = dist;
  215. closestConnectionIndex = i;
  216. // If this node is not the adjacent node
  217. // then the path should go through the start node as well
  218. forceAddPoint = neighbour != adjacentNode;
  219. }
  220. }
  221. connectionBuffer.Clear();
  222. return bestPos;
  223. default:
  224. throw new System.ArgumentException("Cannot reach this point, but the compiler is not smart enough to realize that.");
  225. }
  226. default:
  227. throw new System.ArgumentException("Invalid mode");
  228. }
  229. }
  230. protected Vector3 GetClampedPoint (Vector3 from, Vector3 to, GraphNode hint) {
  231. Vector3 point = to;
  232. RaycastHit hit;
  233. if (useRaycasting && Physics.Linecast(from, to, out hit, mask)) {
  234. point = hit.point;
  235. }
  236. if (useGraphRaycasting && hint != null) {
  237. var rayGraph = AstarData.GetGraph(hint) as IRaycastableGraph;
  238. if (rayGraph != null) {
  239. GraphHitInfo graphHit;
  240. if (rayGraph.Linecast(from, point, out graphHit)) {
  241. point = graphHit.point;
  242. }
  243. }
  244. }
  245. return point;
  246. }
  247. }
  248. }