当前位置: 首页 > news >正文

[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;
#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;}}
}

        变量/方法图解释

变量名类型说明
moveVector2角色的移动输入向量,包含水平和垂直方向
lookVector2相机的视角输入向量,包含水平和垂直方向
jumpbool跳跃输入状态,true 表示按下跳跃键
sprintbool冲刺输入状态,true 表示按下冲刺键
analogMovementbool是否使用模拟输入进行移动
cursorLockedbool是否锁定鼠标光标,默认为 true
cursorInputForLookbool是否使用鼠标光标输入来控制视角,默认为 true
方法名访问修饰符返回类型说明
OnMove(InputValue value)publicvoid处理移动输入事件,调用 MoveInput 方法
OnLook(InputValue value)publicvoid处理视角输入事件,仅当 cursorInputForLook 为 true 时调用 LookInput 方法
OnJump(InputValue value)publicvoid处理跳跃输入事件,调用 JumpInput 方法
OnSprint(InputValue value)publicvoid处理冲刺输入事件,调用 SprintInput 方法
MoveInput(Vector2 newMoveDirection)publicvoid设置 move 变量的值
LookInput(Vector2 newLookDirection)publicvoid设置 look 变量的值
JumpInput(bool newJumpState)publicvoid设置 jump 变量的值
SprintInput(bool newSprintState)publicvoid设置 sprint 变量的值
OnApplicationFocus(bool hasFocus)privatevoid当应用程序获得或失去焦点时调用,调用 SetCursorState 方法
SetCursorState(bool newState)privatevoid设置鼠标光标的锁定状态

        类图

三 .第三人称控制类

        整体代码

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 相机的偏航角
    • 获取所需的组件,如 AnimatorCharacterControllerStarterAssetsInputs 和 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);}
}

相关文章:

[Unity角色控制专题] (借助ai)详细解析官方第三人称控制器

首先模板链接在这里&#xff0c;你可以直接下载并导入unity即可查看官方为开发者写好一套控制器 本文的ai工具用到了豆包&#xff0c;其灵活程度很高&#xff0c;总结能力也强过我太多 因此大量使用&#xff0c;不喜勿喷 Starter Assets - ThirdPerson | Updates in new Charac…...

【数据结构基础_链表】

1、链表的定义 链表与数组的区分&#xff1a; 数组是一块连续的内存空间&#xff0c;有了这块内存空间的首地址&#xff0c;就能直接通过索引计算出任意位置的元素地址。 数组最大的优势是支持通过索引快速访问元素&#xff0c;而链表就不支持。链表不一样&#xff0c;一条链…...

Java 实现 Redis中的GEO数据结构

Java 实现 Redis中的GEO数据结构 LBS &#xff08;基于位置信息服务&#xff08;Location-Based Service&#xff0c;LBS&#xff09;&#xff09;应用访问的数据是和人 或物关联的一组经纬度信息&#xff0c;而且要能查询相邻的经纬度范围&#xff0c;GEO 就非常适合应用在 …...

PostgreSQL如何关闭自动commit

PostgreSQL如何关闭自动commit 在 PostgreSQL 中&#xff0c;默认情况下&#xff0c;每个 SQL 语句都会自动提交&#xff08;即 AUTOCOMMIT 是开启的&#xff09;。如果希望关闭自动提交&#xff0c;以便手动控制事务的提交和回滚&#xff0c;可以通过以下方法实现。 1 使用 …...

1、云原生写在前面

云原生技术是什么&#xff08;包含哪些组件&#xff09;&#xff1f;每个组件是负责什么&#xff1f;学习这些组件技术能解决什问题&#xff1f;哪些类企业需要用到&#xff1f; 这是标准系列的问题&#xff0c;通过 deepseek 的深度思考就能得到我们想要的易于理解的人话式的…...

Redis离线安装

Linux系统Centos安装部署Redis缓存插件 参考&#xff1a;Redis中文网&#xff1a; https://www.redis.net.cn/ 参考&#xff1a;RPM软件包下载地址&#xff1a; https://rpmfind.net/linux/RPM/index.html http://rpm.pbone.net/ https://mirrors.aliyun.com/centos/7/os…...

网络安全-攻击流程-应用层

应用层攻击针对OSI模型的第七层&#xff08;应用层&#xff09;&#xff0c;主要利用协议漏洞、业务逻辑缺陷或用户交互弱点&#xff0c;直接威胁Web应用、API、数据库等服务。以下是常见应用层攻击类型及其流程&#xff0c;以及防御措施&#xff1a; 1. SQL注入&#xff08;SQ…...

java八股文-spring

目录 1. spring基础 1.1 什么是Spring&#xff1f; 1.2 Spring有哪些优点&#xff1f; 1.3 Spring主要模块 1.4 Spring常用注解 1.5 Spring中Bean的作用域 1.6 Spring自动装配的方式 1.7 SpringBean的生命周期 1.8 多级缓存 1.9 循环依赖&#xff1f; 1 .8.1 原因 1.8…...

Jvascript网页设计案例:通过js实现一款密码强度检测,适用于等保测评整改

本文目录 前言功能预览样式特点总结&#xff1a;1. 整体视觉风格2. 密码输入框设计3. 强度指示条4. 结果文本与原因说明 功能特点总结&#xff1a;1. 密码强度检测2. 实时反馈机制3. 详细原因说明4. 视觉提示5. 交互体验优化 密码强度检测逻辑Html代码Javascript代码 前言 能满…...

【Scrapy】Scrapy教程2——工作原理

文章目录 数据流组件引擎Engine调度器Scheduler下载器Downloader爬虫Spiders项目管道Item Pipeline下载器中间件Downloader Middlewares爬虫中间件Spider Middlewares 在学习Scrapy前&#xff0c;我们需要先了解其架构和工作原理&#xff0c;这样才能很好的去使用Scrapy。 Scra…...

探索 DeepSeek:AI 领域的璀璨新星

在人工智能飞速发展的当下&#xff0c;DeepSeek 作为行业内的重要参与者&#xff0c;正以独特的技术和广泛的应用备受瞩目。 DeepSeek 是一家专注于实现 AGI&#xff08;通用人工智能&#xff09;的中国人工智能公司。它拥有自主研发的深度学习框架&#xff0c;能高效处理海量…...

宏基传奇swift edge偶尔开机BIOS重置

电脑是acer swift edge&#xff0c; SFA16-41&#xff0c;出厂是Win11系统&#xff0c; BIOS版本出厂1.04&#xff0c;更新到了目前最新1.10。 问题是 会偶尔开机ACER图标变小跑到屏幕左上方&#xff0c;下次开机BIOS就会被重置&#xff0c;开机等待很长时间。 因为是偶尔现象的…...

自动驾驶---如何打造一款属于自己的自动驾驶系统

在笔者的专栏《自动驾驶Planning决策规划》中&#xff0c;主要讲解了行车的相关知识&#xff0c;从Routing&#xff0c;到Behavior Planning&#xff0c;再到Motion Planning&#xff0c;以及最后的Control&#xff0c;笔者都做了相关介绍&#xff0c;其中主要包括算法在量产上…...

【C语言】第一期——数据类型变量常量

目录 1 字面量 2 整数类型 2.1 整数类型的取值范围 2.1.1 sizeof 运算符 2.2 GB、MB、KB、B之间的关系 2.3 定义整数类型的变量并打印 2.4 整数类型代码演示 3 浮点类型 3.1 浮点类型的取值范围 3.2 定义浮点类型变量并打印 3.3 保留2位小数点 4 char字符型 4.1…...

04运维实用篇(D4_日志)

目录 一、简介 二、代码中使用日志工具记录日志 1. 操作步骤 步骤1&#xff1a;添加日志记录操作 步骤2&#xff1a;设置日志输出级别 步骤3&#xff1a;设置日志组 2. 知识小结 三、优化日志对象创建代码 1. 实例 2. 总结 四、日志输出格式控制 1. 实例 2. 总结 …...

centos部署open-webui

提示&#xff1a;本文将简要介绍一下在linux下open-webui的安装过程,安装中未使用虚拟环境。 文章目录 一、open-webui是什么&#xff1f;二、安装流程1.openssl升级2.Python3.11安装3.sqlite安装升级4.pip 下载安装open-webui 总结 一、open-webui是什么&#xff1f; Open W…...

UE求职Demo开发日志#32 优化#1 交互逻辑实现接口、提取Bag和Warehouse的父类

1 定义并实现交互接口 接口定义&#xff1a; // Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "UObject/Interface.h" #include "MyInterActInterface.generated.h…...

Visonpro 检测是否有缺齿

一、效果展示 二、上面是原展开工具CogPolarUnwrapTool&#xff1b; 第二种方法&#xff1a; 用Blob 和 CogCopyRegionTool 三、 用预处理工具 加减常数&#xff0c;让图片变得更亮点 四、圆展开工具 五、模板匹配 六、代码分解 1.创建集合和文子显示工具 CogGraphicCollec…...

第1章大型互联网公司的基础架构——1.6 RPC服务

你可能在1.1节的引言中注意到业务服务层包括HTTP服务和RPC服务&#xff0c;两者的定位不一样。一般来说&#xff0c;一个业务场景的核心逻辑都是在RPC服务中实现的&#xff0c;强调的是服务于后台系统内部&#xff0c;所谓的“微服务”主要指的就是RPC服务&#xff1b;而HTTP服…...

今日AI和商界事件(2025-02-15)

根据2025年2月15日的科技动态&#xff0c;以下是今日AI领域的重要事件及相关进展总结&#xff1a; 1. DeepSeek日活突破3000万&#xff0c;开源生态加速AI普惠 里程碑意义&#xff1a;开源大模型DeepSeek宣布日活跃用户数突破3000万&#xff0c;其R1模型凭借开源策略和低成本优…...

地震勘探——干扰波识别、井中地震时距曲线特点

目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波&#xff1a;可以用来解决所提出的地质任务的波&#xff1b;干扰波&#xff1a;所有妨碍辨认、追踪有效波的其他波。 地震勘探中&#xff0c;有效波和干扰波是相对的。例如&#xff0c;在反射波…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module

1、为什么要修改 CONNECT 报文&#xff1f; 多租户隔离&#xff1a;自动为接入设备追加租户前缀&#xff0c;后端按 ClientID 拆分队列。零代码鉴权&#xff1a;将入站用户名替换为 OAuth Access-Token&#xff0c;后端 Broker 统一校验。灰度发布&#xff1a;根据 IP/地理位写…...

OkHttp 中实现断点续传 demo

在 OkHttp 中实现断点续传主要通过以下步骤完成&#xff0c;核心是利用 HTTP 协议的 Range 请求头指定下载范围&#xff1a; 实现原理 Range 请求头&#xff1a;向服务器请求文件的特定字节范围&#xff08;如 Range: bytes1024-&#xff09; 本地文件记录&#xff1a;保存已…...

SpringCloudGateway 自定义局部过滤器

场景&#xff1a; 将所有请求转化为同一路径请求&#xff08;方便穿网配置&#xff09;在请求头内标识原来路径&#xff0c;然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

基于matlab策略迭代和值迭代法的动态规划

经典的基于策略迭代和值迭代法的动态规划matlab代码&#xff0c;实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

Web 架构之 CDN 加速原理与落地实践

文章目录 一、思维导图二、正文内容&#xff08;一&#xff09;CDN 基础概念1. 定义2. 组成部分 &#xff08;二&#xff09;CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 &#xff08;三&#xff09;CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 &#xf…...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

【生成模型】视频生成论文调研

工作清单 上游应用方向&#xff1a;控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...