MovementUtilities.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using UnityEngine;
  2. using System.Collections;
  3. namespace Pathfinding.Util {
  4. public static class MovementUtilities {
  5. /// <summary>
  6. /// Clamps the velocity to the max speed and optionally the forwards direction.
  7. ///
  8. /// Note that all vectors are 2D vectors, not 3D vectors.
  9. ///
  10. /// Returns: The clamped velocity in world units per second.
  11. /// </summary>
  12. /// <param name="velocity">Desired velocity of the character. In world units per second.</param>
  13. /// <param name="maxSpeed">Max speed of the character. In world units per second.</param>
  14. /// <param name="slowdownFactor">Value between 0 and 1 which determines how much slower the character should move than normal.
  15. /// Normally 1 but should go to 0 when the character approaches the end of the path.</param>
  16. /// <param name="slowWhenNotFacingTarget">Prevent the velocity from being too far away from the forward direction of the character
  17. /// and slow the character down if the desired velocity is not in the same direction as the forward vector.</param>
  18. /// <param name="forward">Forward direction of the character. Used together with the slowWhenNotFacingTarget parameter.</param>
  19. public static Vector2 ClampVelocity (Vector2 velocity, float maxSpeed, float slowdownFactor, bool slowWhenNotFacingTarget, Vector2 forward) {
  20. // Max speed to use for this frame
  21. var currentMaxSpeed = maxSpeed * slowdownFactor;
  22. // Check if the agent should slow down in case it is not facing the direction it wants to move in
  23. if (slowWhenNotFacingTarget && (forward.x != 0 || forward.y != 0)) {
  24. float currentSpeed;
  25. var normalizedVelocity = VectorMath.Normalize(velocity, out currentSpeed);
  26. float dot = Vector2.Dot(normalizedVelocity, forward);
  27. // Lower the speed when the character's forward direction is not pointing towards the desired velocity
  28. // 1 when velocity is in the same direction as forward
  29. // 0.2 when they point in the opposite directions
  30. float directionSpeedFactor = Mathf.Clamp(dot+0.707f, 0.2f, 1.0f);
  31. currentMaxSpeed *= directionSpeedFactor;
  32. currentSpeed = Mathf.Min(currentSpeed, currentMaxSpeed);
  33. // Angle between the forwards direction of the character and our desired velocity
  34. float angle = Mathf.Acos(Mathf.Clamp(dot, -1, 1));
  35. // Clamp the angle to 20 degrees
  36. // We cannot keep the velocity exactly in the forwards direction of the character
  37. // because we use the rotation to determine in which direction to rotate and if
  38. // the velocity would always be in the forwards direction of the character then
  39. // the character would never rotate.
  40. // Allow larger angles when near the end of the path to prevent oscillations.
  41. angle = Mathf.Min(angle, (20f + 180f*(1 - slowdownFactor*slowdownFactor))*Mathf.Deg2Rad);
  42. float sin = Mathf.Sin(angle);
  43. float cos = Mathf.Cos(angle);
  44. // Determine if we should rotate clockwise or counter-clockwise to move towards the current velocity
  45. sin *= Mathf.Sign(normalizedVelocity.x*forward.y - normalizedVelocity.y*forward.x);
  46. // Rotate the #forward vector by #angle radians
  47. // The rotation is done using an inlined rotation matrix.
  48. // See https://en.wikipedia.org/wiki/Rotation_matrix
  49. return new Vector2(forward.x*cos + forward.y*sin, forward.y*cos - forward.x*sin) * currentSpeed;
  50. } else {
  51. return Vector2.ClampMagnitude(velocity, currentMaxSpeed);
  52. }
  53. }
  54. /// <summary>Calculate an acceleration to move deltaPosition units and get there with approximately a velocity of targetVelocity</summary>
  55. public static Vector2 CalculateAccelerationToReachPoint (Vector2 deltaPosition, Vector2 targetVelocity, Vector2 currentVelocity, float forwardsAcceleration, float rotationSpeed, float maxSpeed, Vector2 forwardsVector) {
  56. // Guard against div by zero
  57. if (forwardsAcceleration <= 0) return Vector2.zero;
  58. float currentSpeed = currentVelocity.magnitude;
  59. // Convert rotation speed to an acceleration
  60. // See https://en.wikipedia.org/wiki/Centripetal_force
  61. var sidewaysAcceleration = currentSpeed * rotationSpeed * Mathf.Deg2Rad;
  62. // To avoid weird behaviour when the rotation speed is very low we allow the agent to accelerate sideways without rotating much
  63. // if the rotation speed is very small. Also guards against division by zero.
  64. sidewaysAcceleration = Mathf.Max(sidewaysAcceleration, forwardsAcceleration);
  65. // Transform coordinates to local space where +X is the forwards direction
  66. // This is essentially equivalent to Transform.InverseTransformDirection.
  67. deltaPosition = VectorMath.ComplexMultiplyConjugate(deltaPosition, forwardsVector);
  68. targetVelocity = VectorMath.ComplexMultiplyConjugate(targetVelocity, forwardsVector);
  69. currentVelocity = VectorMath.ComplexMultiplyConjugate(currentVelocity, forwardsVector);
  70. float ellipseSqrFactorX = 1 / (forwardsAcceleration*forwardsAcceleration);
  71. float ellipseSqrFactorY = 1 / (sidewaysAcceleration*sidewaysAcceleration);
  72. // If the target velocity is zero we can use a more fancy approach
  73. // and calculate a nicer path.
  74. // In particular, this is the case at the end of the path.
  75. if (targetVelocity == Vector2.zero) {
  76. // Run a binary search over the time to get to the target point.
  77. float mn = 0.01f;
  78. float mx = 10;
  79. while (mx - mn > 0.01f) {
  80. var time = (mx + mn) * 0.5f;
  81. // Given that we want to move deltaPosition units from out current position, that our current velocity is given
  82. // and that when we reach the target we want our velocity to be zero. Also assume that our acceleration will
  83. // vary linearly during the slowdown. Then we can calculate what our acceleration should be during this frame.
  84. //{ t = time
  85. //{ deltaPosition = vt + at^2/2 + qt^3/6
  86. //{ 0 = v + at + qt^2/2
  87. //{ solve for a
  88. // a = acceleration vector
  89. // q = derivative of the acceleration vector
  90. var a = (6*deltaPosition - 4*time*currentVelocity)/(time*time);
  91. var q = 6*(time*currentVelocity - 2*deltaPosition)/(time*time*time);
  92. // Make sure the acceleration is not greater than our maximum allowed acceleration.
  93. // If it is we increase the time we want to use to get to the target
  94. // and if it is not, we decrease the time to get there faster.
  95. // Since the acceleration is described by acceleration = a + q*t
  96. // we only need to check at t=0 and t=time.
  97. // Note that the acceleration limit is described by an ellipse, not a circle.
  98. var nextA = a + q*time;
  99. if (a.x*a.x*ellipseSqrFactorX + a.y*a.y*ellipseSqrFactorY > 1.0f || nextA.x*nextA.x*ellipseSqrFactorX + nextA.y*nextA.y*ellipseSqrFactorY > 1.0f) {
  100. mn = time;
  101. } else {
  102. mx = time;
  103. }
  104. }
  105. var finalAcceleration = (6*deltaPosition - 4*mx*currentVelocity)/(mx*mx);
  106. // Boosting
  107. {
  108. // The trajectory calculated above has a tendency to use very wide arcs
  109. // and that does unfortunately not look particularly good in some cases.
  110. // Here we amplify the component of the acceleration that is perpendicular
  111. // to our current velocity. This will make the agent turn towards the
  112. // target quicker.
  113. // How much amplification to use. Value is unitless.
  114. const float Boost = 1;
  115. finalAcceleration.y *= 1 + Boost;
  116. // Clamp the velocity to the maximum acceleration.
  117. // Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle.
  118. float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY;
  119. if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude);
  120. }
  121. return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector);
  122. } else {
  123. // Here we try to move towards the next waypoint which has been modified slightly using our
  124. // desired velocity at that point so that the agent will more smoothly round the corner.
  125. // How much to strive for making sure we reach the target point with the target velocity. Unitless.
  126. const float TargetVelocityWeight = 0.5f;
  127. // Limit to how much to care about the target velocity. Value is in seconds.
  128. // This prevents the character from moving away from the path too much when the target point is far away
  129. const float TargetVelocityWeightLimit = 1.5f;
  130. float targetSpeed;
  131. var normalizedTargetVelocity = VectorMath.Normalize(targetVelocity, out targetSpeed);
  132. var distance = deltaPosition.magnitude;
  133. var targetPoint = deltaPosition - normalizedTargetVelocity * System.Math.Min(TargetVelocityWeight * distance * targetSpeed / (currentSpeed + targetSpeed), maxSpeed*TargetVelocityWeightLimit);
  134. // How quickly the agent will try to reach the velocity that we want it to have.
  135. // We need this to prevent oscillations and jitter which is what happens if
  136. // we let the constant go towards zero. Value is in seconds.
  137. const float TimeToReachDesiredVelocity = 0.1f;
  138. // TODO: Clamp to ellipse using more accurate acceleration (use rotation speed as well)
  139. var finalAcceleration = (targetPoint.normalized*maxSpeed - currentVelocity) * (1f/TimeToReachDesiredVelocity);
  140. // Clamp the velocity to the maximum acceleration.
  141. // Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle.
  142. float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY;
  143. if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude);
  144. return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector);
  145. }
  146. }
  147. }
  148. }