【Unity】NavMeshAgent与Animator及RootMotion的配合
Unity目前(2019.4)还没有内置开箱即用的将导航与动画整合的方案,这里提供了一个将NavMeshAgent和Animator整合的思路,并且兼容了RootMotion。
实现NavMeshAgent和Animator整合时,主要需要解决滑步问题和NavMeshAgent与RootMotion的数据同步问题,在不同的使用情境下,这两个问题有不同的解决方案。
如果是不带RootMotion的角色,需要避免角色滑步,将角色的导航移动速度换算成动画状态机的运动BlendTree控制参数即可,例如 animator.SetFloat("LocomotionSpeed", agent.velocity.magnitude/maxLocomotionSpeed) ,这里可能会有更复杂的换算公式,具体取决于BlendTree形式和其参数。
如果是带RootMotion的角色,并且通过RootMotion来使角色向目标点移动,无需担心滑步问题,但需要处理RootMotion数据和NavMeshAgent数据的同步。数据同步过程在 OnAnimatorMove() 方法中执行,主要步骤是:① 禁用NavMeshAgent组件的位置更新(agent.updatePosition = false );② 将RootMotion数据应用到NavMeshAgent组件中,驱动NavMeshAgent更新状态和进行导航模拟(agent.speed = animator.velocity.magnitude 和 agent.velocity = animator.velocity );③ 将导航模拟结果设置回角色的Transform上,避免RootMotion将角色移动到导航不可达区域(animator.transform.position = agent.nextPosition )。另外,需要在角色转身时对其移动速度进行衰减处理,以免当角色RootMotion速度太快并且目标点在角色身后附近时,角色陷入圆周运动无法抵达目标点。
如果是带RootMotion的角色,并且不使用NavMeshAgent目标点而是通过玩家的XY输入控制角色移动(例如使用键盘或摇杆操控主角),同样无需担心滑步问题,实现过程与上一种情况基本相同,差异是不需要处理转身时的移动速度衰减(除非有设计需求)。
一些额外的值得关注的点:① NavMeshAgent控制角色转向的速度非常慢,即使将AngualrSpeed设置为很大的值也依然很慢,可以禁用NavMeshAgent组件的旋转更新(agent.updateRotation = false ),并自行控制角色旋转(建议不要使用Lerp和SLerp,他们在向背面旋转时效果不好);② 如果某一帧时NavMeshAgent组件处于stop状态(agent.isStopped == true ),将其取消stop要等到下一帧才能生效,当帧为其设置目标点不会使导航生效。
主要代码:
using System;
using UnityEngine;
using UnityEngine.AI;
public enum NavCharacterDriveMode
{
NavDestination,
RootMotion,
Both,
}
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(NavMeshAgent))]
public class NavCharacterController : MonoBehaviour
{
public NavCharacterDriveMode CharacterDriveMode
{
get => _characterDriveMode;
set
{
_characterDriveMode = value;
UpdateCharacterDriver(_characterDriveMode);
}
}
public float CharacterTurningSpeed
{
get => _characterTurningSpeed;
set => _characterTurningSpeed = value;
}
public float CharacterMovementSpeed
{
get => _characterMovementSpeed;
set => _characterMovementSpeed = value;
}
public float BaseLocomotionParamValue
{
get => _baseLocomotionParamValue;
set => _baseLocomotionParamValue = value;
}
public string LocomotionParamName
{
get => _locomotionParamName;
set => _locomotionParamName = value;
}
[Tooltip("可导航角色位移驱动模式。")]
[SerializeField]
private NavCharacterDriveMode _characterDriveMode;
[Tooltip("角色转向速度(角度/秒)。")]
[Range(0f, 3600f)]
[SerializeField]
private float _characterTurningSpeed = 720f;
[Tooltip("角色移动速度(米/秒)。")]
[Range(0f, 1000f)]
[SerializeField]
private float _characterMovementSpeed = 1.4f;
[Tooltip("用于控制角色移动BlendTree的基准参数值。")]
[Range(0f, 1000f)]
[SerializeField]
private float _baseLocomotionParamValue = 1.0f;
[Tooltip("用于控制角色移动BlendTree的基准参数名。")]
[SerializeField]
private string _locomotionParamName = "MoveSpeed";
private NavMeshAgent _agent;
private Animator _animator;
private void OnValidate()
{
if (_agent && _animator)
{
UpdateCharacterDriver(CharacterDriveMode);
}
}
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_animator = GetComponent<Animator>();
_agent.updateRotation = false;
UpdateCharacterDriver(CharacterDriveMode);
}
private void Update()
{
if (CharacterDriveMode == NavCharacterDriveMode.RootMotion)
{
return;
}
if (CharacterDriveMode != NavCharacterDriveMode.RootMotion && Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray.origin, ray.direction, out var hitInfo, 1000, ~0, QueryTriggerInteraction.Ignore))
{
if (_agent.isStopped) { _agent.isStopped = false; }
_agent.SetDestination(hitInfo.point);
}
}
var turnPos = _agent.steeringTarget;
var dir = turnPos - _animator.transform.position;
dir.y = 0;
var forward = _animator.transform.forward;
var deflectionAngle = Vector3.SignedAngle(forward, dir, Vector3.up);
var maxTurnAngle = Mathf.Abs(deflectionAngle);
if (maxTurnAngle > 1)
{
var turnDir = deflectionAngle < 0 ? -1 : 1;
var turnAngle = CharacterTurningSpeed * turnDir * Time.deltaTime;
turnAngle = Mathf.Clamp(turnAngle, -maxTurnAngle, maxTurnAngle);
forward = Quaternion.AngleAxis(turnAngle, Vector3.up) * forward;
_animator.transform.forward = forward;
}
switch (CharacterDriveMode)
{
case NavCharacterDriveMode.NavDestination:
_agent.speed = CharacterMovementSpeed;
var locomotionParamValue0 = _agent.velocity.magnitude / CharacterMovementSpeed;
_animator.SetFloat(LocomotionParamName, locomotionParamValue0);
break;
case NavCharacterDriveMode.Both:
var remainingDistance = _agent.remainingDistance;
var locomotionParamValue1 = remainingDistance > _agent.radius ?
BaseLocomotionParamValue * (1 - Mathf.Pow(maxTurnAngle / 180, 3)) :
BaseLocomotionParamValue * remainingDistance / _agent.radius *
(180 - maxTurnAngle < Mathf.Epsilon ? 0 : Mathf.Pow(2, 10 * (1 - maxTurnAngle / 180) - 10));
_animator.SetFloat(LocomotionParamName, locomotionParamValue1);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void OnAnimatorMove()
{
switch (CharacterDriveMode)
{
case NavCharacterDriveMode.RootMotion:
case NavCharacterDriveMode.Both:
_animator.ApplyBuiltinRootMotion();
var animatorVelocity = _animator.velocity;
_agent.speed = animatorVelocity.magnitude;
_agent.velocity = animatorVelocity;
_animator.transform.position = _agent.nextPosition;
break;
}
}
private void UpdateCharacterDriver(NavCharacterDriveMode characterDriveMode)
{
switch (characterDriveMode)
{
case NavCharacterDriveMode.NavDestination:
_agent.updatePosition = true;
_animator.applyRootMotion = false;
break;
case NavCharacterDriveMode.RootMotion:
case NavCharacterDriveMode.Both:
_agent.updatePosition = false;
_animator.applyRootMotion = true;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
|