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

news2025/5/20 2:59:26

        首先模板链接在这里,你可以直接下载并导入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;
        }
    }
}

        变量/方法图解释

变量名类型说明
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))]
#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 相机的偏航角
    • 获取所需的组件,如 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;

    // 如果没有输入,将目标速度设为 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);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2299942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安装 Docker Desktop 修改默认安装目录到指定目录

Docker Desktop安装目录设置 Docker Desktop 默认安装位置 &#xff08;C:\Program Files\Docker\Docker) 是这个 &#xff0c;导致系统盘占用过大&#xff0c;大概2G ; 那么如何安装到其他磁盘呢&#xff1f; 根据docker desktop 官网 Docker Desktop install 我们可以看到&a…

渗透测试--文件包含漏洞

文件包含漏洞 前言 《Web安全实战》系列集合了WEB类常见的各种漏洞&#xff0c;笔者根据自己在Web安全领域中学习和工作的经验&#xff0c;对漏洞原理和漏洞利用面进行了总结分析&#xff0c;致力于漏洞准确性、丰富性&#xff0c;希望对WEB安全工作者、WEB安全学习者能有所帮助…

【线性代数】2矩阵

1.矩阵的运算 1.1.定义 矩阵行列式数表数行数和列数可以不相等行数和列数必须相等1.2.加法与数乘 矩阵的数乘:所有元素都乘这个数 矩阵的加法:对应位置处元素相加 🦊已知,求 1.3.乘法 矩阵乘法三步法 ①能不能乘:内定乘 ②乘完是何类型:外定型 ③中的元素是什么:左…

python从入门到进去

python从入门到进去 第一章、软件和工具的安装一、安装 python 解释器二、安装 pycharm 第二章、初识 python一、注释可分三种二、打印输入语句三、变量1、基本数据类型1.1、整数数据类型 int1.2、浮点数数据类型 float1.3、布尔数据类型 boolean1.4、字符串数据类型 string 2、…

DeepSeek与医院电子病历的深度融合路径:本地化和上云差异化分析

一、引言 1.1 研究背景与意义 在医疗信息化快速发展的当下,电子病历系统已成为医院信息管理的核心构成。电子病历(EMR)系统,是指医务人员在医疗活动过程中,使用医疗机构信息系统生成的文字、符号、图标、图形、数据、影像等数字化信息,并能实现存储、管理、传输和重现的…

苍穹外卖day4 redis相关简单知识 店铺营业状态设置

内存存储 键值对 key-value 一般用于处理突发性大量请求数据操作&#xff08;暂时浅显理解&#xff09; 读写速度极快&#xff0c;常用于缓存数据&#xff0c;减少对数据库的访问压力&#xff0c;提高系统性能。例如&#xff0c;可以缓存用户会话、商品信息、页面数据 设置默…

pycharm社区版有个window和arm64版本,到底下载哪一个?还有pycharm官网

首先pycharm官网是这一个。我是在2025年2月16日9:57进入的网站。如果网站还没有更新的话&#xff0c;那么就往下滑一下找到 community Edition,这个就是社区版了免费的。PyCharm&#xff1a;适用于数据科学和 Web 开发的 Python IDE 适用于数据科学和 Web 开发的 Python IDE&am…

使用新版本golang项目中goyacc依赖问题的处理

背景 最近项目使用中有用到go mod 和 goyacc工具。goyacc涉及到编译原理的词法分析&#xff0c;文法分析等功能&#xff0c;可以用来生成基于golang的语法分析文件。本期是记录一个使用中遇到的依赖相关的问题。因为用到goyacc&#xff0c;需要生成goyacc的可执行文件。 而项目…

Ubuntu 24.04.1 LTS 本地部署 DeepSeek 私有化知识库

文章目录 前言工具介绍与作用工具的关联与协同工作必要性分析 1、DeepSeek 简介1.1、DeepSeek-R1 硬件要求 2、Linux 环境说明2.1、最小部署&#xff08;Ollama DeepSeek&#xff09;2.1.1、扩展&#xff08;非必须&#xff09; - Ollama 后台运行、开机自启&#xff1a; 2.2、…

python语言进阶之函数

目录 前言 函数的创建和调用 函数创建 调用函数 参数传递 形式参数和实际参数 位置参数 数量必须与定义时一致 位置必须与定义时一致 关键字参数 为参数设置默认值 可变参数 **parameter 返回值 变量的作用域 局部变量 全局变量 匿名函数 前言 提到函数&…

Mybatis-扩展功能

逻辑删除乐观锁 MyBatisPlus从入门到精通-3&#xff08;含mp代码生成器&#xff09; Db静态工具类 Spring依赖循环问题 代码生成器 MybatisPlus代码生成器 枚举处理器 我们这里用int来存储状态 需要注解&#xff0c;很不灵活 希望用枚举类来代替这个Integer 这样的话我…

Baklib知识中台构建企业智能运营核心架构

内容概要 在数字化转型的浪潮中&#xff0c;企业对于知识的系统化管理需求日益迫切。Baklib作为新一代的知识中台&#xff0c;通过构建智能运营核心架构&#xff0c;为企业提供了一套从知识汇聚到场景化落地的完整解决方案。其核心价值在于将分散的知识资源整合为统一的资产池…

保姆级GitHub大文件(100mb-2gb)上传教程

GLF&#xff08;Git Large File Storage&#xff09;安装使用 使用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 条边的有向无环图&#xff0c;请你求解有多少条 1 到 N 的路径。 由于答案可能很大&#xff0c;你只需要输出答案对 998244353 取模后的结果。 输入格式 第一行包含 2 个正整数 N,M&#xff0c;表示有向无环图的节…

DeepSeek 助力 Vue 开发:打造丝滑的面包屑导航(Breadcrumbs)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

Ubuntu 系统 LVM 逻辑卷扩容教程

Ubuntu 系统 LVM 逻辑卷扩容教程 前言 在 Linux 系统中&#xff0c;LVM&#xff08;Logical Volume Manager&#xff09;是一种逻辑卷管理工具&#xff0c;允许管理员动态调整磁盘空间&#xff0c;而无需重启系统。 本文将详细介绍如何使用 LVM 扩容逻辑卷&#xff0c;以实现…

7-Zip Final绿色版:高效压缩解压缩工具

在工作与学习旅程中&#xff0c;我们时常需要与各式各样的文件和文件夹打交道。为了更有效地利用存储空间或促进文件的便捷传输&#xff0c;压缩与解压工具自然而然地成为了我们不可或缺的助手。在众多同类工具中&#xff0c;7-Zip凭借其高效能、免费及开源的特性&#xff0c;深…

QML使用ChartView绘制饼状图

一、工程配置 首先修改CMakeLists.txt&#xff0c;按下图修改&#xff1a; find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets) PRIVATEtarget_link_libraries(appuntitledPRIVATE Qt6::QuickPRIVATE Qt6::Widgets )其次修改main.cpp&#xff0c;按下图修改&#xff…

【Android开发】华为手机安装包安装失败“应用是非正式版发布版本,当前设备不支持安装”问题解决

问题描述 我们将Debug版本的安装包发送到手机上安装&#xff0c;会发现华为手机有如下情况 解决办法 在文件gradle.properties中粘贴代码&#xff1a; android.injected.testOnlyfalse 最后点击“Sync now”&#xff0c;等待重新加载gradle资源即可 后面我们重新编译Debug安装…

Ubuntu添加桌面快捷方式

以idea为例 一. 背景 在ubuntu中&#xff0c;很多时候是自己解压的文件并没有桌面快捷方式&#xff0c;需要自己找到对应的目录的执行文件手动打开&#xff0c;很麻烦 而只需要在 /usr/share/applications 中创建自定义的desktop文件就能自动复制到桌面 二. 添加方法 创建desk…