Files
Fishing2/Assets/ECM2/Source/Components/NavMeshCharacter.cs
2025-05-11 00:46:26 +08:00

401 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.AI;
namespace ECM2
{
/// <summary>
/// This component extends a Character (through composition) adding navigation capabilities using a NavMeshAgent.
/// This replaces the previous AgentCharacter class.
/// </summary>
[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
/// <summary>
/// Cached NavMeshAgent component.
/// </summary>
public NavMeshAgent agent => _agent;
/// <summary>
/// Cached Character component.
/// </summary>
public Character character => _character;
/// <summary>
/// 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.
/// </summary>
public bool autoBraking
{
get => _autoBraking;
set
{
_autoBraking = value;
agent.autoBraking = _autoBraking;
}
}
/// <summary>
/// Distance from target position to start braking.
/// </summary>
public float brakingDistance
{
get => _brakingDistance;
set => _brakingDistance = Mathf.Max(0.0001f, value);
}
/// <summary>
/// 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.
/// </summary>
public float brakingRatio
{
get
{
if (!autoBraking)
return 1f;
return agent.hasPath ? Mathf.InverseLerp(0.0f, brakingDistance, agent.remainingDistance) : 1.0f;
}
}
/// <summary>
/// Stop within this distance from the target position.
/// </summary>
public float stoppingDistance
{
get => _stoppingDistance;
set
{
_stoppingDistance = Mathf.Max(0.0f, value);
agent.stoppingDistance = _stoppingDistance;
}
}
#endregion
#region EVENTS
public delegate void DestinationReachedEventHandler();
/// <summary>
/// Event triggered when agent reaches its destination.
/// </summary>
public event DestinationReachedEventHandler DestinationReached;
/// <summary>
/// Trigger DestinationReached event.
/// Called when agent reaches its destination.
/// </summary>
public virtual void OnDestinationReached()
{
DestinationReached?.Invoke();
}
#endregion
#region METHODS
/// <summary>
/// Cache used components.
/// </summary>
protected virtual void CacheComponents()
{
_agent = GetComponent<NavMeshAgent>();
_character = GetComponent<Character>();
}
/// <summary>
/// Does the Agent currently has a path?
/// </summary>
public virtual bool HasPath()
{
return agent.hasPath;
}
/// <summary>
/// True if Agent is following a path, false otherwise.
/// </summary>
public virtual bool IsPathFollowing()
{
return agent.hasPath && !agent.isStopped;
}
/// <summary>
/// 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.
/// </summary>
public virtual Vector3 GetDestination()
{
return agent.destination;
}
/// <summary>
/// Requests the character to move to the valid navmesh position that's closest to the requested destination.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
public virtual void PauseMovement(bool pause)
{
agent.isStopped = pause;
character.SetMovementDirection(Vector3.zero);
}
/// <summary>
/// Halts Character's current path following movement.
/// This will clear agent's current path.
/// </summary>
public virtual void StopMovement()
{
agent.ResetPath();
character.SetMovementDirection(Vector3.zero);
}
/// <summary>
/// Computes the analog input modifier (0.0f to 1.0f) based on Character's max speed and given desired velocity.
/// </summary>
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;
}
/// <summary>
/// Calculates Character movement direction from a given desired velocity factoring (if enabled) auto braking.
/// </summary>
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));
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// Synchronize the NavMeshAgent with Character (eg: speed, acceleration, velocity, etc) as we moves the Agent.
/// </summary>
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;
}
/// <summary>
/// On MovementMode change, stop agent movement if character is not walking or falling.
/// </summary>
protected virtual void OnMovementModeChanged(Character.MovementMode prevMovementMode, int prevCustomMovementMode)
{
if (!character.IsWalking() || !character.IsFalling())
{
StopMovement();
}
}
/// <summary>
/// While Character has a valid path, do path following.
/// </summary>
protected virtual void OnBeforeSimulationUpdated(float deltaTime)
{
DoPathFollowing();
}
#endregion
#region MONOBEHAVIOUR
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
private void Reset()
{
_autoBraking = true;
_brakingDistance = 2.0f;
_stoppingDistance = 1.0f;
}
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
private void OnValidate()
{
if (_agent == null)
_agent = GetComponent<NavMeshAgent>();
brakingDistance = _brakingDistance;
stoppingDistance = _stoppingDistance;
}
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
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;
}
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
protected virtual void OnEnable()
{
// Subscribe to Character events
character.MovementModeChanged += OnMovementModeChanged;
character.BeforeSimulationUpdated += OnBeforeSimulationUpdated;
}
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
protected virtual void OnDisable()
{
// Un-Subscribe to Character events
character.MovementModeChanged -= OnMovementModeChanged;
character.BeforeSimulationUpdated -= OnBeforeSimulationUpdated;
}
/// <summary>
/// If overriden, base method MUST be called.
/// </summary>
protected virtual void LateUpdate()
{
SyncNavMeshAgent();
}
#endregion
}
}