using UnityEngine; using UnityEngine.AI; namespace ECM2 { /// /// This component extends a Character (through composition) adding navigation capabilities using a NavMeshAgent. /// This replaces the previous AgentCharacter class. /// [RequireComponent(typeof(Character)), RequireComponent(typeof(NavMeshAgent))] public class NavMeshCharacter : MonoBehaviour { #region EDITOR EXPOSED FIELDS [Space(15f)] [Tooltip("Should the agent brake automatically to avoid overshooting the destination point? \n" + "If true, the agent will brake automatically as it nears the destination.")] [SerializeField] private bool _autoBraking; [Tooltip("Distance from target position to start braking.")] [SerializeField] private float _brakingDistance; [Tooltip("Stop within this distance from the target position.")] [SerializeField] private float _stoppingDistance; #endregion #region FIELDS private NavMeshAgent _agent; private Character _character; #endregion #region PROPERTIES /// /// Cached NavMeshAgent component. /// public NavMeshAgent agent => _agent; /// /// Cached Character component. /// public Character character => _character; /// /// Should the agent brake automatically to avoid overshooting the destination point? /// If this property is set to true, the agent will brake automatically as it nears the destination. /// public bool autoBraking { get => _autoBraking; set { _autoBraking = value; agent.autoBraking = _autoBraking; } } /// /// Distance from target position to start braking. /// public float brakingDistance { get => _brakingDistance; set => _brakingDistance = Mathf.Max(0.0001f, value); } /// /// The ratio (0 - 1 range) of the agent's remaining distance and the braking distance. /// 1 If no auto braking or if agent's remaining distance is greater than brakingDistance. /// less than 1, if agent's remaining distance is less than brakingDistance. /// public float brakingRatio { get { if (!autoBraking) return 1f; return agent.hasPath ? Mathf.InverseLerp(0.0f, brakingDistance, agent.remainingDistance) : 1.0f; } } /// /// Stop within this distance from the target position. /// public float stoppingDistance { get => _stoppingDistance; set { _stoppingDistance = Mathf.Max(0.0f, value); agent.stoppingDistance = _stoppingDistance; } } #endregion #region EVENTS public delegate void DestinationReachedEventHandler(); /// /// Event triggered when agent reaches its destination. /// public event DestinationReachedEventHandler DestinationReached; /// /// Trigger DestinationReached event. /// Called when agent reaches its destination. /// public virtual void OnDestinationReached() { DestinationReached?.Invoke(); } #endregion #region METHODS /// /// Cache used components. /// protected virtual void CacheComponents() { _agent = GetComponent(); _character = GetComponent(); } /// /// Does the Agent currently has a path? /// public virtual bool HasPath() { return agent.hasPath; } /// /// True if Agent is following a path, false otherwise. /// public virtual bool IsPathFollowing() { return agent.hasPath && !agent.isStopped; } /// /// Returns the destination set for this agent. /// If a destination is set but the path is not yet processed, /// the returned position will be valid navmesh position that's closest to the previously set position. /// If the agent has no path or requested path - returns the agents position on the navmesh. /// If the agent is not mapped to the navmesh (e.g. Scene has no navmesh) - returns a position at infinity. /// public virtual Vector3 GetDestination() { return agent.destination; } /// /// Requests the character to move to the valid navmesh position that's closest to the requested destination. /// public virtual void MoveToDestination(Vector3 destination) { Vector3 worldUp = -character.GetGravityDirection(); Vector3 toDestination2D = Vector3.ProjectOnPlane(destination - character.position, worldUp); if (toDestination2D.sqrMagnitude >= MathLib.Square(stoppingDistance)) agent.SetDestination(destination); } /// /// Pause / Resume Character path following movement. /// If set to True, the character's movement will be stopped along its current path. /// If set to False after the character has stopped, it will resume moving along its current path. /// public virtual void PauseMovement(bool pause) { agent.isStopped = pause; character.SetMovementDirection(Vector3.zero); } /// /// Halts Character's current path following movement. /// This will clear agent's current path. /// public virtual void StopMovement() { agent.ResetPath(); character.SetMovementDirection(Vector3.zero); } /// /// Computes the analog input modifier (0.0f to 1.0f) based on Character's max speed and given desired velocity. /// protected virtual float ComputeAnalogInputModifier(Vector3 desiredVelocity) { float maxSpeed = _character.GetMaxSpeed(); if (desiredVelocity.sqrMagnitude > 0.0f && maxSpeed > 0.00000001f) return Mathf.Clamp01(desiredVelocity.magnitude / maxSpeed); return 0.0f; } /// /// Calculates Character movement direction from a given desired velocity factoring (if enabled) auto braking. /// protected virtual Vector3 CalcMovementDirection(Vector3 desiredVelocity) { Vector3 worldUp = -character.GetGravityDirection(); Vector3 desiredVelocity2D = Vector3.ProjectOnPlane(desiredVelocity, worldUp); Vector3 scaledDesiredVelocity2D = desiredVelocity2D * brakingRatio; float minAnalogSpeed = _character.GetMinAnalogSpeed(); if (scaledDesiredVelocity2D.sqrMagnitude < MathLib.Square(minAnalogSpeed)) scaledDesiredVelocity2D = scaledDesiredVelocity2D.normalized * minAnalogSpeed; return Vector3.ClampMagnitude(scaledDesiredVelocity2D, ComputeAnalogInputModifier(scaledDesiredVelocity2D)); } /// /// Makes the character's follow Agent's path (if any). /// Eg: Keep updating Character's movement direction vector to steer towards Agent's destination until reached. /// protected virtual void DoPathFollowing() { if (!IsPathFollowing()) return; // Is destination reached ? if (agent.remainingDistance <= stoppingDistance) { // Destination is reached, stop movement StopMovement(); // Trigger event OnDestinationReached(); } else { // If destination not reached, request a Character to move towards agent's desired velocity direction Vector3 movementDirection = CalcMovementDirection(agent.desiredVelocity); character.SetMovementDirection(movementDirection); } } /// /// Synchronize the NavMeshAgent with Character (eg: speed, acceleration, velocity, etc) as we moves the Agent. /// protected virtual void SyncNavMeshAgent() { agent.angularSpeed = _character.rotationRate; agent.speed = _character.GetMaxSpeed(); agent.acceleration = _character.GetMaxAcceleration(); agent.velocity = _character.GetVelocity(); agent.nextPosition = _character.GetPosition(); agent.radius = _character.radius; agent.height = _character.height; } /// /// On MovementMode change, stop agent movement if character is not walking or falling. /// protected virtual void OnMovementModeChanged(Character.MovementMode prevMovementMode, int prevCustomMovementMode) { if (!character.IsWalking() || !character.IsFalling()) { StopMovement(); } } /// /// While Character has a valid path, do path following. /// protected virtual void OnBeforeSimulationUpdated(float deltaTime) { DoPathFollowing(); } #endregion #region MONOBEHAVIOUR /// /// If overriden, base method MUST be called. /// private void Reset() { _autoBraking = true; _brakingDistance = 2.0f; _stoppingDistance = 1.0f; } /// /// If overriden, base method MUST be called. /// private void OnValidate() { if (_agent == null) _agent = GetComponent(); brakingDistance = _brakingDistance; stoppingDistance = _stoppingDistance; } /// /// If overriden, base method MUST be called. /// protected virtual void Awake() { // Cache used components CacheComponents(); // Initialize NavMeshAgent agent.autoBraking = autoBraking; agent.stoppingDistance = stoppingDistance; // Turn-off NavMeshAgent auto-control, // we control it (see SyncNavMeshAgent method) agent.updatePosition = false; agent.updateRotation = false; agent.updateUpAxis = false; } /// /// If overriden, base method MUST be called. /// protected virtual void OnEnable() { // Subscribe to Character events character.MovementModeChanged += OnMovementModeChanged; character.BeforeSimulationUpdated += OnBeforeSimulationUpdated; } /// /// If overriden, base method MUST be called. /// protected virtual void OnDisable() { // Un-Subscribe to Character events character.MovementModeChanged -= OnMovementModeChanged; character.BeforeSimulationUpdated -= OnBeforeSimulationUpdated; } /// /// If overriden, base method MUST be called. /// protected virtual void LateUpdate() { SyncNavMeshAgent(); } #endregion } }