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

news2024/10/24 21:30:14

在这里插入图片描述

目录

  • 一 内容摘要
  • 二 前言
  • 三 状态模式的必要性
    • 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.coloradmin.cn/o/2222718.html

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

相关文章

Java | Leetcode Java题解之第497题非重叠矩形中的随机点

题目&#xff1a; 题解&#xff1a; class Solution {Random rand;List<Integer> arr;int[][] rects;public Solution(int[][] rects) {rand new Random();arr new ArrayList<Integer>();arr.add(0);this.rects rects;for (int[] rect : rects) {int a rect[0…

【大模型问答测试】大模型问答测试脚本实现(第二版)——接入pytest与代码解耦

背景 接上一篇&#xff0c;【大模型问答测试】大模型问答测试脚本实现&#xff08;第一版&#xff09;。 在实现自动化的时候&#xff0c;原先把很多方法与request请求写在一块了&#xff0c;趁着目前实现接口数量较少&#xff0c;决定对代码进行解耦&#xff0c;并且清晰目录…

《梦里花落知多少》凄美地,如同散落的花

《梦里花落知多少》凄美地&#xff0c;如同散落的花 三毛&#xff08;1943/3/26~1991/01/04&#xff09;&#xff0c;本名陈平&#xff0c;当代女作家&#xff0c;旅行家。著有《雨季不再来》《撒哈拉的故事》《哭泣的骆驼》《稻草人手记》《温柔的夜》《梦里花落知多少》等作品…

HDFS异常org.apache.hadoop.hdfs.protocol.NSQuotaExceededException

HDFS异常org.apache.hadoop.hdfs.protocol.NSQuotaExceededException 异常信息&#xff1a; Hive:org.apache.hadoop.hdfs.protocol.NSQuotaExceededException: The NameSpace quota (directories and files) of directory /xxxdir is exceeded: quota10000 file count15001N…

代码随想录-哈希表-有效的字母异位词

思路 哈希表的三种方式:数组、set、map 本题采用数组形式的哈希表来解决 三个注意事项: ①数组哈希表定义&#xff0c;包括思路和原始大小 ②Java中字符串的某个字符访问方式&#xff0c;字符串长度的方法 ③for-each循环的书写方式 代码 class Solution {public boole…

Python 爬虫下载图片

使用免费的代理ip进行网络请求,降低了反爬机制的触发率。加入自动发送邮件的功能,在代码运行出错的时候可以及时收到提醒消息。 主程序代码: import requests import os import time from lxml import etree from bs4 import BeautifulSoup import random # 自定义模块,发…

安全防护修改用户sudo权限

修改ssh端口 vim /etc/ssh/sshd_config 修改port端口为60022端口&#xff0c;端口最大为65535 修改完&#xff0c;重启服务 可以看到此时ssh监听端口为60022,此时远程登陆时就需要用端口60022&#xff0c;原来的22端口拒绝访问 下载nmap端口扫描工具 用命令nmap 192.168.45.…

CZX前端秘籍2

vue生命周期&#xff08; 组件从创建到销毁的过程就是它的生命周期&#xff09; 创建前 beforeCreat&#xff08; 在这个阶段属性和方法都不能使用&#xff09; 创建时 created&#xff08; 这里时实例创建完成之后&#xff0c; 在这里完成了数据监测&#xff0c; 可以使用数…

【C++进阶】之C++11的简单介绍(二)

&#x1f4c3;博客主页&#xff1a; 小镇敲码人 &#x1f49a;代码仓库&#xff0c;欢迎访问 &#x1f680; 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f30f; 任尔江湖满血骨&#xff0c;我自踏雪寻梅香。 万千浮云遮碧…

C#使用log4net结合sqlite数据库记录日志

0 前言 为什么要把日志存到数据库里? 因为结构化的数据库存储的日志信息,可以写专门的软件读取历史日志信息,通过各种条件筛选,可操作性极大增强,有这方面需求的开发人员可以考虑。 为什么选择SQLite? 轻量级数据库,免安装,数据库的常用的基本功能都有,可以随程序…

批量修改YOLO格式的标注类别

1.解决的问题 假如你有一个YOLO格式的数据集&#xff0c;标注类别为0&#xff0c;1&#xff0c;2&#xff0c;3四个类别标签。如果你想删除标签1&#xff0c;只保留0&#xff0c;2&#xff0c;3类别的标注信息&#xff0c;或者想将标签0和标签1合并为标签1&#xff0c;只剩下标…

第三届“奇安信杯”网络安全技能竞赛 部分题目WriteUP

第三届“奇安信杯”网络安全技能竞赛WriteUP 文章目录 第三届“奇安信杯”网络安全技能竞赛WriteUPMISCGIFpycseeyouagain CRYPTObase全家桶base6432rsa WEB MISC GIF 下载附件&#xff0c;解压得到test1.jpg。 用010 Editor打开&#xff0c;发现GIF文件头&#xff0c;修改文…

从“Hello World”到“Success” —— 1024程序员节的感悟与成长

目录 1.成为程序员 2.成长之路 3.困难与挑战 4.磨炼与前进 5.总结与收获 6.感悟 1.成为程序员 今天&#xff0c;我们迎来了专属于程序员的节日——1024程序员节。这一天不仅是对所有编程爱好者的致敬&#xff0c;更是回顾过去一年来成长历程的时刻。对于每一位踏上编程之旅…

AI带货主播如何打造真实视觉效果!

AI带货主播作为新兴的数字营销手段&#xff0c;正在逐步改变着电商行业的面貌&#xff0c;AI技术的不断进步使得带货主播能够以更加真实、生动的视觉效果展现在消费者面前&#xff0c;从而大大提升了购物体验和销售转化率。 那么&#xff0c;AI带货主播如何打造真实视觉效果呢…

深入浅出神经网络:从基础原理到高级应用

第5章 神经网络 更加详细内容可以看这篇文章 5.1 神经元模型 神经网络的基本单元是神经元模型。神经元模拟了生物神经元的行为&#xff0c;通过接收输入信号&#xff0c;进行加权求和&#xff0c;然后经过激活函数输出结果。 数学上&#xff0c;一个简单的神经元可以表示为&…

业务开发常见问题-并发工具类

hello&#xff0c;大家好&#xff0c;本讲我们一起聊一下常见的几个并发工具类的使用和坑&#xff01; 在日常工作中&#xff0c;我们经常会遇到多线程并发问题&#xff0c;比如ThreadLocal、锁、ConcurrentHashMap、CopyOnWriteArrayList等。那么如何正常的使用呢&#xff1f;…

P7400 [COCI2020-2021#5] Magenta 题解

#1024程序员节&#xff5c;征文# 人生中的第二道紫题。。。 题目传送门 解题思路 下文中的距离指的是 a , b a,b a,b 之间的边的数量。 Sub 2 即所有边 Paula 与 Marin 都可以行走。 根据题意 Paula 先手。因此&#xff0c;如果一开始 Paula 动不了&#xff0c;那么 M…

浏览器的渲染过程

文章目录 什么是浏览器的渲染&#xff1f;浏览器渲染过程面试问点&#xff1a;为什么操作DOM慢&#xff1f;回流与重绘那么&#xff0c;什么情况下会触发回流&#xff1f; 浏览器的优化 什么是浏览器的渲染&#xff1f; 简单的说就是浏览器将 HTML 代码解析出来&#xff0c;把…

轻松学会!回收站数据恢复的几种妙招

回收站数据恢复方法是一个涉及计算机操作和数据安全的重要话题。在日常使用电脑的过程中&#xff0c;我们经常会遇到误删文件或清空回收站的情况&#xff0c;导致重要数据丢失。幸运的是&#xff0c;有多种方法可以尝试恢复这些丢失的数据。以下将详细介绍几种常见的回收站数据…

C++: C/C++内存管理

前言 本篇博客将详细介绍C的内存管理 &#x1f496; 个人主页&#xff1a;熬夜写代码的小蔡 &#x1f5a5; 文章专栏&#xff1a;C 若有问题 评论区见 &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 ​ 一.C/C内存分布 让我们先来看看下面的代码吧 int globalVar 1; st…