using UnityEngine; namespace Pathfinding.Examples { #if MODULE_ENTITIES using Pathfinding.ECS; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; #endif /// /// Example of how to use Mecanim with the included movement scripts. /// /// This script will use Mecanim to apply root motion to move the character /// instead of allowing the movement script to do the movement. /// /// It assumes that the Mecanim controller uses 3 input variables /// - InputMagnitude which is simply 1 when the character should be moving and 0 when it should stop. Or, for the FollowerEntity component, 1 when it is moving at its natural speed, and less than 1 when it is moving slower. /// - X which is component of the desired movement along the left/right axis. For the AIPath and RichAI movement scripts, this will be a velocity in meters/second, while for the FollowerEntity movement script, this will be an angular velocity in radians/second. /// - Y which is component of the desired movement direction along the forward/backward axis. This is a velocity in meters/second. /// /// It works with the , and movement scripts. /// /// See: https://docs.unity3d.com/Manual/RootMotion.html /// See: /// See: /// See: /// See: /// [HelpURL("https://arongranberg.com/astar/documentation/stable/mecanimbridge.html")] public class MecanimBridge : VersionedMonoBehaviour { /// Smoothing factor for the velocity, in seconds. public float velocitySmoothing = 1; /// /// Smoothing factor for the angular velocity, in seconds. /// /// Note: This is only used with the movement script. /// public float angularVelocitySmoothing = 1; public float naturalSpeed = 5; #if MODULE_ENTITIES float smoothedRotationSpeed; #endif /// Cached reference to the movement script IAstarAI ai; /// Cached Animator component Animator anim; /// Cached Transform component Transform tr; Vector3 smoothedVelocity; /// Position of the left and right feet during the previous frame Vector3[] prevFootPos = new Vector3[2]; /// Cached reference to the left and right feet Transform[] footTransforms; const string InputMagnitudeKey = "InputMagnitude"; static int InputMagnitudeKeyHash = Animator.StringToHash(InputMagnitudeKey); const string NormalizedSpeedKey = "NormalizedSpeed"; static int NormalizedSpeedKeyHash = Animator.StringToHash(NormalizedSpeedKey); const string XAxisKey = "X"; static int XAxisKeyHash = Animator.StringToHash(XAxisKey); const string YAxisKey = "Y"; static int YAxisKeyHash = Animator.StringToHash(YAxisKey); protected override void Awake () { base.Awake(); ai = GetComponent(); anim = GetComponent(); tr = transform; // Find the feet of the character footTransforms = new [] { anim.GetBoneTransform(HumanBodyBones.LeftFoot), anim.GetBoneTransform(HumanBodyBones.RightFoot) }; if (anim != null) { if (!HasParameter(anim, InputMagnitudeKey)) { Debug.LogError($"No '{InputMagnitudeKey}' parameter found on the animator. The animator must have a float parameter called '{InputMagnitudeKey}'", this); enabled = false; } if (!HasParameter(anim, XAxisKey)) { Debug.LogError($"No '{XAxisKey}' parameter found on the animator. The animator must have a float parameter called '{XAxisKey}'", this); enabled = false; } if (!HasParameter(anim, YAxisKey)) { Debug.LogError($"No '{YAxisKey}' parameter found on the animator. The animator must have a float parameter called '{YAxisKey}'", this); enabled = false; } } } static bool HasParameter (Animator animator, string paramName) { foreach (AnimatorControllerParameter param in animator.parameters) if (param.name == paramName) return true; return false; } #if MODULE_ENTITIES void OnEnable () { if (ai is FollowerEntity followerEntity) { followerEntity.movementOverrides.AddBeforeMovementCallback(MovementOverride); } } void OnDisable () { if (ai is FollowerEntity followerEntity) { followerEntity.movementOverrides.RemoveBeforeMovementCallback(MovementOverride); } } void MovementOverride (Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement) { var desiredVelocity = math.normalizesafe(resolvedMovement.targetPoint - localTransform.Position) * resolvedMovement.speed; var currentRotation = movementPlane.value.ToPlane(localTransform.Rotation); var deltaRotationSpeed = AstarMath.DeltaAngle(currentRotation, resolvedMovement.targetRotation); deltaRotationSpeed = Mathf.Sign(deltaRotationSpeed) * Mathf.Clamp01(Mathf.Abs(deltaRotationSpeed) / math.max(0.001f, dt * resolvedMovement.rotationSpeed)); deltaRotationSpeed = -deltaRotationSpeed * resolvedMovement.rotationSpeed; smoothedRotationSpeed = Mathf.Lerp(smoothedRotationSpeed, deltaRotationSpeed, angularVelocitySmoothing > 0 ? dt / angularVelocitySmoothing : 1); // Calculate the desired velocity relative to the character (+Z = forward, +X = right) var localDesiredVelocity = localTransform.InverseTransformDirection(desiredVelocity); localDesiredVelocity.y = 0; smoothedVelocity = Vector3.Lerp(smoothedVelocity, localDesiredVelocity, velocitySmoothing > 0 ? dt / velocitySmoothing : 1); if (smoothedVelocity.magnitude < 0.4f) { smoothedVelocity = smoothedVelocity.normalized * 0.4f; } var normalizedRotationSpeed = movementSettings.follower.maxRotationSpeed > 0 ? Mathf.Rad2Deg * Mathf.Abs(resolvedMovement.rotationSpeed) / movementSettings.follower.maxRotationSpeed : 0; var normalizedSpeed = movementSettings.follower.speed * naturalSpeed > 0 ? resolvedMovement.speed / naturalSpeed : 0; // Combine the normalized rotation speed and normalized speed such that either of them being large, results in the input magnitude being large. // This is to ensure that even if the agent wants to almost rotate on the spot, the input magnitude will still be large. var inputMagnitude = Mathf.Min(1, Mathf.Sqrt(normalizedSpeed*normalizedSpeed + normalizedRotationSpeed*normalizedRotationSpeed)); anim.SetFloat(InputMagnitudeKeyHash, inputMagnitude); anim.SetFloat(XAxisKeyHash, smoothedRotationSpeed); anim.SetFloat(YAxisKeyHash, smoothedVelocity.z); // Calculate how much the agent should rotate during this frame var nextPosition = localTransform.Position; var nextRotation = localTransform.Rotation; // Apply rotational root motion nextRotation = anim.deltaRotation * nextRotation; nextPosition += (float3)anim.deltaPosition; resolvedMovement.targetPoint = nextPosition; resolvedMovement.targetRotation = movementPlane.value.ToPlane(nextRotation); // target rotation speed? resolvedMovement.speed = math.length(nextPosition - localTransform.Position) / math.max(0.001f, dt); } #endif /// Update is called once per frame void Update () { if (ai is AIBase aiBase) { aiBase.canMove = false; // aiBase.updatePosition = false; // aiBase.updateRotation = false; } } /// Calculate position of the currently grounded foot Vector3 CalculateBlendPoint () { // Fall back to rotating around the transform position if no feet could be found if (footTransforms[0] == null || footTransforms[1] == null) return tr.position; var leftFootPos = footTransforms[0].position; var rightFootPos = footTransforms[1].position; // This is the same calculation that Unity uses for // Animator.pivotWeight and Animator.pivotPosition // but those properties do not work for all animations apparently. var footVelocity1 = (leftFootPos - prevFootPos[0]) / Time.deltaTime; var footVelocity2 = (rightFootPos - prevFootPos[1]) / Time.deltaTime; float denominator = footVelocity1.magnitude + footVelocity2.magnitude; var pivotWeight = denominator > 0 ? footVelocity1.magnitude / denominator : 0.5f; prevFootPos[0] = leftFootPos; prevFootPos[1] = rightFootPos; var pivotPosition = Vector3.Lerp(leftFootPos, rightFootPos, pivotWeight); return pivotPosition; } void OnAnimatorMove () { #if MODULE_ENTITIES if (ai is FollowerEntity) return; #endif Vector3 nextPosition; Quaternion nextRotation; ai.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation); //var desiredVelocity = (ai.steeringTarget - tr.position).normalized * 2;//ai.desiredVelocity; var desiredVelocity = ai.desiredVelocity; // Calculate the desired velocity relative to the character (+Z = forward, +X = right) var localDesiredVelocity = tr.InverseTransformDirection(desiredVelocity); localDesiredVelocity.y = 0; var desiredVelocityWithoutGrav = tr.TransformDirection(localDesiredVelocity); anim.SetFloat(InputMagnitudeKeyHash, ai.reachedEndOfPath || localDesiredVelocity.magnitude < 0.1f ? 0f : 1f); smoothedVelocity = Vector3.Lerp(smoothedVelocity, localDesiredVelocity, velocitySmoothing > 0 ? Time.deltaTime / velocitySmoothing : 1); if (smoothedVelocity.magnitude < 0.4f) { smoothedVelocity = smoothedVelocity.normalized * 0.4f; } anim.SetFloat(XAxisKeyHash, smoothedVelocity.x); anim.SetFloat(YAxisKeyHash, smoothedVelocity.z); // The IAstarAI interface doesn't expose rotation speeds right now, so we have to do this ugly thing. // In case this is an unknown movement script, we fall back to a reasonable value. var rotationSpeed = 360f; if (ai is AIPath aipath) { rotationSpeed = aipath.rotationSpeed; } else if (ai is RichAI richai) { rotationSpeed = richai.rotationSpeed; } // Calculate how much the agent should rotate during this frame var newRot = RotateTowards(desiredVelocityWithoutGrav, Time.deltaTime * rotationSpeed); // Rotate the character around the currently grounded foot to prevent foot sliding nextPosition = ai.position; nextRotation = ai.rotation; nextPosition = RotatePointAround(nextPosition, CalculateBlendPoint(), newRot * Quaternion.Inverse(nextRotation)); nextRotation = newRot; // Apply rotational root motion nextRotation = anim.deltaRotation * nextRotation; // Use gravity from the movement script, not from animation var deltaPos = anim.deltaPosition; deltaPos.y = desiredVelocity.y * Time.deltaTime; nextPosition += deltaPos; // Call the movement script to perform the final movement ai.FinalizeMovement(nextPosition, nextRotation); } static Vector3 RotatePointAround (Vector3 point, Vector3 around, Quaternion rotation) { return rotation * (point - around) + around; } /// /// Calculates a rotation closer to the desired direction. /// Returns: The new rotation for the character /// /// Direction in the movement plane to rotate toward. /// Maximum number of degrees to rotate this frame. protected virtual Quaternion RotateTowards (Vector3 direction, float maxDegrees) { if (direction != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(direction); return Quaternion.RotateTowards(tr.rotation, targetRotation, maxDegrees); } else { return tr.rotation; } } } }