【Unity实战笔记】第二一 · 基于状态模式的角色控制——以UnityChan为例

在这里插入图片描述

目录

  • 一 内容摘要
  • 二 前言
  • 三 状态模式的必要性
    • 3.1 非状态模式的角色控制
    • 3.2 简易状态模式的角色控制
    • 3.3 状态模式
      • 3.3.1 IState
      • 3.3.2 IdleState
      • 3.3.3 RunState
      • 3.3.4 JumpState
      • 3.3.5 PlayerController_ComplexStateMode
      • 3.3.6 注意事项
    • 3.4 SMB
  • 四 基于SMB的角色控制
    • 4.1 项目实战案例
      • 4.1.1资源准备
      • 4.1.2 目录结构:
      • 4.1.3 状态机
      • 4.1.4 cinemachine参数
      • 4.1.5 UnityChan_Idle_SMB
      • 4.1.6 UnityChan_Run_SMB
      • 4.1.7 UnityChan_Jump_SMB
      • 4.1.8 效果
    • 4.2 案例优化
      • 4.2.1 过渡不丝滑
      • 4.2.2 优化SMB,添加统一父类
      • 4.2.3 优化跳跃流程
        • 4.2.3.1 分割跳跃动画
        • 4.2.3.2 添加落地检测
        • 4.2.3.3 新建三个跳跃相关的SMB
      • 4.2.4 角色抖动
      • 4.2.5 New Input System另种用法
  • 五 后记

参考链接

  • Game Programming Patterns - State
  • 平台游戏控制器 教程 B站阿严Dev
  • 与Unity动画状态绑定的脚本:State Machine Behaviour B站IGBeginner0116
  • Unity手册 状态机行为
  • StateMachineBehaviour API
  • 源代码资源

转载请注明出处:🔗https://blog.csdn.net/weixin_44013533/article/details/143217580
作者:CSDN@|Ringleader|

一 内容摘要

本文用UnityChan角色,以实际案例展示角色控制的不同构架,包含非状态模式、简易状态模式、普通状态模式、基于SMB的状态模式。涉及的技术有 cinemachine、new InputSystem、SMB、Animator State Machine。

文章包含大量动图、代码和bug排查和优化思路,如果本文对你有帮助,千万不要吝惜点赞收藏关注(*^_^*)~

二 前言

本文偏Unity中级,基础知识可参考作者系列博客 |Ringleader|的博客——unity

强烈建议观看上面两个up的视频!

本文使用cinemachine、newInputSystem插件,导入项目报错先检查是否导入这两个插件。

本文涉及的源代码下载链接:

在这里插入图片描述
导入方式:新建工程,然后工具栏 Assets>Improt Pacage>Custom Package 导入下载的资源:源代码资源

若报如下错误,退出安全模式删除Assets/Settings 下的UnityChanInputAction文件(不知道为什么导出时会多了个重复文件)

在这里插入图片描述

三 状态模式的必要性

3.1 非状态模式的角色控制

在这里插入图片描述

以一个简单的 “ 待机-移动-跳跃 ” 控制为例子,实现上述状态切换,首先想到的方式代码实现如下(输入相关见后文状态模式):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player2Controller : MonoBehaviour
{
    private Animator _animator;
    private PlayerInput _playerInput;
    private Rigidbody _playerRig;
    private Transform _camTransform;
    public float jumpForce = 200f;
    public float runSpeed = 3f;

    void Start()
    {
        _animator = GetComponentInChildren<Animator>();
        _playerInput = GetComponent<PlayerInput>();
        _playerRig = GetComponent<Rigidbody>();
        _camTransform = Camera.main.transform;
        _playerInput.EnablePlayerAction();
    }

    private void OnEnable()
    {
        _playerInput.EnablePlayerAction();
    }

    private void OnDisable()
    {
        _playerInput.DisablePlayerAction();
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        var stateInfo = _animator.GetCurrentAnimatorStateInfo(0);
        // 按下跳跃键且在地面时可以跳跃,播放跳跃动画
        if (_playerInput.jumpInput && isOnGround())
        {
            // 播放跳跃动画
            _animator.Play("Jump");
            // 施加跳跃冲量
            _playerRig.AddForce(Vector3.up * jumpForce);
            // 跳跃时可移动转向
        }else if (!isOnGround() && _playerInput.moveInput != Vector2.zero) // 跳跃时可移动
        {
            MoveInPhysics(); //角色移动转向
        }
        else if (_playerInput.moveInput != Vector2.zero)
        {
            _animator.Play("Run");
            MoveInPhysics(); //角色移动转向
        }
        else if (isOnGround())
        {
            _animator.Play("Idle");
        }
    }

    protected void MoveInPhysics()
    {
        Vector3 moveInput = new Vector3(_playerInput.moveInput.x, 0, _playerInput.moveInput.y);
        // 相对主摄的移动(注意最后需要投影到水平面,否则会有上下位移导致镜头波动)
        Vector3 _camMove = Vector3.ProjectOnPlane(_camTransform.TransformDirection(moveInput).normalized, Vector3.up);
        // 转向
        _playerRig.MoveRotation(Quaternion.RotateTowards(_playerRig.rotation, Quaternion.LookRotation(_camMove), 30));
        // 移动
        _playerRig.MovePosition(_playerRig.position + _camMove * runSpeed * Time.fixedDeltaTime);
        Debug.Log("运动速度_playerRig.velocity:" + _playerRig.velocity);
    }

    #region ground detector

    public float radius = 0.32f;
    public LayerMask layerMask;

    private Collider[] results = new Collider[1];
    public Vector3 offset = new Vector3(0,0.26f,0);

    public bool isOnGround()
    {
        print("检测到落地!");
        return Physics.OverlapSphereNonAlloc(transform.position + offset, radius, results, layerMask) != 0;
    }

    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position + offset, radius);
    }

    #endregion
}

在这里插入图片描述
效果还行,但是跳跃还未结束就进入Idle了,当然也可以在前面加个判断,这样跳跃没播放完不切Idle:

else if (stateInfo.tagHash == Animator.StringToHash("Jump") && stateInfo.normalizedTime < 1)
        {
            // pass
        }
else if (isOnGround())
{
    _animator.Play("Idle");
}

在这里插入图片描述
OK了~

但可以明显看到整个update逻辑比较混乱,状态切换逻辑耦合严重,考虑的东西会比较杂。当未来添加更多状态时,就需要在这一大串if else代码中小心翼翼地修改,非常不优美。

有没有更好的方式呢?

3.2 简易状态模式的角色控制

在这里插入图片描述
我们发现,上面代码之所以混乱,在于同样的按键并不一定能切换相同的状态,比如按移动键,在待机和移动时按移动键(③⑤)都会播放移动动画,但在跳跃状态按移动键(④)并不会切换状态,所以需要许多if else进行判断。而且判断顺序对判断逻辑也有影响。

当后续添加诸如游泳状态时,你在if(WSAD)还要排除游泳状态,随着状态越来越多,这种窘境会越来越频繁,直至再也无法下手,游戏开发便成为一件恐惧且无趣的事。

解决办法就是:先按键识别后状态判断 改为 先状态判断后按键识别

public State _currentState;

public enum State
{
    Idle,
    Run,
    Air //跳跃态
}
void Start()
{
    ...// 省略了和前面相同的代码
    _currentState = State.Idle;
}
void FixedUpdate()
    {
        SwitchState();
        StateLogicUpdate();
    }
    private void SwitchState()
    {
        switch (_currentState)
        {
            case State.Idle:
                // 跳跃就不需要再判断OnGrounded
                if (_playerInput.jumpInput)
                {
                    _currentState = State.Air;
                    _animator.Play("Jump");
                    _playerRig.AddForce(Vector3.up * jumpForce);//这个不能放在StateLogicUpdate,否则会无限升空
                }

                if (_playerInput.moveInput != Vector2.zero)
                {
                    _currentState = State.Run;
                    _animator.Play("Run");
                }
                break;
            case State.Run:
                if (_playerInput.moveInput == Vector2.zero)
                {
                    _currentState = State.Idle;
                    _animator.Play("Idle");
                }
                if (_playerInput.jumpInput)
                {
                    _currentState = State.Air;
                    _animator.Play("Jump");
                    _playerRig.AddForce(Vector3.up * jumpForce);//这个不能放在StateLogicUpdate,否则会无限升空
                }
                break;
            case State.Air:
                var stateInfo = _animator.GetCurrentAnimatorStateInfo(0);
                // 无需利用stateTag判断是否是jump状态了
                if (isOnGround() && stateInfo.normalizedTime >= 1)
                {
                    _currentState = State.Idle;
                    _animator.Play("Idle");
                }
                if (isOnGround() && _playerInput.moveInput != Vector2.zero)
                {
                    _currentState = State.Run;
                    _animator.Play("Run");
                }
                break;
        }
    }
    private void StateLogicUpdate()
    {
        switch (_currentState)
        {
            case State.Idle:
                break;
            case State.Run:
                MoveInPhysics();
                break;
            case State.Air:
                MoveInPhysics();
                break;
        }
    }

效果:
一样丝滑
在这里插入图片描述

当然这里跳跃动画还可以优化,分割成 “ 起跳-滞空-着陆 ” 三个状态,否则跳跃高度和动画会不匹配,像下面这样(跳跃分割参考后面SMB实例)
在这里插入图片描述

可以看到,通过引入状态枚举,整体逻辑变得非常清晰,后续添加更多状态也不会混乱。而且拆分状态切换与状态内循环逻辑,结构更加优美。

但还是有个小缺陷,_playerRig.AddForce(Vector3.up * jumpForce);//这个不能放在StateLogicUpdate,否则会无限升空
这段代码是初次切换到跳跃时执行一次的逻辑,不能放到StateLogicUpdate状态内循环逻辑,如果我们能扩充状态枚举为状态类,在进入状态类开始时执行一次这段代码,那不是更完美了吗?

3.3 状态模式

在这里插入图片描述
将上面状态枚举改成状态类,并统一实现IState接口。
PlayerController负责管理所有状态类初始化,以及持有当前运行状态类IState currentState,在FixedUpdate中调用IState.SwitchState() IState.StateLogicUpdate(),交由具体IState类修改状态和执行具体状态逻辑。

3.3.1 IState

public interface IState
{
    void EnterState(){}
    void ExitState(){}
    void SwitchState(){}
    void StateLogicUpdate(){}
}

3.3.2 IdleState

public class IdleState : IState
{
    private PlayerInput _playerInput;
    private Animator _animator;
    private PlayerController_ComplexStateMode _playerController;


    public IdleState(PlayerInput playerInput, Animator animator,
        PlayerController_ComplexStateMode playerController)
    {
        _playerInput = playerInput;
        _animator = animator;
        _playerController = playerController;
    }

    public void SwitchState()
    {
        if (_playerInput.jumpInput)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._jumpState;
            _animator.Play("Jump");
            _playerController._currentState.EnterState();
            return;
        }

        if (_playerInput.moveInput != Vector2.zero)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._runState;
            _animator.Play("Run");
            _playerController._currentState.EnterState();
        }
    }
}

3.3.3 RunState

using UnityEngine;

public class RunState : IState
{
    public float runSpeed = 3f;
    
    private PlayerInput _playerInput;
    private Animator _animator;
    private Rigidbody _playerRig;
    private Transform _camTransform;
    private PlayerController_ComplexStateMode _playerController;

    public RunState(PlayerInput playerInput, Animator animator, Rigidbody playerRig,
        Transform camTransform,PlayerController_ComplexStateMode playerController)
    {
        _playerInput = playerInput;
        _animator = animator;
        _playerRig = playerRig;
        _camTransform = camTransform;
        _playerController = playerController;
    }

    public void SwitchState()
    {
        if (_playerInput.moveInput == Vector2.zero)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._idleState;
            _animator.Play("Idle");
            _playerController._currentState.EnterState();
            return;
        }

        if (_playerInput.jumpInput)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._jumpState;
            _animator.Play("Jump");
            _playerController._currentState.EnterState();
            return;
        }
    }

    public void StateLogicUpdate()
    {
        MoveInPhysics();//和前面相同
    }
}

3.3.4 JumpState

using UnityEngine;

public class JumpState : IState
{
    public float runSpeed = 3f;
    public float jumpForce = 200f;

    private PlayerInput _playerInput;
    private Animator _animator;
    private Rigidbody _playerRig;
    private Transform _camTransform;
    private PlayerController_ComplexStateMode _playerController;

    public JumpState(PlayerInput playerInput, Animator animator, Rigidbody playerRig,
         Transform camTransform, PlayerController_ComplexStateMode playerController)
    {
        _playerInput = playerInput;
        _animator = animator;
        _playerRig = playerRig;
        _camTransform = camTransform;
        _playerController = playerController;
    }

    public void EnterState()
    {
        _playerRig.AddForce(Vector3.up * jumpForce);
    }

    public void SwitchState()
    {
        var stateInfo = _animator.GetCurrentAnimatorStateInfo(0);
        
        if (_playerController.isOnGround() && stateInfo.normalizedTime >= 1)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._idleState;
            _animator.Play("Idle");
            _playerController._currentState.EnterState();
            return;
        }

        if (_playerController.isOnGround() && _playerInput.moveInput != Vector2.zero)
        {
            _playerController._currentState.ExitState();
            _playerController._currentState = _playerController._runState;
            _animator.Play("Run");
            _playerController._currentState.EnterState();
        }
    }

    public void StateLogicUpdate()
    {
        MoveInPhysics();//和前面相同
    }
}

3.3.5 PlayerController_ComplexStateMode

using System;
using UnityEngine;

public class PlayerController_ComplexStateMode : MonoBehaviour
{
    private Animator _animator;
    private PlayerInput _playerInput;
    private Rigidbody _playerRig;

    private Transform _camTransform;

    // 状态类
    public IState _currentState;

    public IdleState _idleState;
    public RunState _runState;
    public JumpState _jumpState;

    private void Awake()
    {
        _animator = GetComponentInChildren<Animator>();
        _playerInput = GetComponent<PlayerInput>();
        _playerRig = GetComponent<Rigidbody>();
        _camTransform = Camera.main.transform;
    }

    void Start()
    {
        _idleState = new IdleState(_playerInput, _animator, this);
        _runState = new RunState(_playerInput, _animator, _playerRig, _camTransform, this);
        _jumpState = new JumpState(_playerInput, _animator, _playerRig, _camTransform, this);
        
        _playerInput.EnablePlayerAction();
        _currentState = _idleState;
    }

    void FixedUpdate()
    {
        StateMachineJob();
    }

    // 执行当前状态机的逻辑,包含enter
    private void StateMachineJob()
    {
        _currentState.SwitchState();
        _currentState.StateLogicUpdate();
    }

    // 启动输入系统
    // 注意_playerInput初始化放到awake中,否则会报NullReferenceException(但不影响角色控制?)
    private void OnEnable()
    {
        _playerInput.EnablePlayerAction();
    }

    private void OnDisable()
    {
        _playerInput.DisablePlayerAction();
    }

    //ground detector逻辑也可以抽离成单独类
    #region ground detector
    
    public float radius = 0.32f;
    public LayerMask layerMask;

    private Collider[] results = new Collider[1];
    public Vector3 offset = new Vector3(0, 0.26f, 0);

    public bool isOnGround()
    {
        return Physics.OverlapSphereNonAlloc(transform.position + offset, radius, results, layerMask) != 0;
    }

    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position + offset, radius);
    }

    #endregion
}

3.3.6 注意事项

  • 本文落地检测使用了layer,遇到落地后无法切换到Idle状态检查下 OnGroundDetector 的layer参数是否配置!
  • 注意_playerInput的初始化放到awake中(而不是start中,unity执行顺序是awake→OnEnable→start),否则会报NullReferenceException(但不影响角色控制?)

其实上面代码可以继续优化,比如将PlayerController中关于状态类的部分(State初始化,currentState变量持有,执行switch stateUpdate等)抽离成单独类比如StateMachine

3.4 SMB

Unity其实已经帮我们实现了上面的状态模式,SMB(State Machine Behaviour)就是类似IState的功能。
而且在内部隐藏了管理状态和控制状态切换和执行enter、stateUpdate、exit等逻辑。

在这里插入图片描述
事件函数的执行顺序

使用SMB方法很简单,就是新增脚本继承StateMachineBehaviour方法(或者点击状态机里状态的Add Behaviour按钮),然后添加到状态里。
在这里插入图片描述
StateMachineBehaviour包含三个常用方法::

  • OnStateEnter 进入状态时执行一次
  • OnStateExit 离开状态时执行一次
  • OnStateUpdate 除第一帧和最后一帧外,在每个 Update 帧上进行调用

其它方法本文暂不涉及。

值得注意的是,同一时刻可能包含两个状态,即当前状态和下一个状态(针对包含过渡的animator而言,这里不考虑过渡中断)

以状态A向状态B过渡为例,如下图所示,过渡开始时,原先A状态的update并不停止,B的enter也早早开始,直到过渡结束,A调用exit,B才开始执行Update。这个在后面使用crossFade进行状态过渡时需要着重注意。

在这里插入图片描述
那么下面正式开始用SMB重构上面代码!

四 基于SMB的角色控制

注意:项目路径千万不要带中文,否则会遇到奇怪的bug,比如:

  1. editor频繁hold on报rider相关的东西
    在这里插入图片描述
  2. 添加cinemachine就会报错GUI相关的NullReferenceException。

注意:unity_chan有generic和humanoid两种,动画也分这两类,状态机添加动画时要对应,否则会摆A-pose。

4.1 项目实战案例

4.1.1资源准备

  • 添加Q版Unity Chan角色 :SD chan Animation bundle
  • 添加cinemachine、NewInputSystem 插件

4.1.2 目录结构:

在这里插入图片描述

角色挂载父节点Player下。
父节点Player添加Rigidbody、Player Input组件,添加下面PlayerInput脚本
在这里插入图片描述

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerInput : MonoBehaviour
{
    public Vector2 moveInput;
    public bool jumpInput;
    public bool sprintInput;
    public bool fireInput;

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        moveInput = context.ReadValue<Vector2>();
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        jumpInput = context.ReadValueAsButton();
    }
}

4.1.3 状态机

本文状态切换使用纯代码控制,所以只需要添加状态,无需添加transition和parameter。

添加三个state,为每个state添加SMB。
在这里插入图片描述

4.1.4 cinemachine参数

cinemachine使用free look相机,参数可参考下面:
在这里插入图片描述

4.1.5 UnityChan_Idle_SMB

using UnityEngine;

public class UnityChan_Idle_SMB : StateMachineBehaviour
{
    private PlayerInput _playerInput;
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        _playerInput = animator.GetComponentInParent<PlayerInput>();
    }

    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        SwitchState(animator, stateInfo, layerIndex);
    }

    private void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (_playerInput.moveInput.magnitude > 0)
        {
            animator.Play("Run",0);
        }

        if (_playerInput.jumpInput)
        {
            animator.Play("Jump",0);
        }
    }
}

4.1.6 UnityChan_Run_SMB

using UnityEngine;

public class UnityChan_Run_SMB : StateMachineBehaviour
{
    private PlayerInput _playerInput;
    private Transform _playerTransform;
    private Transform _camTransform;
    public float runSpeed = 5f;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        _playerInput = animator.GetComponentInParent<PlayerInput>();
        _playerTransform = animator.transform;
        _camTransform = Camera.main.transform;
    }

    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        SwitchState(animator, stateInfo, layerIndex);
        DoStateJob();
    }

    void DoStateJob()
    {
        Vector3 moveInput = new Vector3(_playerInput.moveInput.x, 0, _playerInput.moveInput.y);
        // 相对主摄的移动(注意最后需要投影到水平面,否则会有上下位移导致镜头波动)
        Vector3 camMove = Vector3.ProjectOnPlane(_camTransform.TransformDirection(moveInput).normalized, Vector3.up);
        // 转向
        _playerTransform.rotation =
            Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(camMove), 30);
        // 移动
        _playerTransform.Translate(camMove * runSpeed * Time.deltaTime, Space.World);
    }

    void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (_playerInput.moveInput.magnitude < 0.01f)
        {
            animator.Play("Idle", layerIndex);
        }

        if (_playerInput.jumpInput)
        {
            animator.Play("Jump", layerIndex);
        }
    }
}

4.1.7 UnityChan_Jump_SMB

using UnityEngine;

public class UnityChan_Jump_SMB : StateMachineBehaviour
{
    private PlayerInput _playerInput;
    [Range(0,1)]
    public float transitionDuration = 0.1f;
    private Transform _playerTransform;
    private Transform _camTransform;
    public float runSpeed = 5f;
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        _playerInput = animator.GetComponentInParent<PlayerInput>();
        _playerTransform = animator.transform;
        _camTransform = Camera.main.transform;
    }

    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        SwitchState(animator, stateInfo, layerIndex);
        DoStateJob();
    }

    private void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 检查动画的播放进度,≥1表示动画播放完毕
        if (stateInfo.normalizedTime >= 1.0f)
        {
            // 切换到Idle动画
            animator.Play("Idle", layerIndex);
        }
    }
    void DoStateJob()
    {
        Vector3 moveInput = new Vector3(_playerInput.moveInput.x, 0, _playerInput.moveInput.y);
        Vector3 _camMove = Vector3.ProjectOnPlane(_camTransform.TransformDirection(moveInput).normalized,Vector3.up);
        // 转向

        _playerTransform.rotation =
            Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(_camMove), 30);
        // 移动
        _playerTransform.Translate(_camMove * runSpeed * Time.deltaTime, Space.World);
    }
}

这里用到了AnimatorStateInfo.normalizedTime来判断动画播放进度,比如4.306表示动画循环了4次,目前播放到30%。

在这里插入图片描述

4.1.8 效果

在这里插入图片描述

存在几个问题:

  • 过渡不丝滑
  • 跳跃落地状态未分割,无法精细控制
  • 角色抖动

4.2 案例优化

4.2.1 过渡不丝滑

使用Animator.CrossFade(int stateHashName, float normalizedTransitionDuration)方法代替Play方法实现平滑过渡。

但初次使用时会发现奇怪的bug,比如动画不动了。

例如A状态过渡到B状态,通过日志打印发现A的update方法一直执行,而B反复enter和exit。

说明在过渡时上一个状态依旧能执行OnStateUpdate方法,导致反复执行里面的SwitchState中animator.CrossFade方法,所以导致走走不动、跳跳不起的现象。

解决方法:加入animator.IsInTransition(layerIndex)判断。

Idle的SwitchState代码:

protected override void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (animator.IsInTransition(layerIndex))
        {
            return;
        }
        // 奔跑
        if (_playerInput.moveInput != Vector2.zero)
        {
            // animator.Play(PLAYER_STATE_RUN,layerIndex);
            animator.CrossFade(PLAYER_STATE_RUN,0.25f);
        }

        // 跳跃
        if (_playerInput.jumpInput)
        {
            // animator.Play(PLAYER_STATE_JUMP,layerIndex);
            animator.CrossFade(PLAYER_STATE_JUMP,0.25f);
        }
    }

左无过渡,右有过渡(仔细看发尾)
在这里插入图片描述   在这里插入图片描述

4.2.2 优化SMB,添加统一父类

这样其他SMB只要继承这个父类就行,简化代码(若报缺失类接着往下看)

public class Player_Base_SMB : StateMachineBehaviour
{ 
    protected static int PLAYER_STATE_IDLE = Animator.StringToHash("Idle");
    protected static int PLAYER_STATE_RUN = Animator.StringToHash("Run");
    protected static int PLAYER_STATE_JUMPUP = Animator.StringToHash("JumpUp");
    protected static int PLAYER_STATE_FALL = Animator.StringToHash("Fall");
    protected static int PLAYER_STATE_LAND = Animator.StringToHash("Land");

    public string StateName;
    public float runSpeed = 3f;
    protected PlayerInput _playerInput;
    protected PlayerController _playerController;
    protected Transform _playerTransform;
    protected Transform _camTransform;
    protected Rigidbody _playerRig;
    
    protected bool isOnGround() => _playerController.isOnGround();
    protected bool AnimationPlayFinished(AnimatorStateInfo stateInfo)
    {
        return stateInfo.normalizedTime >= 1.0f;
    }

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // Debug.Log("Enter in "+ StateName + " state!");
        _playerInput = animator.GetComponentInParent<PlayerInput>();
        _playerController = animator.GetComponentInParent<PlayerController>();
        _playerTransform = _playerController.transform;
        _playerRig = animator.GetComponentInParent<Rigidbody>();
        _camTransform = Camera.main.transform;
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // Debug.Log("Do update for "+ StateName + " state!");
        SwitchState(animator, stateInfo, layerIndex);
        DoStateJob(animator, stateInfo, layerIndex);
    }

    protected virtual void DoStateJob(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    protected virtual void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // Debug.Log("Exit from "+ StateName + " state!");
    }
    protected void DoMoveInPhysics()
    {
        if (_playerInput.moveInput != Vector2.zero)
        {
            Vector3 moveInput = new Vector3(_playerInput.moveInput.x, 0, _playerInput.moveInput.y);
            // 相对主摄的移动(注意最后需要投影到水平面,否则会有上下位移导致镜头波动)
            Vector3 _camMove = Vector3.ProjectOnPlane(_camTransform.TransformDirection(moveInput).normalized, Vector3.up);
            // 转向
            _playerRig.MoveRotation(Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(_camMove), 30));
            // 移动
            _playerRig.MovePosition(_playerRig.position + _camMove * runSpeed * Time.fixedDeltaTime);
        }
        Debug.Log("运动速度_playerRig.velocity:"+_playerRig.velocity);
    }
    protected void DoMoveNoPhysics()
    {
        if (_playerInput.moveInput != Vector2.zero)
        {
            Vector3 moveInput = new Vector3(_playerInput.moveInput.x, 0, _playerInput.moveInput.y);
            // 相对主摄的移动(注意最后需要投影到水平面,否则会有上下位移导致镜头波动)
            Vector3 _camMove = Vector3.ProjectOnPlane(_camTransform.TransformDirection(moveInput).normalized, Vector3.up);
            // 转向
            _playerTransform.rotation =
                Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(_camMove), 30);
            // 移动
            _playerTransform.Translate(_camMove * runSpeed * Time.fixedDeltaTime, Space.World);
        }
        Debug.Log("运动速度_playerRig.velocity:"+_playerRig.velocity);
    }
}

4.2.3 优化跳跃流程

4.2.3.1 分割跳跃动画

在这里插入图片描述

4.2.3.2 添加落地检测
public class PlayerGroundDetector : MonoBehaviour
{
    [SerializeField] float detectionRadius = 0.1f;
    [SerializeField] LayerMask groundLayer;

    Collider[] colliders = new Collider[1];

    public bool IsGrounded => Physics.OverlapSphereNonAlloc(transform.position, detectionRadius, colliders, groundLayer) != 0;

    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position, detectionRadius);
    }
}

在这里插入图片描述
落地检测需要仔细调整,因为跳跃动画脚会抬起,所以offset往下移一些,radiu适当大一点,否则角色本身碰撞体会先判定导致角色卡住
在这里插入图片描述

4.2.3.3 新建三个跳跃相关的SMB

在这里插入图片描述

  • UnityChan_JumpUp_SMB

    public class UnityChan_JumpUp_SMB : Player_Base_SMB
    {
        [Range(0,1)]
        public float transitionDuration = 0.1f;
        public float jumpForce = 5f;
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            StateName = "JumpUp";
            base.OnStateEnter(animator,stateInfo,layerIndex);
            _playerRig.AddForce(Vector3.up*jumpForce,ForceMode.Force);
        }
    
        protected override void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (_playerRig.velocity.y < 0 && !animator.IsInTransition(layerIndex))
            {
                animator.CrossFade(PLAYER_STATE_FALL,transitionDuration);
            }
        }
        protected override void DoStateJob(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            DoMoveInPhysics();
        }
    }
    
  • UnityChan_Fall_SMB

    public class UnityChan_Fall_SMB : Player_Base_SMB
    {
        [Range(0,1)]
        public float transitionDuration = 0.1f;
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            base.OnStateEnter(animator,stateInfo,layerIndex);
            StateName = "Fall";
        }
    
        protected override void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (animator.IsInTransition(layerIndex))
            {
                return;
            }
            if (isOnGround())
            {
                animator.CrossFade(PLAYER_STATE_LAND,transitionDuration);
            }
        }
        protected override void DoStateJob(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            DoMoveInPhysics();
        }
    }
    
  • UnityChan_Land_SMB

    public class UnityChan_Land_SMB : Player_Base_SMB
    {
        [Range(0,1)]
        public float transitionDuration = 0.1f;
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            base.OnStateEnter(animator,stateInfo,layerIndex);
            StateName = "Land";
        }
    
        protected override void SwitchState(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (animator.IsInTransition(layerIndex))
            {
                return;
            }
            // 落地结束播放待机动画
            if (AnimationPlayFinished(stateInfo))
            {
                animator.CrossFade(PLAYER_STATE_IDLE,transitionDuration);
            }
            // 奔跑
            if (_playerInput.moveInput != Vector2.zero)
            {
                // animator.Play(PLAYER_STATE_RUN,layerIndex);
                animator.CrossFade(PLAYER_STATE_RUN,0.25f);
            }
            // 跳跃
            if (_playerInput.jumpInput)
            {
                // animator.Play(PLAYER_STATE_JUMP,layerIndex);
                animator.CrossFade(PLAYER_STATE_JUMPUP,0.25f);
            }
        }
    }
    

4.2.4 角色抖动

发现出现角色抖动问题
在这里插入图片描述
尝试解决方法:

  • 跳跃的loop time不要勾选;

  • 相机aim添加垂直阻尼
    在这里插入图片描述
    还是不行,仔细查看是位移时震颤。
    在这里插入图片描述
    UnityChan移动跳跃降落都会震颤

  • 修改刚体插值为interpolate 或extrapolate(对跳跃和降落震颤有效,但移动抖动无效。)
    在这里插入图片描述

  • 改变状态机update mode
    在这里插入图片描述

  • 用物理的方式更新位置

    Tranform.TranslateRigidbody.MoveRotationRigidbody.MovePosition

    _playerRig.MoveRotation(Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(_camMove), 30));
    _playerRig.MovePosition(_playerRig.position + _camMove * runSpeed * Time.fixedDeltaTime);
    

    但还是不行

  • 改变相机更新模式
    在这里插入图片描述
    水平运动可以,跳跃和降落时的垂直运动依然存在抖动现象。
    而且相机使用lateUpdate背景抖,角色不抖;相机使用fixedUpdate 人物抖背景不抖
    在这里插入图片描述
    对比scene和game窗口,发现还是镜头问题
    在这里插入图片描述
    关闭cinemachine发现跳跃抖动消除了,说明确实是cinemachien的问题,搜索 “ unity cinemachine aiming jittery ”,发现是RigidBody.Interpolation 和 cinemachine不兼容。
    在这里插入图片描述

Cinemachine - Crazy jitter

总结:

首先区分是角色本身抖动还是镜头抖动(对比scene和game窗口,关闭cinemachine插件等方式)

  1. 角色本身抖动,分动画抖动和移动抖动

    • 动画抖动:将动画loop关闭,合理裁剪动画保留1个关键帧即可

    • 移动抖动:用物理方式更新位置和旋转,animator组件的 update mode改为 Animate Physics

      _playerRig.MoveRotation(Quaternion.RotateTowards(_playerTransform.rotation, Quaternion.LookRotation(_camMove), 30));
      _playerRig.MovePosition(_playerRig.position + _camMove * runSpeed * Time.fixedDeltaTime);
      
  2. 镜头抖动

    • 如果使用了cinemachine 插件,那可能就是与RigidBody.Interpolation兼容问题。cinemachine brain update method为fixedupdate/smart 都可以(lateupdate背景依然抖),但RigidBody.Interpolation一定要none。

最终丝滑效果:
在这里插入图片描述

4.2.5 New Input System另种用法

除了常见的PlayerInput组件,还可以用纯代码的方式。
首先在InputAction按键设置文件的Inspector栏生成对应的C#文件
在这里插入图片描述
然后再自己的InputController类引用这个生成类,当然为了方便使用可以直接继承其中的接口,这样就能生成代实现的方法模板。

注意InputAction必须要enable才能生效,方法要加入委托才能被监听:
_unityChanInputAction.Player.Enable();
_unityChanInputAction.Player.AddCallbacks(this);

完整代码:

public class PlayerInput : MonoBehaviour,UnityChanInputAction.IPlayerActions
{
    private UnityChanInputAction _unityChanInputAction;
    public Vector2 moveInput;
    public bool jumpInput;

    private void Awake()
    {
        _unityChanInputAction = new UnityChanInputAction();
    }

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        _unityChanInputAction.Player.Enable();
        _unityChanInputAction.Player.AddCallbacks(this);
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        moveInput = context.ReadValue<Vector2>();
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        jumpInput = context.ReadValueAsButton();
    }
}

对New Input System不熟悉的可以参见 【Unity学习笔记·第十二】Unity New Input System 及其系统结构和源码浅析

五 后记

至此,本文详细梳理了一遍状态模式,对于状态模式使用的必要性也有了深刻的认识,也更能体会SMB带来的便宜性。

下篇文章预计研究技能系统和Timeline~

拜~

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【NOIP提高组】 自由落体

【NOIP提高组】 自由落体 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 在高为 H 的天花板上有 n 个小球&#xff0c;体积不计&#xff0c;位置分别为 0&#xff0c;1&#xff0c;2&#xff0c;…&#xff0e;n-1。在地面上有一个小车&…

ECMAScript 标准详解

ECMAScript 是 JavaScript 的基础标准&#xff0c;由 Ecma International 制定。它定义了脚本语言的语法和行为。自 1997 年以来&#xff0c;ECMAScript 经过了多个版本的迭代&#xff0c;每个版本都对 JavaScript 产生了深远的影响。 1. ECMAScript 1 (ES1) 发布时间&#xf…

react18中的受控与非受控组件及ref的使用

受控与非受控组件 受控组件,基于修改 state 的值,修改组件内部的状态&#xff0c;来实现页面的更新&#xff0c;推荐使用 非受控组件&#xff0c;基于 ref 获取 dom 的值&#xff0c;来实现页面的更新,不推荐使用,偶尔特殊的场景会使用 给需要获取的元素设置 ref“xxx”,后期基…

Pytorch学习--如何下载及使用Pytorch中自带数据集,如何把数据集和transforms联合在一起使用

一、标准数据集使用 pytorch官网–标准数据集 这里以CIFAR10数据集为例&#xff1a;CIFAR10 下载数据集 代码&#xff1a; import torchvision train_datatorchvision.datasets.CIFAR10(root"datasets",trainTrue,downloadTrue) test_datatorchvision.datasets.…

【实用知识】Spring Boot 优雅捕捉异常的几种姿势

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

计算机网络:数据链路层 —— 虚拟局域网 VLAN

文章目录 局域网虚拟局域网 VLAN虚拟局域网 VLAN 概述实现机制IEEE 802.1Q帧以太网交换机的接口类型Access 接口Trunk 接口Hybrid 接口不进行人为的VLAN划分划分两个不同VLANTrunk接口去标签后进行转发Trunk接口直接转发 局域网 局域网&#xff08;Local Area Network&#xf…

ICP之点云特征计算

这次我们来聊一下点云的特征计算方法&#xff0c; 主流的有两类 1&#xff1a;基于直方图的特征计算 2&#xff1a;基于签名的特征计算 这次我将介绍基于直方图的方式。 基于直方图的特征方法中&#xff0c;PFH&#xff08;Point Feature Histograms&#xff09;和FPFH&#x…

vue3项目中引入阿里图标库

开篇 本篇的主题是在vue3项目中引入阿里图标库 步骤 注册阿里图标库账号(阿里图标)&#xff0c;并创建项目 将图标加入项目中 将需要的图标先加入购物车&#xff0c;随后加入到项目中 生成项目代码 在项目中生成项目代码&#xff0c;便于后续复制到vue项目中 ## 在vue3项目…

基于Ubuntu24.04,下载并编译Android12系统源码 (一)

1. 前言 1.1 编译源码可以干什么 定制Android系统将最新版本的Android系统刷入到自己的Android设备中将整个系统源码导入到Android Studio中&#xff08;可以不用编译源码来实现&#xff09;。 只要有对应的Android源码版本的android.iml和android.ipr文件&#xff0c;就可以…

使用 v-html 指令渲染的标签, 标签内绑定的 click 事件不生效

背景 在项目开发中&#xff0c;实现用户友好的输入交互是提升用户体验的关键之一。例如&#xff0c;在客服对话框中&#xff0c;其中有包含多个快捷选项用于快速问答&#xff0c;每个快捷选项都是一个可点击的按钮&#xff0c;并需要绑定点击事件来执行相应操作。然而&#xf…

JavaSE笔记2】面向对象

目录 一、深刻认识面向对象 二、对象在计算机中的执行原理 三、this 四、构造器 五、封装 六、实体Javabean(实体类) 1. 是什么&#xff1f; 2. 实体类用来干什么&#xff1f; 七、成员变量和局部变量的区别 一、深刻认识面向对象 二、对象在计算机中的执行原理 三、this Java中…

LINUX1.3

cp&#xff08;复制&#xff09;: 将需要复制的文件或目录&#xff08;源&#xff09;重建一份&#xff0c;并保存为新的文件或目录 cp 源文件 -------目标地址 在当前文件夹时&#xff1a; cp 源文件 &#xff08;要改名&#xff09;------目的地址&#xff08;可以改名或…

Redis高频面试题

一、Redis有什么好处? 高性能:Redis是一个基于内存的数据存储系统,相比于传统的基于磁盘的数据库系统,它能够提供更高的读写性能。支持丰富的数据类型:Redis支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等,这使得它可以用于多种不同的应用场景。持久化:Re…

4.2-7 运行MR应用:词频统计

文章目录 1. 准备数据文件2. 文件上传到HDFS指定目录2.1 创建HDFS目录2.2 上传文件到HDFS2.3 查看上传的文件 3. 运行词频统计程序的jar包3.1 查看Hadoop自带示例jar包3.2 运行示例jar包里的词频统计 4. 查看词频统计结果5. 在HDFS集群UI界面查看结果文件6. 在YARN集群UI界面查…

基于Python和OpenCV的疲劳检测系统设计与实现

项目运行 需要先安装Python的相关依赖&#xff1a;pymysql&#xff0c;Django3.2.8&#xff0c;pillow 使用pip install 安装 第一步&#xff1a;创建数据库 第二步&#xff1a;执行SQL语句&#xff0c;.sql文件&#xff0c;运行该文件中的SQL语句 第三步&#xff1a;修改源…

【Web开发】什么是Nuxt? 利用Nuxt快速搭建前端项目

Nuxt官网&#xff1a;https://nuxt.com/ 启动一个Nuxt项目 在vscode的项目文件终端运行以下命令&#xff1a; npx nuxilatest init <my-app>npm installnpm run dev然后就启动了一个Nuxt项目 安装Nuxt UI Nuxt UI官网&#xff1a;https://ui.nuxt.com/ npx nuxilates…

【acwing】算法基础课-搜索与图论

目录 1、dfs(深度优先搜索) 1.1 排列数字 1.2 n皇后问题 搜索顺序1 搜索顺序2 2、bfs(广度优先搜索) 2.1 走迷宫 2.2 八数码 3、树与图的存储 4、树与图的遍历 4.1 树的重心 4.2 图中点的层次 5、拓扑排序 6、最短路问题 6.1 朴素Dijkstra算法 6.2 堆优化Dijks…

JAVA基础:集合 (学习笔记)

集合 什么是集合&#xff1f; 一种引用数据类型&#xff0c;可以存储多个数据 为什么要学习集合&#xff1f; 数组的缺点&#xff1a; &#xff08;1&#xff09;空间长度不可以改变。 &#xff08;2&#xff09;没办法获得数组中真实的元素个数。 &#xff08;3&#xff…

江协科技STM32学习- P22 实验-ADC单通道/ADC多通道

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

『 Linux 』网络传输层 - TCP (一)

文章目录 TCP协议TCP数据段格式TCP确认应答机制TCP流量控制 TCP协议 TCP协议(Transmission Control Protocol , 传输控制协议) 是互联网协议套件中的核心协议之一; 主要用于确保数据在网络上的可靠传输,其具有以下特点: 面向连接 在数据传输前,TCP需要在通信双方之间建立一个连…