[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;}}
}
变量/方法图解释
| 变量名 | 类型 | 说明 |
|---|---|---|
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);}
}
相关文章:
[Unity角色控制专题] (借助ai)详细解析官方第三人称控制器
首先模板链接在这里,你可以直接下载并导入unity即可查看官方为开发者写好一套控制器 本文的ai工具用到了豆包,其灵活程度很高,总结能力也强过我太多 因此大量使用,不喜勿喷 Starter Assets - ThirdPerson | Updates in new Charac…...
安装 Docker Desktop 修改默认安装目录到指定目录
Docker Desktop安装目录设置 Docker Desktop 默认安装位置 (C:\Program Files\Docker\Docker) 是这个 ,导致系统盘占用过大,大概2G ; 那么如何安装到其他磁盘呢? 根据docker desktop 官网 Docker Desktop install 我们可以看到&a…...
渗透测试--文件包含漏洞
文件包含漏洞 前言 《Web安全实战》系列集合了WEB类常见的各种漏洞,笔者根据自己在Web安全领域中学习和工作的经验,对漏洞原理和漏洞利用面进行了总结分析,致力于漏洞准确性、丰富性,希望对WEB安全工作者、WEB安全学习者能有所帮助…...
【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第十一节】
ISO 14229-1:2023 UDS诊断服务测试用例全解析(RequestTransferExit0x37服务) 作者:车端域控测试工程师 更新日期:2025年02月13日 关键词:UDS协议、0x37服务、传输终止、NRC验证、ISO 14229-1:2023 一、服务功能概述 0…...
虚拟环境测试部署应用
一、作用 虚拟环境(env)在计算机领域,特别是在软件开发和测试中扮演着重要角色。它主要用于创建一个隔离的环境,使得开发者可以在不影响系统其余部分的情况下安装、配置和运行软件项目。以下是虚拟环境的一些主要作用: 1、依赖管理 不同的项目可能需要不同版本的库或框…...
【线性代数】2矩阵
1.矩阵的运算 1.1.定义 矩阵行列式数表数行数和列数可以不相等行数和列数必须相等1.2.加法与数乘 矩阵的数乘:所有元素都乘这个数 矩阵的加法:对应位置处元素相加 🦊已知,求 1.3.乘法 矩阵乘法三步法 ①能不能乘:内定乘 ②乘完是何类型:外定型 ③中的元素是什么:左…...
前端为什么要使用new Promise包裹一个函数
在前端开发中,使用 new Promise 包裹一个函数主要是为了将原本不支持 Promise 规范的操作转化为支持 Promise 规范的操作,从而可以更好地处理异步操作,提升代码的可读性和可维护性。下面详细介绍这么做的常见原因和应用场景: 1. …...
深度学习在天文观测中的应用:解锁宇宙的奥秘
深度学习在天文观测中的应用:解锁宇宙的奥秘 引言 宇宙是无尽的,天文学家通过观测天体来揭示宇宙的奥秘。随着现代天文设备技术的进步,我们现在可以通过 射电望远镜、空间望远镜 和 地面望远镜 获取大量的天文数据。然而,这些数据的规模和复杂性让传统的手工分析方法变得…...
SaaS 平台开发要点
如何在 SaaS 平台的前端开发中,编写高性能、高质量且高度通用化的 Vue 组件 一、组件设计原则 单一职责原则:每个组件只负责一个核心功能受控/非受控模式:同时支持 v-model 和自主状态管理组合式 API:使用 Composition API 提升逻辑复用性可访问性:遵循 WAI-ARIA 规范Typ…...
python从入门到进去
python从入门到进去 第一章、软件和工具的安装一、安装 python 解释器二、安装 pycharm 第二章、初识 python一、注释可分三种二、打印输入语句三、变量1、基本数据类型1.1、整数数据类型 int1.2、浮点数数据类型 float1.3、布尔数据类型 boolean1.4、字符串数据类型 string 2、…...
DeepSeek与医院电子病历的深度融合路径:本地化和上云差异化分析
一、引言 1.1 研究背景与意义 在医疗信息化快速发展的当下,电子病历系统已成为医院信息管理的核心构成。电子病历(EMR)系统,是指医务人员在医疗活动过程中,使用医疗机构信息系统生成的文字、符号、图标、图形、数据、影像等数字化信息,并能实现存储、管理、传输和重现的…...
苍穹外卖day4 redis相关简单知识 店铺营业状态设置
内存存储 键值对 key-value 一般用于处理突发性大量请求数据操作(暂时浅显理解) 读写速度极快,常用于缓存数据,减少对数据库的访问压力,提高系统性能。例如,可以缓存用户会话、商品信息、页面数据 设置默…...
pycharm社区版有个window和arm64版本,到底下载哪一个?还有pycharm官网
首先pycharm官网是这一个。我是在2025年2月16日9:57进入的网站。如果网站还没有更新的话,那么就往下滑一下找到 community Edition,这个就是社区版了免费的。PyCharm:适用于数据科学和 Web 开发的 Python IDE 适用于数据科学和 Web 开发的 Python IDE&am…...
使用新版本golang项目中goyacc依赖问题的处理
背景 最近项目使用中有用到go mod 和 goyacc工具。goyacc涉及到编译原理的词法分析,文法分析等功能,可以用来生成基于golang的语法分析文件。本期是记录一个使用中遇到的依赖相关的问题。因为用到goyacc,需要生成goyacc的可执行文件。 而项目…...
Moya 网络框架
Moya 网络框架 通过 Moya 进行网络请求的一般步骤如下: 1. 定义 TargetType:为每个 API 请求创建一个枚举,遵循 TargetType 协议,指定基础 URL、请求路径、方法、参数等。 2. 创建 MoyaProvider:实例化 MoyaProvider&…...
FreeRTOS第3篇:链表的“精密齿轮”——列表与列表项
文章目录 1 列表与列表项:FreeRTOS的“排队系统”2 列表操作:FreeRTOS的“排队算法”3 列表的应用场景:FreeRTOS的“任务调度枢纽”4 源码级洞察:列表的“灵魂代码”5 实战:列表操作实验6 总结与思考引言:嵌入式系统的“任务候车厅” 想象你正在管理一座繁忙的火车站:乘…...
React.memo 使用详解与最佳实践
React.memo 使用详解与最佳实践 引言React.memo 是什么?使用场景实战示例示例解析自定义比较函数使用注意事项总结 引言 在 React 应用程序中,性能优化是一个永恒的话题。当父组件状态发生变化时,即使子组件的 props 没有改变,子…...
SpringBoot中集成SaToken
SpringBoot中集成SaToken 1. 写一个拦截器2. 对拦截器的说明&解释2. 拦截器 1. 写一个拦截器 import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.stp.StpUtil; import org.springframework.beans.factory.annotation.Value; import org.spri…...
网络安全-攻击流程-应用层
应用层攻击针对OSI模型的第七层(应用层),主要利用协议漏洞、业务逻辑缺陷或用户交互弱点,直接威胁Web应用、API、数据库等服务。以下是常见应用层攻击类型及其流程,以及防御措施: 1. SQL注入(SQ…...
Ubuntu 24.04.1 LTS 本地部署 DeepSeek 私有化知识库
文章目录 前言工具介绍与作用工具的关联与协同工作必要性分析 1、DeepSeek 简介1.1、DeepSeek-R1 硬件要求 2、Linux 环境说明2.1、最小部署(Ollama DeepSeek)2.1.1、扩展(非必须) - Ollama 后台运行、开机自启: 2.2、…...
微信小程序中缓存数据全方位解惑
微信小程序中缓存数据全方位解惑 微信小程序中的数据缓存是提升用户体验和优化性能的重要手段,跟电脑浏览器中的Local Storage的性质一样。以下是关于微信小程序数据缓存的相关知识点和示例的详细介绍: 1. 数据缓存的类型 微信小程序提供了两种数据缓…...
python语言进阶之函数
目录 前言 函数的创建和调用 函数创建 调用函数 参数传递 形式参数和实际参数 位置参数 数量必须与定义时一致 位置必须与定义时一致 关键字参数 为参数设置默认值 可变参数 **parameter 返回值 变量的作用域 局部变量 全局变量 匿名函数 前言 提到函数&…...
Mybatis-扩展功能
逻辑删除乐观锁 MyBatisPlus从入门到精通-3(含mp代码生成器) Db静态工具类 Spring依赖循环问题 代码生成器 MybatisPlus代码生成器 枚举处理器 我们这里用int来存储状态 需要注解,很不灵活 希望用枚举类来代替这个Integer 这样的话我…...
青少年编程与数学 02-009 Django 5 Web 编程 16课题、权限管理
青少年编程与数学 02-009 Django 5 Web 编程 16课题、权限管理 一、授权授权的主要特点和作用授权的类型应用场景 二、权限系统使用Django内置的权限系统使用组管理权限使用第三方库在视图中应用权限 三、权限管理示例步骤 1: 创建Django项目和应用步骤 2: 定义模型和权限步骤 …...
Baklib知识中台构建企业智能运营核心架构
内容概要 在数字化转型的浪潮中,企业对于知识的系统化管理需求日益迫切。Baklib作为新一代的知识中台,通过构建智能运营核心架构,为企业提供了一套从知识汇聚到场景化落地的完整解决方案。其核心价值在于将分散的知识资源整合为统一的资产池…...
Java爬虫获取1688商品搜索API接口的实现指南
在电商数据分析、市场调研以及商品选品等领域,按关键字搜索1688商品并获取相关数据是一项重要的任务。本文将详细介绍如何使用Java爬虫技术,通过1688的API接口按关键字搜索商品,并解析返回的数据。以下是实现的完整步骤和代码示例。 一、前期…...
Ubuntu启动geteck/jetlinks实战:Docker启动
参考: JetLinks 物联网基础平台 安装Docker Ubuntu下载安装Docker-Desktop-CSDN博客 sudo apt install -y docker-compose 下载源码 # github亦可 git clone https://gitee.com/jetlinks/jetlinks-community.git cd jetlinks-community 启动 cd docker/run-a…...
保姆级GitHub大文件(100mb-2gb)上传教程
GLF(Git Large File Storage)安装使用 使用GitHub desktop上传大于100mb的文件时报错 The following files are over 100MB. lf you commit these files, you will no longer beable to push this repository to GitHub.com.term.rarWe recommend you a…...
【16届蓝桥杯寒假刷题营】第2期DAY1I
4.有向无环的路径数 - 蓝桥云课 问题描述 给定 N 个节点 M 条边的有向无环图,请你求解有多少条 1 到 N 的路径。 由于答案可能很大,你只需要输出答案对 998244353 取模后的结果。 输入格式 第一行包含 2 个正整数 N,M,表示有向无环图的节…...
WEB安全--SQL注入--PDO与绕过
一、PDO介绍: 1.1、原理: PDO支持使用预处理语句(Prepared Statements),这可以有效防止SQL注入攻击。预处理语句将SQL语句与数据分开处理,使得用户输入的数据始终作为参数传递给数据库,而不会直…...
