首先模板链接在这里,你可以直接下载并导入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;
#endifnamespace 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_SYSTEMpublic 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);}
#endifpublic 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))]
#endifpublic 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;// 动画 IDprivate int _animIDSpeed;private int _animIDGrounded;private int _animIDJump;private int _animIDFreeFall;private int _animIDMotionSpeed;#if ENABLE_INPUT_SYSTEM private PlayerInput _playerInput;
#endifprivate 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_SYSTEMreturn _playerInput.currentControlScheme == "KeyboardMouse";
#elsereturn 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>();
#elseDebug.LogError( "Starter Assets 包缺少依赖项,请使用 Tools/Starter Assets/Reinstall Dependencies 进行修复");
#endif// 分配动画 IDAssignAnimationIDs();// 初始化跳跃和下落超时计时器_jumpTimeoutDelta = JumpTimeout;_fallTimeoutDelta = FallTimeout;}private void Update(){// 尝试获取动画器组件_hasAnimator = TryGetComponent(out _animator);// 处理跳跃和重力逻辑JumpAndGravity();// 检查角色是否着地GroundedCheck();// 处理角色移动逻辑Move();}private void LateUpdate(){// 处理相机旋转逻辑CameraRotation();}// 分配动画参数的哈希 IDprivate 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 的 == 运算符使用近似值,不会出现浮点误差,且比计算向量长度更高效// 如果没有输入,将目标速度设为 0if (_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>();
#elseDebug.LogError( "Starter Assets 包缺少依赖项,请使用 Tools/Starter Assets/Reinstall Dependencies 进行修复");
#endif// 分配动画 IDAssignAnimationIDs();// 初始化跳跃和下落超时计时器_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;// 如果没有输入,将目标速度设为 0if (_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);}
}