[Unity角色控制专题] (借助ai)详细解析官方第三人称控制器
首先模板链接在这里,你可以直接下载并导入unity即可查看官方为开发者写好一套控制器
本文的ai工具用到了豆包,其灵活程度很高,总结能力也强过我太多 因此大量使用,不喜勿喷
Starter Assets - ThirdPerson | Updates in new CharacterController package | 必备工具 | Unity Asset Store
目录
一.前提准备
虚拟相机
角色控制器
新输入系统
动画状态机
二.玩家输入处理类
先看代码
变量/方法图解释
类图
三 .第三人称控制类
整体代码
类图编辑
分步解析
1.初始化
2.交互处理
3.移动方法
4.跳跃和重力处理
5.着地检测
6.相机旋转处理
7..动画处理
四.角色推动刚体类
一.前提准备
虚拟相机
位置
角色控制器
新输入系统
动画状态机
Idel walk run blend
二.玩家输入处理类
先看代码
其实这个脚本没什么好说的,仅仅是用新输入系统处理了输入的逻辑 还没有将其应用于角色实际的运动,相当于地基 因此我将其放在了本文章的最开始的部分
注意InputValue 是新输入系统的一个重要的结构体,其内部使用一种灵活的数据存储方式,可以根据不同的输入类型存储相应的数据,当调用
Get<T>()
方法时,它会尝试将存储的数据转换为指定的类型,如果转换成功,则返回转换后的值;如果转换失败,可能会抛出异常或者返回默认值,具体取决于输入系统的实现
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
namespace StarterAssets
{
// 该类用于处理角色的输入逻辑
public class StarterAssetsInputs : MonoBehaviour
{
[Header("角色输入值")]
// 角色的移动输入向量,包含水平和垂直方向
public Vector2 move;
// 相机的视角输入向量,包含水平和垂直方向
public Vector2 look;
public bool jump;
public bool sprint;
[Header("移动设置")]
// 是否使用模拟输入进行移动
public bool analogMovement;
[Header("鼠标光标设置")]
// 是否锁定鼠标光标
public bool cursorLocked = true;
// 是否使用鼠标光标输入来控制视角
public bool cursorInputForLook = true;
#if ENABLE_INPUT_SYSTEM
public void OnMove(InputValue value)
{
MoveInput(value.Get<Vector2>());
}
// 处理视角输入事件
public void OnLook(InputValue value)
{
// 仅当允许使用鼠标光标输入控制视角时才处理
if (cursorInputForLook)
{
// 将输入的视角向量传递给 LookInput 方法
LookInput(value.Get<Vector2>());
}
}
// 处理跳跃输入事件
public void OnJump(InputValue value)
{
// 将跳跃键的按下状态传递给 JumpInput 方法
JumpInput(value.isPressed);
}
// 处理冲刺输入事件
public void OnSprint(InputValue value)
{
// 将冲刺键的按下状态传递给 SprintInput 方法
SprintInput(value.isPressed);
}
#endif
public void MoveInput(Vector2 newMoveDirection)
{
move = newMoveDirection;
}
// 设置相机的视角输入向量
public void LookInput(Vector2 newLookDirection)
{
look = newLookDirection;
}
public void JumpInput(bool newJumpState)
{
jump = newJumpState;
}
public void SprintInput(bool newSprintState)
{
sprint = newSprintState;
}
// 当应用程序获得或失去焦点时调用
private void OnApplicationFocus(bool hasFocus)
{
SetCursorState(cursorLocked);
}
// 设置鼠标光标的锁定状态
private void SetCursorState(bool newState)
{
// 如果 newState 为 true,则锁定鼠标光标;否则解锁
Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None;
}
}
}
变量/方法图解释
变量名 | 类型 | 说明 |
---|---|---|
move | Vector2 | 角色的移动输入向量,包含水平和垂直方向 |
look | Vector2 | 相机的视角输入向量,包含水平和垂直方向 |
jump | bool | 跳跃输入状态,true 表示按下跳跃键 |
sprint | bool | 冲刺输入状态,true 表示按下冲刺键 |
analogMovement | bool | 是否使用模拟输入进行移动 |
cursorLocked | bool | 是否锁定鼠标光标,默认为 true |
cursorInputForLook | bool | 是否使用鼠标光标输入来控制视角,默认为 true |
方法名 | 访问修饰符 | 返回类型 | 说明 |
---|---|---|---|
OnMove(InputValue value) | public | void | 处理移动输入事件,调用 MoveInput 方法 |
OnLook(InputValue value) | public | void | 处理视角输入事件,仅当 cursorInputForLook 为 true 时调用 LookInput 方法 |
OnJump(InputValue value) | public | void | 处理跳跃输入事件,调用 JumpInput 方法 |
OnSprint(InputValue value) | public | void | 处理冲刺输入事件,调用 SprintInput 方法 |
MoveInput(Vector2 newMoveDirection) | public | void | 设置 move 变量的值 |
LookInput(Vector2 newLookDirection) | public | void | 设置 look 变量的值 |
JumpInput(bool newJumpState) | public | void | 设置 jump 变量的值 |
SprintInput(bool newSprintState) | public | void | 设置 sprint 变量的值 |
OnApplicationFocus(bool hasFocus) | private | void | 当应用程序获得或失去焦点时调用,调用 SetCursorState 方法 |
SetCursorState(bool newState) | private | void | 设置鼠标光标的锁定状态 |
类图
三 .第三人称控制类
整体代码
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/* 注意:角色和胶囊体的动画通过控制器调用,并使用动画器空值检查
*/
namespace StarterAssets
{
[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM
[RequireComponent(typeof(PlayerInput))]
#endif
public class ThirdPersonController : MonoBehaviour
{
[Header("玩家")]
[Tooltip("角色的移动速度,单位:米/秒")]
public float MoveSpeed = 2.0f;
[Tooltip("角色的冲刺速度,单位:米/秒")]
public float SprintSpeed = 5.335f;
[Tooltip("角色转向移动方向的速度")]
[Range(0.0f, 0.3f)]
public float RotationSmoothTime = 0.12f;
[Tooltip("加速和减速的速率")]
public float SpeedChangeRate = 10.0f;
public AudioClip LandingAudioClip;
public AudioClip[] FootstepAudioClips;
[Range(0, 1)] public float FootstepAudioVolume = 0.5f;
[Space(10)]
[Tooltip("玩家能够跳跃的高度")]
public float JumpHeight = 1.2f;
[Tooltip("角色使用自定义的重力值,引擎默认值为 -9.81f")]
public float Gravity = -15.0f;
[Space(10)]
[Tooltip("再次跳跃所需的间隔时间,设置为 0f 可立即再次跳跃")]
public float JumpTimeout = 0.50f;
[Tooltip("进入下落状态前所需的时间,适用于下楼梯等情况")]
public float FallTimeout = 0.15f;
[Header("玩家是否着地")]
[Tooltip("角色是否着地,此判断并非基于 CharacterController 内置的着地检查")]
public bool Grounded = true;
[Tooltip("适用于不平整地面的偏移量")]
public float GroundedOffset = -0.14f;
[Tooltip("着地检查的半径,应与 CharacterController 的半径一致")]
public float GroundedRadius = 0.28f;
[Tooltip("角色判定为地面的图层")]
public LayerMask GroundLayers;
[Header("Cinemachine 相机")]
[Tooltip("Cinemachine 虚拟相机所跟随的目标对象")]
public GameObject CinemachineCameraTarget;
[Tooltip("相机向上移动的最大角度(单位:度)")]
public float TopClamp = 70.0f;
[Tooltip("相机向下移动的最大角度(单位:度)")]
public float BottomClamp = -30.0f;
[Tooltip("用于覆盖相机角度的额外度数,在锁定相机位置时可用于微调相机位置")]
public float CameraAngleOverride = 0.0f;
[Tooltip("是否锁定相机在所有轴上的位置")]
public bool LockCameraPosition = false;
// Cinemachine 相机相关
private float _cinemachineTargetYaw;
private float _cinemachineTargetPitch;
// 玩家相关
private float _speed;
private float _animationBlend;
private float _targetRotation = 0.0f;
private float _rotationVelocity;
private float _verticalVelocity;
private float _terminalVelocity = 53.0f;
// 超时计时器
private float _jumpTimeoutDelta;
private float _fallTimeoutDelta;
// 动画 ID
private int _animIDSpeed;
private int _animIDGrounded;
private int _animIDJump;
private int _animIDFreeFall;
private int _animIDMotionSpeed;
#if ENABLE_INPUT_SYSTEM
private PlayerInput _playerInput;
#endif
private Animator _animator;
private CharacterController _controller;
private StarterAssetsInputs _input;
private GameObject _mainCamera;
private const float _threshold = 0.01f;
private bool _hasAnimator;
// 判断当前输入设备是否为鼠标
private bool IsCurrentDeviceMouse
{
get
{
#if ENABLE_INPUT_SYSTEM
return _playerInput.currentControlScheme == "KeyboardMouse";
#else
return false;
#endif
}
}
private void Awake()
{
// 获取主相机的引用
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
}
}
private void Start()
{
// 初始化 Cinemachine 相机目标的偏航角
_cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;
// 尝试获取动画器组件
_hasAnimator = TryGetComponent(out _animator);
// 获取角色控制器组件
_controller = GetComponent<CharacterController>();
// 获取输入组件
_input = GetComponent<StarterAssetsInputs>();
#if ENABLE_INPUT_SYSTEM
// 获取玩家输入组件
_playerInput = GetComponent<PlayerInput>();
#else
Debug.LogError( "Starter Assets 包缺少依赖项,请使用 Tools/Starter Assets/Reinstall Dependencies 进行修复");
#endif
// 分配动画 ID
AssignAnimationIDs();
// 初始化跳跃和下落超时计时器
_jumpTimeoutDelta = JumpTimeout;
_fallTimeoutDelta = FallTimeout;
}
private void Update()
{
// 尝试获取动画器组件
_hasAnimator = TryGetComponent(out _animator);
// 处理跳跃和重力逻辑
JumpAndGravity();
// 检查角色是否着地
GroundedCheck();
// 处理角色移动逻辑
Move();
}
private void LateUpdate()
{
// 处理相机旋转逻辑
CameraRotation();
}
// 分配动画参数的哈希 ID
private void AssignAnimationIDs()
{
_animIDSpeed = Animator.StringToHash("Speed");
_animIDGrounded = Animator.StringToHash("Grounded");
_animIDJump = Animator.StringToHash("Jump");
_animIDFreeFall = Animator.StringToHash("FreeFall");
_animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
}
// 检查角色是否着地
private void GroundedCheck()
{
// 设置球体位置并添加偏移量
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
transform.position.z);
// 检测球体范围内是否与地面图层发生碰撞
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
QueryTriggerInteraction.Ignore);
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDGrounded, Grounded);
}
}
// 处理相机旋转逻辑
private void CameraRotation()
{
// 如果有鼠标或其他输入,并且相机位置未锁定
if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
{
// 根据当前输入设备确定时间乘数
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
// 更新相机的偏航角和俯仰角
_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
}
// 限制相机的旋转角度在 360 度范围内
_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);
// 设置 Cinemachine 相机目标的旋转角度
CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
_cinemachineTargetYaw, 0.0f);
}
// 处理角色移动逻辑
private void Move()
{
// 根据是否按下冲刺键,设置目标速度
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
// 简单的加速和减速逻辑,便于修改或扩展
// 注意:Vector2 的 == 运算符使用近似值,不会出现浮点误差,且比计算向量长度更高效
// 如果没有输入,将目标速度设为 0
if (_input.move == Vector2.zero) targetSpeed = 0.0f;
// 获取玩家当前的水平速度
float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;
float speedOffset = 0.1f;
// 根据是否为模拟输入,确定输入的幅度
float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;
// 加速或减速到目标速度
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
currentHorizontalSpeed > targetSpeed + speedOffset)
{
// 使用插值计算速度,使速度变化更自然
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
Time.deltaTime * SpeedChangeRate);
// 将速度值保留三位小数
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}
// 插值计算动画混合值
_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
if (_animationBlend < 0.01f) _animationBlend = 0f;
// 归一化输入方向
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
// 注意:Vector2 的 != 运算符使用近似值,不会出现浮点误差,且比计算向量长度更高效
// 如果有移动输入,并且角色正在移动,则旋转角色
if (_input.move != Vector2.zero)
{
// 计算目标旋转角度
_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
_mainCamera.transform.eulerAngles.y;
// 平滑旋转角色
float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
RotationSmoothTime);
// 旋转角色以面向输入方向(相对于相机位置)
transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
}
// 计算目标移动方向
Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
// 移动角色
_controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetFloat(_animIDSpeed, _animationBlend);
_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
}
}
// 处理跳跃和重力逻辑
private void JumpAndGravity()
{
if (Grounded)
{
// 重置下落超时计时器
_fallTimeoutDelta = FallTimeout;
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, false);
_animator.SetBool(_animIDFreeFall, false);
}
// 当角色着地时,避免垂直速度无限下降
if (_verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}
// 处理跳跃逻辑
if (_input.jump && _jumpTimeoutDelta <= 0.0f)
{
// 根据跳跃高度和重力计算所需的垂直速度
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, true);
}
}
// 处理跳跃超时逻辑
if (_jumpTimeoutDelta >= 0.0f)
{
_jumpTimeoutDelta -= Time.deltaTime;
}
}
else
{
// 重置跳跃超时计时器
_jumpTimeoutDelta = JumpTimeout;
// 处理下落超时逻辑
if (_fallTimeoutDelta >= 0.0f)
{
_fallTimeoutDelta -= Time.deltaTime;
}
else
{
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDFreeFall, true);
}
}
// 角色未着地时,禁止跳跃
_input.jump = false;
}
// 应用重力,当垂直速度未达到终端速度时,逐渐增加垂直速度
if (_verticalVelocity < _terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}
// 限制角度范围
private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
// 当对象在场景视图中被选中时,绘制调试辅助线
private void OnDrawGizmosSelected()
{
Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);
// 根据角色是否着地设置调试线颜色
if (Grounded) Gizmos.color = transparentGreen;
else Gizmos.color = transparentRed;
// 绘制着地检测球体的调试线
Gizmos.DrawSphere(
new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),
GroundedRadius);
}
// 脚步声事件处理
private void OnFootstep(AnimationEvent animationEvent)
{
if (animationEvent.animatorClipInfo.weight > 0.5f)
{
if (FootstepAudioClips.Length > 0)
{
// 随机选择一个脚步声音频剪辑
var index = Random.Range(0, FootstepAudioClips.Length);
// 在角色中心位置播放脚步声音频
AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);
}
}
}
// 着陆事件处理
private void OnLand(AnimationEvent animationEvent)
{
if (animationEvent.animatorClipInfo.weight > 0.5f)
{
// 在角色中心位置播放着陆音频
AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);
}
}
}
}
类图
分步解析
1.初始化
private void Awake()
{
// 获取主相机的引用
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
}
}
private void Start()
{
// 初始化 Cinemachine 相机目标的偏航角
_cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;
// 尝试获取动画器组件
_hasAnimator = TryGetComponent(out _animator);
// 获取角色控制器组件
_controller = GetComponent<CharacterController>();
// 获取输入组件
_input = GetComponent<StarterAssetsInputs>();
#if ENABLE_INPUT_SYSTEM
// 获取玩家输入组件
_playerInput = GetComponent<PlayerInput>();
#else
Debug.LogError( "Starter Assets 包缺少依赖项,请使用 Tools/Starter Assets/Reinstall Dependencies 进行修复");
#endif
// 分配动画 ID
AssignAnimationIDs();
// 初始化跳跃和下落超时计时器
_jumpTimeoutDelta = JumpTimeout;
_fallTimeoutDelta = FallTimeout;
}
private void AssignAnimationIDs()
{
_animIDSpeed = Animator.StringToHash("Speed");
_animIDGrounded = Animator.StringToHash("Grounded");
_animIDJump = Animator.StringToHash("Jump");
_animIDFreeFall = Animator.StringToHash("FreeFall");
_animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
}
Awake
方法在对象实例化时调用,用于获取主相机的引用。Start
方法在对象启用后调用,进行一系列的初始化操作:- 初始化 Cinemachine 相机的偏航角
- 获取所需的组件,如
Animator
、CharacterController
、StarterAssetsInputs
和PlayerInput
- 调用
AssignAnimationIDs
方法分配动画参数的哈希 ID - 初始化跳跃和下落超时计时器
2.交互处理
private StarterAssetsInputs _input;
// 在 Move 方法中使用输入
private void Move()
{
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
if (_input.move == Vector2.zero) targetSpeed = 0.0f;
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
// ...
}
// 在 JumpAndGravity 方法中使用输入
private void JumpAndGravity()
{
if (Grounded && _input.jump && _jumpTimeoutDelta <= 0.0f)
{
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, true);
}
}
// ...
}
// 在 CameraRotation 方法中使用输入
private void CameraRotation()
{
if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
{
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
}
// ...
}
_input
是StarterAssetsInputs
类的实例,用于获取玩家的移动、冲刺、跳跃和视角输入。- 在
Move
方法中,根据_input.sprint
判断是否冲刺,根据_input.move
确定移动方向和目标速度。 - 在
JumpAndGravity
方法中,根据_input.jump
判断是否触发跳跃。 - 在
CameraRotation
方法中,根据_input.look
控制相机的旋转
3.移动方法
private void Move()
{
// 根据是否按下冲刺键,设置目标速度
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
// 如果没有输入,将目标速度设为 0
if (_input.move == Vector2.zero) targetSpeed = 0.0f;
// 获取玩家当前的水平速度
float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;
float speedOffset = 0.1f;
// 根据是否为模拟输入,确定输入的幅度
float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;
// 加速或减速到目标速度
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
currentHorizontalSpeed > targetSpeed + speedOffset)
{
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
Time.deltaTime * SpeedChangeRate);
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}
// 插值计算动画混合值
_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
if (_animationBlend < 0.01f) _animationBlend = 0f;
// 归一化输入方向
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
// 如果有移动输入,并且角色正在移动,则旋转角色
if (_input.move != Vector2.zero)
{
_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
_mainCamera.transform.eulerAngles.y;
float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
RotationSmoothTime);
transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
}
// 计算目标移动方向
Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
// 移动角色
_controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetFloat(_animIDSpeed, _animationBlend);
_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
}
}
- 根据玩家的冲刺输入设置目标速度,如果没有移动输入则将目标速度设为 0
- 计算当前水平速度,并根据当前速度和目标速度的差异,使用
Mathf.Lerp
进行平滑加速或减速。 - 计算动画混合值,用于控制动画的过渡
- 根据玩家的移动输入计算目标旋转角度,并使用
Mathf.SmoothDampAngle
进行平滑旋转 - 计算目标移动方向,并使用
CharacterController.Move
方法移动角色 - 如果有动画器组件,更新动画参数
_animIDSpeed
和_animIDMotionSpeed
4.跳跃和重力处理
private void JumpAndGravity()
{
if (Grounded)
{
// 重置下落超时计时器
_fallTimeoutDelta = FallTimeout;
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, false);
_animator.SetBool(_animIDFreeFall, false);
}
// 当角色着地时,避免垂直速度无限下降
if (_verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}
// 处理跳跃逻辑
if (_input.jump && _jumpTimeoutDelta <= 0.0f)
{
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, true);
}
}
// 处理跳跃超时逻辑
if (_jumpTimeoutDelta >= 0.0f)
{
_jumpTimeoutDelta -= Time.deltaTime;
}
}
else
{
// 重置跳跃超时计时器
_jumpTimeoutDelta = JumpTimeout;
// 处理下落超时逻辑
if (_fallTimeoutDelta >= 0.0f)
{
_fallTimeoutDelta -= Time.deltaTime;
}
else
{
if (_hasAnimator)
{
_animator.SetBool(_animIDFreeFall, true);
}
}
// 角色未着地时,禁止跳跃
_input.jump = false;
}
// 应用重力,当垂直速度未达到终端速度时,逐渐增加垂直速度
if (_verticalVelocity < _terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}
- 如果角色着地:
- 重置下落超时计时器。
- 更新动画参数,将跳跃和自由落体状态设为
false
。 - 确保垂直速度不会无限下降。
- 如果玩家按下跳跃键且跳跃超时计时器已过,则根据跳跃高度和重力计算垂直速度,并更新动画参数。
- 递减跳跃超时计时器。
- 如果角色未着地:
- 重置跳跃超时计时器。
- 递减下落超时计时器,如果超时则更新动画参数为自由落体状态。
- 禁止跳跃输入。
- 应用重力,使垂直速度逐渐增加,直到达到终端速度
5.着地检测
private void GroundedCheck()
{
// 设置球体位置并添加偏移量
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
transform.position.z);
// 检测球体范围内是否与地面图层发生碰撞
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
QueryTriggerInteraction.Ignore);
// 如果有动画器组件,更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDGrounded, Grounded);
}
}
- 在角色位置下方设置一个球体,使用
Physics.CheckSphere
方法检测球体是否与指定的地面图层发生碰撞。 - 根据检测结果更新
Grounded
变量。 - 如果有动画器组件,更新动画参数
_animIDGrounded
。
6.相机旋转处理
private void CameraRotation()
{
// 如果有鼠标或其他输入,并且相机位置未锁定
if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
{
// 根据当前输入设备确定时间乘数
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
// 更新相机的偏航角和俯仰角
_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
}
// 限制相机的旋转角度在 360 度范围内
_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);
// 设置 Cinemachine 相机目标的旋转角度
CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
_cinemachineTargetYaw, 0.0f);
}
private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
- 如果有视角输入且相机位置未锁定,根据输入更新相机的偏航角和俯仰角,同时根据输入设备确定时间乘数。
- 使用
ClampAngle
方法限制相机的旋转角度在指定范围内。 - 设置 Cinemachine 相机目标的旋转角度。
7..动画处理
// 在 Move 方法中更新动画参数
if (_hasAnimator)
{
_animator.SetFloat(_animIDSpeed, _animationBlend);
_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
}
// 在 GroundedCheck 方法中更新动画参数
if (_hasAnimator)
{
_animator.SetBool(_animIDGrounded, Grounded);
}
// 在 JumpAndGravity 方法中更新动画参数
if (_hasAnimator)
{
_animator.Set
四.角色推动刚体类
这个类是一个单独挂载于player的类 已经详细标明了注释 还请自行查看
using UnityEngine;
// 该类用于实现角色推动刚体的功能
public class BasicRigidBodyPush : MonoBehaviour
{
// 可推动刚体所在的图层遮罩,只有这些图层的刚体才能被推动
public LayerMask pushLayers;
// 是否允许推动刚体的开关,若为 false 则不会触发推动逻辑
public bool canPush;
// 推动刚体的力量强度,取值范围在 0.5f 到 5f 之间,默认值为 1.1f
[Range(0.5f, 5f)] public float strength = 1.1f;
// 当角色控制器与其他碰撞体发生碰撞时调用此方法
private void OnControllerColliderHit(ControllerColliderHit hit)
{
// 只有当 canPush 为 true 时,才调用 PushRigidBodies 方法来处理推动逻辑
if (canPush) PushRigidBodies(hit);
}
// 处理推动刚体的具体逻辑
private void PushRigidBodies(ControllerColliderHit hit)
{
// 参考文档:https://docs.unity3d.com/ScriptReference/CharacterController.OnControllerColliderHit.html
// 获取碰撞体所附着的刚体组件
Rigidbody body = hit.collider.attachedRigidbody;
// 如果没有刚体或者刚体是运动学刚体(即不受物理模拟影响),则不进行推动操作,直接返回
if (body == null || body.isKinematic) return;
// 获取刚体所在游戏对象的图层对应的图层遮罩
var bodyLayerMask = 1 << body.gameObject.layer;
// 检查刚体所在的图层是否在可推动的图层范围内,如果不在则不进行推动操作,直接返回
if ((bodyLayerMask & pushLayers.value) == 0) return;
// 如果角色的移动方向主要是向下(y 轴分量小于 -0.3f),则不进行推动操作,直接返回
// 这是为了避免角色在向下移动时推动下方的物体
if (hit.moveDirection.y < -0.3f) return;
// 计算推动方向,只考虑水平方向的移动,忽略垂直方向
Vector3 pushDir = new Vector3(hit.moveDirection.x, 0.0f, hit.moveDirection.z);
// 对刚体施加力,力的大小为推动方向乘以推动强度,力的模式为冲量模式
// 冲量模式会瞬间改变刚体的动量
body.AddForce(pushDir * strength, ForceMode.Impulse);
}
}