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

【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~

拜~

在这里插入图片描述


http://www.kler.cn/news/364098.html

相关文章:

  • ASP.NET Core 8.0 中使用 Hangfire 调度 API
  • Python异步编程:使用`asyncio`和`aiofiles`进行高效的文件批量写入
  • ctfshow-web入门-web31
  • Vue2、Element中实现Enter模拟Tab,实现切换下一个框的效果
  • 链表(虚拟头节点)
  • 前端技巧第一期
  • ArcGIS计算落入面图层中的线的长度或面的面积
  • 十七、行为型(命令模式)
  • 社区团购在一线城市的新机遇:定制开发小程序助力用户细分
  • Lua简介
  • 【CSS in Depth 2 精译_054】8.2 CSS 层叠图层(cascade layer)的推荐组织方案
  • Redis 安装部署与常用命令
  • 【H2O2|全栈】JS入门知识(八)DOM(2)
  • rabbitmq 使用注意事项
  • JVM 的定义、内部工作原理以及不同 JVM 实现的区别, Oracle JVM 、 OpenJ9、GraalVM对比。
  • 51 单片机[11]:蜂鸣器播放提示音和音乐
  • DNS 原理
  • 证明非平方整数阶射影平面关联矩阵的主对角线有t+1个1
  • Python 爬虫下载图片
  • 将 Docker 安装到指定目录
  • Spring Boot 中常见的注解,分类列出
  • 机房巡检机器人有哪些功能和作用
  • 【数据分析】Power BI的使用教程
  • asp.net core会话session设置滑动过期时间
  • Web3.0技术入门
  • YOLO11改进 | 主干网络 | 简单而优雅且有效的VanillaNet 【华为诺亚方舟】