Files
BABA_YAGA/Assets/Opsive/UltimateCharacterController/Scripts/Character/Abilities/AI/NavMeshAgentMovement.cs
2026-06-14 23:57:44 +07:00

262 lines
12 KiB
C#

/// ---------------------------------------------
/// 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;
/// <summary>
/// Moves the character according to the NavMeshAgent desired velocity.
/// </summary>
[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; } } }
/// <summary>
/// Initialize the default values.
/// </summary>
public override void Awake()
{
base.Awake();
m_NavMeshAgent = GetComponent<NavMeshAgent>();
m_NavMeshAgent.autoTraverseOffMeshLink = false;
m_NavMeshAgent.updatePosition = false;
m_LastPathPendingFrame = int.MinValue;
m_NavMeshJumpArea = NavMesh.GetAreaFromName("Jump");
m_JumpAbility = m_CharacterLocomotion.GetAbility<Jump>();
m_FallAbility = m_CharacterLocomotion.GetAbility<Fall>();
EventHandler.RegisterEvent<bool>(m_GameObject, "OnCharacterGrounded", OnGrounded);
EventHandler.RegisterEvent<Vector3, Vector3, GameObject>(m_GameObject, "OnDeath", OnDeath);
EventHandler.RegisterEvent(m_GameObject, "OnRespawn", OnRespawn);
if (!Enabled) {
m_NavMeshAgent.enabled = false;
}
}
/// <summary>
/// Sets the destination of the pathfinding agent.
/// </summary>
/// <param name="target">The position to move towards.</param>
/// <returns>True if the destination was set.</returns>
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;
}
/// <summary>
/// Updates the ability.
/// </summary>
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();
}
/// <summary>
/// Ensure the move direction is valid.
/// </summary>
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;
}
/// <summary>
/// Updates the velocity and look rotation using the off mesh link.
/// </summary>
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);
}
}
}
}
/// <summary>
/// Can the ability be stopped?
/// </summary>
/// <returns>True if the ability can be stopped.</returns>
public override bool CanStopAbility()
{
if (!base.CanStopAbility()) {
return false;
}
return m_NavMeshAgent.hasPath && m_NavMeshAgent.remainingDistance <= m_ArrivedDistance;
}
/// <summary>
/// The ability has stopped running.
/// </summary>
/// <param name="force">Was the ability force stopped?</param>
protected override void AbilityStopped(bool force)
{
base.AbilityStopped(force);
if (!m_PrevEnabled) {
Enabled = false;
}
}
/// <summary>
/// The character has changed grounded state.
/// </summary>
/// <param name="grounded">Is the character on the ground?</param>
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);
}
}
}
/// <summary>
/// The character has died.
/// </summary>
/// <param name="position">The position of the force.</param>
/// <param name="force">The amount of force which killed the character.</param>
/// <param name="attacker">The GameObject that killed the character.</param>
private void OnDeath(Vector3 position, Vector3 force, GameObject attacker)
{
m_UpdateRotation = m_NavMeshAgent.updateRotation;
m_NavMeshAgent.updateRotation = false;
}
/// <summary>
/// The character has respawned. Start moving again.
/// </summary>
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;
}
/// <summary>
/// The character has been destroyed.
/// </summary>
public override void OnDestroy()
{
base.OnDestroy();
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnCharacterGrounded", OnGrounded);
EventHandler.UnregisterEvent<Vector3, Vector3, GameObject>(m_GameObject, "OnDeath", OnDeath);
EventHandler.UnregisterEvent(m_GameObject, "OnRespawn", OnRespawn);
}
}
}