RichAI.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. namespace Pathfinding {
  5. using Pathfinding.RVO;
  6. using Pathfinding.Util;
  7. [AddComponentMenu("Pathfinding/AI/RichAI (3D, for navmesh)")]
  8. /// <summary>
  9. /// Advanced AI for navmesh based graphs.
  10. ///
  11. /// [Open online documentation to see images]
  12. ///
  13. /// See: movementscripts (view in online documentation for working links)
  14. /// </summary>
  15. public partial class RichAI : AIBase, IAstarAI {
  16. /// <summary>
  17. /// Max acceleration of the agent.
  18. /// In world units per second per second.
  19. /// </summary>
  20. public float acceleration = 5;
  21. /// <summary>
  22. /// Max rotation speed of the agent.
  23. /// In degrees per second.
  24. /// </summary>
  25. public float rotationSpeed = 360;
  26. /// <summary>
  27. /// How long before reaching the end of the path to start to slow down.
  28. /// A lower value will make the agent stop more abruptly.
  29. ///
  30. /// Note: The agent may require more time to slow down if
  31. /// its maximum <see cref="acceleration"/> is not high enough.
  32. ///
  33. /// If set to zero the agent will not even attempt to slow down.
  34. /// This can be useful if the target point is not a point you want the agent to stop at
  35. /// but it might for example be the player and you want the AI to slam into the player.
  36. ///
  37. /// Note: A value of zero will behave differently from a small but non-zero value (such as 0.0001).
  38. /// When it is non-zero the agent will still respect its <see cref="acceleration"/> when determining if it needs
  39. /// to slow down, but if it is zero it will disable that check.
  40. /// This is useful if the <see cref="destination"/> is not a point where you want the agent to stop.
  41. ///
  42. /// \htmlonly <video class="tinyshadow" controls loop><source src="images/richai_slowdown_time.mp4" type="video/mp4"></video> \endhtmlonly
  43. /// </summary>
  44. public float slowdownTime = 0.5f;
  45. /// <summary>
  46. /// Max distance to the endpoint to consider it reached.
  47. ///
  48. /// See: <see cref="reachedEndOfPath"/>
  49. /// See: <see cref="OnTargetReached"/>
  50. /// </summary>
  51. public float endReachedDistance = 0.01f;
  52. /// <summary>
  53. /// Force to avoid walls with.
  54. /// The agent will try to steer away from walls slightly.
  55. ///
  56. /// See: <see cref="wallDist"/>
  57. /// </summary>
  58. public float wallForce = 3;
  59. /// <summary>
  60. /// Walls within this range will be used for avoidance.
  61. /// Setting this to zero disables wall avoidance and may improve performance slightly
  62. ///
  63. /// See: <see cref="wallForce"/>
  64. /// </summary>
  65. public float wallDist = 1;
  66. /// <summary>
  67. /// Use funnel simplification.
  68. /// On tiled navmesh maps, but sometimes on normal ones as well, it can be good to simplify
  69. /// the funnel as a post-processing step to make the paths straighter.
  70. ///
  71. /// This has a moderate performance impact during frames when a path calculation is completed.
  72. ///
  73. /// The RichAI script uses its own internal funnel algorithm, so you never
  74. /// need to attach the FunnelModifier component.
  75. ///
  76. /// [Open online documentation to see images]
  77. ///
  78. /// See: <see cref="Pathfinding.FunnelModifier"/>
  79. /// </summary>
  80. public bool funnelSimplification = false;
  81. /// <summary>
  82. /// Slow down when not facing the target direction.
  83. /// Incurs at a small performance overhead.
  84. /// </summary>
  85. public bool slowWhenNotFacingTarget = true;
  86. /// <summary>
  87. /// Called when the agent starts to traverse an off-mesh link.
  88. /// Register to this callback to handle off-mesh links in a custom way.
  89. ///
  90. /// If this event is set to null then the agent will fall back to traversing
  91. /// off-mesh links using a very simple linear interpolation.
  92. ///
  93. /// <code>
  94. /// void OnEnable () {
  95. /// ai = GetComponent<RichAI>();
  96. /// if (ai != null) ai.onTraverseOffMeshLink += TraverseOffMeshLink;
  97. /// }
  98. ///
  99. /// void OnDisable () {
  100. /// if (ai != null) ai.onTraverseOffMeshLink -= TraverseOffMeshLink;
  101. /// }
  102. ///
  103. /// IEnumerator TraverseOffMeshLink (RichSpecial link) {
  104. /// // Traverse the link over 1 second
  105. /// float startTime = Time.time;
  106. ///
  107. /// while (Time.time < startTime + 1) {
  108. /// transform.position = Vector3.Lerp(link.first.position, link.second.position, Time.time - startTime);
  109. /// yield return null;
  110. /// }
  111. /// transform.position = link.second.position;
  112. /// }
  113. /// </code>
  114. /// </summary>
  115. public System.Func<RichSpecial, IEnumerator> onTraverseOffMeshLink;
  116. /// <summary>Holds the current path that this agent is following</summary>
  117. protected readonly RichPath richPath = new RichPath();
  118. protected bool delayUpdatePath;
  119. protected bool lastCorner;
  120. /// <summary>Distance to <see cref="steeringTarget"/> in the movement plane</summary>
  121. protected float distanceToSteeringTarget = float.PositiveInfinity;
  122. protected readonly List<Vector3> nextCorners = new List<Vector3>();
  123. protected readonly List<Vector3> wallBuffer = new List<Vector3>();
  124. public bool traversingOffMeshLink { get; protected set; }
  125. /// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary>
  126. public float remainingDistance {
  127. get {
  128. return distanceToSteeringTarget + Vector3.Distance(steeringTarget, richPath.Endpoint);
  129. }
  130. }
  131. /// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary>
  132. public bool reachedEndOfPath { get { return approachingPathEndpoint && distanceToSteeringTarget < endReachedDistance; } }
  133. /// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
  134. public bool reachedDestination {
  135. get {
  136. if (!reachedEndOfPath) return false;
  137. // Note: distanceToSteeringTarget is the distance to the end of the path when approachingPathEndpoint is true
  138. if (approachingPathEndpoint && distanceToSteeringTarget + movementPlane.ToPlane(destination - richPath.Endpoint).magnitude > endReachedDistance) return false;
  139. // Don't do height checks in 2D mode
  140. if (orientation != OrientationMode.YAxisForward) {
  141. // Check if the destination is above the head of the character or far below the feet of it
  142. float yDifference;
  143. movementPlane.ToPlane(destination - position, out yDifference);
  144. var h = tr.localScale.y * height;
  145. if (yDifference > h || yDifference < -h*0.5) return false;
  146. }
  147. return true;
  148. }
  149. }
  150. /// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
  151. public bool hasPath { get { return richPath.GetCurrentPart() != null; } }
  152. /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
  153. public bool pathPending { get { return waitingForPathCalculation || delayUpdatePath; } }
  154. /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
  155. public Vector3 steeringTarget { get; protected set; }
  156. /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary>
  157. float IAstarAI.radius { get { return radius; } set { radius = value; } }
  158. /// <summary>\copydoc Pathfinding::IAstarAI::height</summary>
  159. float IAstarAI.height { get { return height; } set { height = value; } }
  160. /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary>
  161. float IAstarAI.maxSpeed { get { return maxSpeed; } set { maxSpeed = value; } }
  162. /// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary>
  163. bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } }
  164. /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
  165. bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } }
  166. /// <summary>
  167. /// True if approaching the last waypoint in the current part of the path.
  168. /// Path parts are separated by off-mesh links.
  169. ///
  170. /// See: <see cref="approachingPathEndpoint"/>
  171. /// </summary>
  172. public bool approachingPartEndpoint {
  173. get {
  174. return lastCorner && nextCorners.Count == 1;
  175. }
  176. }
  177. /// <summary>
  178. /// True if approaching the last waypoint of all parts in the current path.
  179. /// Path parts are separated by off-mesh links.
  180. ///
  181. /// See: <see cref="approachingPartEndpoint"/>
  182. /// </summary>
  183. public bool approachingPathEndpoint {
  184. get {
  185. return approachingPartEndpoint && richPath.IsLastPart;
  186. }
  187. }
  188. /// <summary>
  189. /// \copydoc Pathfinding::IAstarAI::Teleport
  190. ///
  191. /// When setting transform.position directly the agent
  192. /// will be clamped to the part of the navmesh it can
  193. /// reach, so it may not end up where you wanted it to.
  194. /// This ensures that the agent can move to any part of the navmesh.
  195. /// </summary>
  196. public override void Teleport (Vector3 newPosition, bool clearPath = true) {
  197. // Clamp the new position to the navmesh
  198. var nearest = AstarPath.active != null? AstarPath.active.GetNearest(newPosition) : new NNInfo();
  199. float elevation;
  200. movementPlane.ToPlane(newPosition, out elevation);
  201. newPosition = movementPlane.ToWorld(movementPlane.ToPlane(nearest.node != null ? nearest.position : newPosition), elevation);
  202. base.Teleport(newPosition, clearPath);
  203. }
  204. /// <summary>Called when the component is disabled</summary>
  205. protected override void OnDisable () {
  206. // Note that the AIBase.OnDisable call will also stop all coroutines
  207. base.OnDisable();
  208. traversingOffMeshLink = false;
  209. // Stop the off mesh link traversal coroutine
  210. StopAllCoroutines();
  211. }
  212. protected override bool shouldRecalculatePath {
  213. get {
  214. // Don't automatically recalculate the path in the middle of an off-mesh link
  215. return base.shouldRecalculatePath && !traversingOffMeshLink;
  216. }
  217. }
  218. public override void SearchPath () {
  219. // Calculate paths after the current off-mesh link has been completed
  220. if (traversingOffMeshLink) {
  221. delayUpdatePath = true;
  222. } else {
  223. base.SearchPath();
  224. }
  225. }
  226. protected override void OnPathComplete (Path p) {
  227. waitingForPathCalculation = false;
  228. p.Claim(this);
  229. if (p.error) {
  230. p.Release(this);
  231. return;
  232. }
  233. if (traversingOffMeshLink) {
  234. delayUpdatePath = true;
  235. } else {
  236. // The RandomPath and MultiTargetPath do not have a well defined destination that could have been
  237. // set before the paths were calculated. So we instead set the destination here so that some properties
  238. // like #reachedDestination and #remainingDistance work correctly.
  239. if (p is RandomPath rpath) {
  240. destination = rpath.originalEndPoint;
  241. } else if (p is MultiTargetPath mpath) {
  242. destination = mpath.originalEndPoint;
  243. }
  244. richPath.Initialize(seeker, p, true, funnelSimplification);
  245. // Check if we have already reached the end of the path
  246. // We need to do this here to make sure that the #reachedEndOfPath
  247. // property is up to date.
  248. var part = richPath.GetCurrentPart() as RichFunnel;
  249. if (part != null) {
  250. if (updatePosition) simulatedPosition = tr.position;
  251. // Note: UpdateTarget has some side effects like setting the nextCorners list and the lastCorner field
  252. var localPosition = movementPlane.ToPlane(UpdateTarget(part));
  253. // Target point
  254. steeringTarget = nextCorners[0];
  255. Vector2 targetPoint = movementPlane.ToPlane(steeringTarget);
  256. distanceToSteeringTarget = (targetPoint - localPosition).magnitude;
  257. if (lastCorner && nextCorners.Count == 1 && distanceToSteeringTarget <= endReachedDistance) {
  258. NextPart();
  259. }
  260. }
  261. }
  262. p.Release(this);
  263. }
  264. protected override void ClearPath () {
  265. CancelCurrentPathRequest();
  266. richPath.Clear();
  267. lastCorner = false;
  268. delayUpdatePath = false;
  269. distanceToSteeringTarget = float.PositiveInfinity;
  270. }
  271. /// <summary>
  272. /// Declare that the AI has completely traversed the current part.
  273. /// This will skip to the next part, or call OnTargetReached if this was the last part
  274. /// </summary>
  275. protected void NextPart () {
  276. if (!richPath.CompletedAllParts) {
  277. if (!richPath.IsLastPart) lastCorner = false;
  278. richPath.NextPart();
  279. if (richPath.CompletedAllParts) {
  280. OnTargetReached();
  281. }
  282. }
  283. }
  284. /// <summary>\copydoc Pathfinding::IAstarAI::GetRemainingPath</summary>
  285. public void GetRemainingPath (List<Vector3> buffer, out bool stale) {
  286. richPath.GetRemainingPath(buffer, simulatedPosition, out stale);
  287. }
  288. /// <summary>Called when the end of the path is reached</summary>
  289. protected virtual void OnTargetReached () {
  290. }
  291. protected virtual Vector3 UpdateTarget (RichFunnel fn) {
  292. nextCorners.Clear();
  293. // This method assumes simulatedPosition is up to date as our current position.
  294. // We read and write to tr.position as few times as possible since doing so
  295. // is much slower than to read and write from/to a local/member variable.
  296. bool requiresRepath;
  297. Vector3 position = fn.Update(simulatedPosition, nextCorners, 2, out lastCorner, out requiresRepath);
  298. if (requiresRepath && !waitingForPathCalculation && canSearch) {
  299. // TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated?
  300. SearchPath();
  301. }
  302. return position;
  303. }
  304. /// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary>
  305. protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
  306. if (updatePosition) simulatedPosition = tr.position;
  307. if (updateRotation) simulatedRotation = tr.rotation;
  308. RichPathPart currentPart = richPath.GetCurrentPart();
  309. if (currentPart is RichSpecial) {
  310. // Start traversing the off mesh link if we haven't done it yet
  311. if (!traversingOffMeshLink && !richPath.CompletedAllParts) {
  312. StartCoroutine(TraverseSpecial(currentPart as RichSpecial));
  313. }
  314. nextPosition = steeringTarget = simulatedPosition;
  315. nextRotation = rotation;
  316. } else {
  317. var funnel = currentPart as RichFunnel;
  318. // Check if we have a valid path to follow and some other script has not stopped the character
  319. if (funnel != null && !isStopped) {
  320. TraverseFunnel(funnel, deltaTime, out nextPosition, out nextRotation);
  321. } else {
  322. // Unknown, null path part, or the character is stopped
  323. // Slow down as quickly as possible
  324. velocity2D -= Vector2.ClampMagnitude(velocity2D, acceleration * deltaTime);
  325. FinalMovement(simulatedPosition, deltaTime, float.PositiveInfinity, 1f, out nextPosition, out nextRotation);
  326. steeringTarget = simulatedPosition;
  327. }
  328. }
  329. }
  330. void TraverseFunnel (RichFunnel fn, float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
  331. // Clamp the current position to the navmesh
  332. // and update the list of upcoming corners in the path
  333. // and store that in the 'nextCorners' field
  334. var position3D = UpdateTarget(fn);
  335. float elevation;
  336. Vector2 position = movementPlane.ToPlane(position3D, out elevation);
  337. // Only find nearby walls every 5th frame to improve performance
  338. if (Time.frameCount % 5 == 0 && wallForce > 0 && wallDist > 0) {
  339. wallBuffer.Clear();
  340. fn.FindWalls(wallBuffer, wallDist);
  341. }
  342. // Target point
  343. steeringTarget = nextCorners[0];
  344. Vector2 targetPoint = movementPlane.ToPlane(steeringTarget);
  345. // Direction to target
  346. Vector2 dir = targetPoint - position;
  347. // Normalized direction to the target
  348. Vector2 normdir = VectorMath.Normalize(dir, out distanceToSteeringTarget);
  349. // Calculate force from walls
  350. Vector2 wallForceVector = CalculateWallForce(position, elevation, normdir);
  351. Vector2 targetVelocity;
  352. if (approachingPartEndpoint) {
  353. targetVelocity = slowdownTime > 0 ? Vector2.zero : normdir * maxSpeed;
  354. // Reduce the wall avoidance force as we get closer to our target
  355. wallForceVector *= System.Math.Min(distanceToSteeringTarget/0.5f, 1);
  356. if (distanceToSteeringTarget <= endReachedDistance) {
  357. // Reached the end of the path or an off mesh link
  358. NextPart();
  359. }
  360. } else {
  361. var nextNextCorner = nextCorners.Count > 1 ? movementPlane.ToPlane(nextCorners[1]) : position + 2*dir;
  362. targetVelocity = (nextNextCorner - targetPoint).normalized * maxSpeed;
  363. }
  364. var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
  365. Vector2 accel = MovementUtilities.CalculateAccelerationToReachPoint(targetPoint - position, targetVelocity, velocity2D, acceleration, rotationSpeed, maxSpeed, forwards);
  366. // Update the velocity using the acceleration
  367. velocity2D += (accel + wallForceVector*wallForce)*deltaTime;
  368. // Distance to the end of the path (almost as the crow flies)
  369. var distanceToEndOfPath = distanceToSteeringTarget + Vector3.Distance(steeringTarget, fn.exactEnd);
  370. var slowdownFactor = distanceToEndOfPath < maxSpeed * slowdownTime? Mathf.Sqrt(distanceToEndOfPath / (maxSpeed * slowdownTime)) : 1;
  371. FinalMovement(position3D, deltaTime, distanceToEndOfPath, slowdownFactor, out nextPosition, out nextRotation);
  372. }
  373. void FinalMovement (Vector3 position3D, float deltaTime, float distanceToEndOfPath, float slowdownFactor, out Vector3 nextPosition, out Quaternion nextRotation) {
  374. var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
  375. velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, slowdownFactor, slowWhenNotFacingTarget && enableRotation, forwards);
  376. ApplyGravity(deltaTime);
  377. if (rvoController != null && rvoController.enabled) {
  378. // Send a message to the RVOController that we want to move
  379. // with this velocity. In the next simulation step, this
  380. // velocity will be processed and it will be fed back to the
  381. // rvo controller and finally it will be used by this script
  382. // when calling the CalculateMovementDelta method below
  383. // Make sure that we don't move further than to the end point
  384. // of the path. If the RVO simulation FPS is low and we did
  385. // not do this, the agent might overshoot the target a lot.
  386. var rvoTarget = position3D + movementPlane.ToWorld(Vector2.ClampMagnitude(velocity2D, distanceToEndOfPath));
  387. rvoController.SetTarget(rvoTarget, velocity2D.magnitude, maxSpeed);
  388. }
  389. // Direction and distance to move during this frame
  390. var deltaPosition = lastDeltaPosition = CalculateDeltaToMoveThisFrame(movementPlane.ToPlane(position3D), distanceToEndOfPath, deltaTime);
  391. // Rotate towards the direction we are moving in
  392. // Slow down the rotation of the character very close to the endpoint of the path to prevent oscillations
  393. var rotationSpeedFactor = approachingPartEndpoint ? Mathf.Clamp01(1.1f * slowdownFactor - 0.1f) : 1f;
  394. nextRotation = enableRotation ? SimulateRotationTowards(deltaPosition, rotationSpeed * rotationSpeedFactor * deltaTime) : simulatedRotation;
  395. nextPosition = position3D + movementPlane.ToWorld(deltaPosition, verticalVelocity * deltaTime);
  396. }
  397. protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
  398. if (richPath != null) {
  399. var funnel = richPath.GetCurrentPart() as RichFunnel;
  400. if (funnel != null) {
  401. var clampedPosition = funnel.ClampToNavmesh(position);
  402. // We cannot simply check for equality because some precision may be lost
  403. // if any coordinate transformations are used.
  404. var difference = movementPlane.ToPlane(clampedPosition - position);
  405. float sqrDifference = difference.sqrMagnitude;
  406. if (sqrDifference > 0.001f*0.001f) {
  407. // The agent was outside the navmesh. Remove that component of the velocity
  408. // so that the velocity only goes along the direction of the wall, not into it
  409. velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference;
  410. // Make sure the RVO system knows that there was a collision here
  411. // Otherwise other agents may think this agent continued
  412. // to move forwards and avoidance quality may suffer
  413. if (rvoController != null && rvoController.enabled) {
  414. rvoController.SetCollisionNormal(difference);
  415. }
  416. positionChanged = true;
  417. // Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate
  418. return position + movementPlane.ToWorld(difference);
  419. }
  420. }
  421. }
  422. positionChanged = false;
  423. return position;
  424. }
  425. Vector2 CalculateWallForce (Vector2 position, float elevation, Vector2 directionToTarget) {
  426. if (wallForce <= 0 || wallDist <= 0) return Vector2.zero;
  427. float wLeft = 0;
  428. float wRight = 0;
  429. var position3D = movementPlane.ToWorld(position, elevation);
  430. for (int i = 0; i < wallBuffer.Count; i += 2) {
  431. Vector3 closest = VectorMath.ClosestPointOnSegment(wallBuffer[i], wallBuffer[i+1], position3D);
  432. float dist = (closest-position3D).sqrMagnitude;
  433. if (dist > wallDist*wallDist) continue;
  434. Vector2 tang = movementPlane.ToPlane(wallBuffer[i+1]-wallBuffer[i]).normalized;
  435. // Using the fact that all walls are laid out clockwise (looking from inside the obstacle)
  436. // Then left and right (ish) can be figured out like this
  437. float dot = Vector2.Dot(directionToTarget, tang);
  438. float weight = 1 - System.Math.Max(0, (2*(dist / (wallDist*wallDist))-1));
  439. if (dot > 0) wRight = System.Math.Max(wRight, dot * weight);
  440. else wLeft = System.Math.Max(wLeft, -dot * weight);
  441. }
  442. Vector2 normal = new Vector2(directionToTarget.y, -directionToTarget.x);
  443. return normal*(wRight-wLeft);
  444. }
  445. /// <summary>Traverses an off-mesh link</summary>
  446. protected virtual IEnumerator TraverseSpecial (RichSpecial link) {
  447. traversingOffMeshLink = true;
  448. // The current path part is a special part, for example a link
  449. // Movement during this part of the path is handled by the TraverseSpecial coroutine
  450. velocity2D = Vector3.zero;
  451. var offMeshLinkCoroutine = onTraverseOffMeshLink != null? onTraverseOffMeshLink(link) : TraverseOffMeshLinkFallback(link);
  452. yield return StartCoroutine(offMeshLinkCoroutine);
  453. // Off-mesh link traversal completed
  454. traversingOffMeshLink = false;
  455. NextPart();
  456. // If a path completed during the time we traversed the special connection, we need to recalculate it
  457. if (delayUpdatePath) {
  458. delayUpdatePath = false;
  459. // TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated?
  460. if (canSearch) SearchPath();
  461. }
  462. }
  463. /// <summary>
  464. /// Fallback for traversing off-mesh links in case <see cref="onTraverseOffMeshLink"/> is not set.
  465. /// This will do a simple linear interpolation along the link.
  466. /// </summary>
  467. protected IEnumerator TraverseOffMeshLinkFallback (RichSpecial link) {
  468. float duration = maxSpeed > 0 ? Vector3.Distance(link.second.position, link.first.position) / maxSpeed : 1;
  469. float startTime = Time.time;
  470. while (true) {
  471. var pos = Vector3.Lerp(link.first.position, link.second.position, Mathf.InverseLerp(startTime, startTime + duration, Time.time));
  472. if (updatePosition) tr.position = pos;
  473. else simulatedPosition = pos;
  474. if (Time.time >= startTime + duration) break;
  475. yield return null;
  476. }
  477. }
  478. protected static readonly Color GizmoColorPath = new Color(8.0f/255, 78.0f/255, 194.0f/255);
  479. protected override void OnDrawGizmos () {
  480. base.OnDrawGizmos();
  481. if (tr != null) {
  482. Gizmos.color = GizmoColorPath;
  483. Vector3 lastPosition = position;
  484. for (int i = 0; i < nextCorners.Count; lastPosition = nextCorners[i], i++) {
  485. Gizmos.DrawLine(lastPosition, nextCorners[i]);
  486. }
  487. }
  488. }
  489. protected override int OnUpgradeSerializedData (int version, bool unityThread) {
  490. #pragma warning disable 618
  491. if (unityThread && animCompatibility != null) anim = animCompatibility;
  492. #pragma warning restore 618
  493. return base.OnUpgradeSerializedData(version, unityThread);
  494. }
  495. }
  496. }