/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Character.Abilities.AI { using Opsive.Shared.Events; using UnityEngine; using UnityEngine.AI; /// /// Moves the character according to the NavMeshAgent desired velocity. /// [RequireComponent(typeof(NavMeshAgent))] public class NavMeshAgentMovement : PathfindingMovement { [Tooltip("The agent has arrived at the destination when the remaining distance is less than the arrived distance.")] [SerializeField] protected float m_ArrivedDistance = 0.2f; public float ArrivedDistance { get { return m_ArrivedDistance; } set { m_ArrivedDistance = value; } } private NavMeshAgent m_NavMeshAgent; private Jump m_JumpAbility; private Fall m_FallAbility; private bool m_PrevEnabled = true; private Vector2 m_InputVector; private Vector3 m_DeltaRotation; private bool m_UpdateRotation; private int m_LastPathPendingFrame; private int m_NavMeshJumpArea; public override Vector2 InputVector { get { return m_InputVector; } } public override Vector3 DeltaRotation { get { return m_DeltaRotation; } } public override bool Enabled { set { base.Enabled = value; if (m_NavMeshAgent != null) { m_NavMeshAgent.enabled = value; } } } /// /// Initialize the default values. /// public override void Awake() { base.Awake(); m_NavMeshAgent = GetComponent(); m_NavMeshAgent.autoTraverseOffMeshLink = false; m_NavMeshAgent.updatePosition = false; m_LastPathPendingFrame = int.MinValue; m_NavMeshJumpArea = NavMesh.GetAreaFromName("Jump"); m_JumpAbility = m_CharacterLocomotion.GetAbility(); m_FallAbility = m_CharacterLocomotion.GetAbility(); EventHandler.RegisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded); EventHandler.RegisterEvent(m_GameObject, "OnDeath", OnDeath); EventHandler.RegisterEvent(m_GameObject, "OnRespawn", OnRespawn); if (!Enabled) { m_NavMeshAgent.enabled = false; } } /// /// Sets the destination of the pathfinding agent. /// /// The position to move towards. /// True if the destination was set. public override bool SetDestination(Vector3 target) { // Set the new destination if the ability is already active. if (m_NavMeshAgent.hasPath && IsActive) { return m_NavMeshAgent.SetDestination(target); } // The NavMeshAgent must be enabled in order to set the destination. m_PrevEnabled = Enabled; Enabled = true; // Move towards the destination. if (m_NavMeshAgent.isOnNavMesh && m_NavMeshAgent.SetDestination(target)) { StartAbility(); return true; } Enabled = m_PrevEnabled; return false; } /// /// Updates the ability. /// public override void Update() { m_InputVector = Vector2.zero; var lookRotation = Quaternion.LookRotation(m_Transform.forward, m_CharacterLocomotion.Up); if (m_NavMeshAgent.isOnOffMeshLink) { UpdateOffMeshLink(); } else { // When the path is pending the desired velocity isn't correct. Add a small buffer to ensure the path is valid. if (m_NavMeshAgent.pathPending) { m_LastPathPendingFrame = Time.frameCount; } // Only move if a path exists. if (m_NavMeshAgent.velocity.sqrMagnitude > 0.01f && m_NavMeshAgent.remainingDistance > 0.01f && m_LastPathPendingFrame + 2 < Time.frameCount) { Vector3 velocity; if (m_NavMeshAgent.updateRotation) { lookRotation = Quaternion.LookRotation(m_NavMeshAgent.velocity, m_CharacterLocomotion.Up); // The normalized velocity should be relative to the target rotation. velocity = Quaternion.Inverse(lookRotation) * m_NavMeshAgent.velocity; } else { velocity = m_Transform.InverseTransformDirection(m_NavMeshAgent.velocity); } // Only normalize if the magnitude is greater than 1. This will allow the character to walk. if (velocity.sqrMagnitude > 1) { velocity.Normalize(); } m_InputVector.x = velocity.x; m_InputVector.y = velocity.z; } } var rotation = lookRotation * Quaternion.Inverse(m_Transform.rotation); m_DeltaRotation.y = Utility.MathUtility.ClampInnerAngle(rotation.eulerAngles.y); base.Update(); } /// /// Ensure the move direction is valid. /// public override void ApplyPosition() { if (m_NavMeshAgent.remainingDistance < m_NavMeshAgent.stoppingDistance) { // Prevent the character from jittering back and forth to land precisely on the target. var direction = m_Transform.InverseTransformPoint(m_NavMeshAgent.destination); var moveDirection = m_Transform.InverseTransformDirection(m_CharacterLocomotion.MoveDirection); if (Mathf.Abs(moveDirection.x) > Mathf.Abs(direction.x)) { moveDirection.x = direction.x; } if (Mathf.Abs(moveDirection.z) > Mathf.Abs(direction.z)) { moveDirection.z = direction.z; } m_CharacterLocomotion.MoveDirection = m_Transform.TransformDirection(moveDirection); } m_NavMeshAgent.nextPosition = m_Transform.position + m_CharacterLocomotion.MoveDirection; } /// /// Updates the velocity and look rotation using the off mesh link. /// protected virtual void UpdateOffMeshLink() { if (m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeDropDown || m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeJumpAcross || (m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeManual && m_NavMeshAgent.currentOffMeshLinkData.offMeshLink.area == m_NavMeshJumpArea)) { // Ignore the y difference when determining a look direction and velocity. // This will give XZ distances a greater impact when normalized. var direction = m_NavMeshAgent.currentOffMeshLinkData.endPos - m_Transform.position; direction.y = 0; if (direction.sqrMagnitude > 0.1f || m_CharacterLocomotion.Grounded) { var nextPositionDirection = m_Transform.InverseTransformPoint(m_NavMeshAgent.currentOffMeshLinkData.endPos); nextPositionDirection.y = 0; nextPositionDirection.Normalize(); m_InputVector.x = nextPositionDirection.x; m_InputVector.y = nextPositionDirection.z; } // Jump if the agent hasn't jumped yet. if (m_JumpAbility != null && (m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeJumpAcross || (m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeManual && m_NavMeshAgent.currentOffMeshLinkData.offMeshLink.area == m_NavMeshJumpArea))) { if (!m_JumpAbility.IsActive && (m_FallAbility == null || !m_FallAbility.IsActive)) { m_CharacterLocomotion.TryStartAbility(m_JumpAbility); } } } } /// /// Can the ability be stopped? /// /// True if the ability can be stopped. public override bool CanStopAbility() { if (!base.CanStopAbility()) { return false; } return m_NavMeshAgent.hasPath && m_NavMeshAgent.remainingDistance <= m_ArrivedDistance; } /// /// The ability has stopped running. /// /// Was the ability force stopped? protected override void AbilityStopped(bool force) { base.AbilityStopped(force); if (!m_PrevEnabled) { Enabled = false; } } /// /// The character has changed grounded state. /// /// Is the character on the ground? protected virtual void OnGrounded(bool grounded) { if (grounded && m_NavMeshAgent.enabled) { // The agent is no longer on an off mesh link if they just landed. if (m_NavMeshAgent.isOnOffMeshLink && (m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeDropDown || m_NavMeshAgent.currentOffMeshLinkData.linkType == OffMeshLinkType.LinkTypeJumpAcross)) { m_NavMeshAgent.CompleteOffMeshLink(); } // Warp the NavMeshAgent just in case the navmesh position doesn't match the transform position. var destination = m_NavMeshAgent.destination; m_NavMeshAgent.Warp(m_Transform.position); // Warp can change the destination so make sure that doesn't happen. if (m_NavMeshAgent.destination != destination) { m_NavMeshAgent.SetDestination(destination); } } } /// /// The character has died. /// /// The position of the force. /// The amount of force which killed the character. /// The GameObject that killed the character. private void OnDeath(Vector3 position, Vector3 force, GameObject attacker) { m_UpdateRotation = m_NavMeshAgent.updateRotation; m_NavMeshAgent.updateRotation = false; } /// /// The character has respawned. Start moving again. /// private void OnRespawn() { // Reset the NavMeshAgent to the new position. m_NavMeshAgent.Warp(m_Transform.position); if (m_NavMeshAgent.isOnOffMeshLink) { m_NavMeshAgent.ActivateCurrentOffMeshLink(false); } m_NavMeshAgent.updateRotation = m_UpdateRotation; m_LastPathPendingFrame = int.MinValue; } /// /// The character has been destroyed. /// public override void OnDestroy() { base.OnDestroy(); EventHandler.UnregisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded); EventHandler.UnregisterEvent(m_GameObject, "OnDeath", OnDeath); EventHandler.UnregisterEvent(m_GameObject, "OnRespawn", OnRespawn); } } }