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

news/2024/10/28 19:32:39/

在这里插入图片描述

目录

  • 一 内容摘要
  • 二 前言
  • 状态模式的必要性
    • 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 framevoid 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 detectorpublic 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:// 跳跃就不需要再判断OnGroundedif (_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();}// 执行当前状态机的逻辑,包含enterprivate void StateMachineJob(){_currentState.SwitchState();_currentState.StateLogicUpdate();}// 启动输入系统// 注意_playerInput初始化放到awake中,否则会报NullReferenceException(但不影响角色控制?)private void OnEnable(){_playerInput.EnablePlayerAction();}private void OnDisable(){_playerInput.DisablePlayerAction();}//ground detector逻辑也可以抽离成单独类#region ground detectorpublic 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

SMB_511">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_534">四 基于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相机,参数可参考下面:
在这里插入图片描述

SMB_588">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);}}
}

SMB_620">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);}}
}

SMB_671">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);}}

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

SMB_771">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适当大一点,否则角色本身碰撞体会先判定导致角色卡住
在这里插入图片描述

SMB_881">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.ppmy.cn/news/1542666.html

相关文章

怎么提取pdf的某一页?批量提取pdf的某一页的简单方法

怎么提取pdf的某一页&#xff1f;在日常工作与学习中&#xff0c;我们经常会遇到各式各样的PDF文件&#xff0c;它们以其良好的兼容性和稳定性&#xff0c;成为了信息传输和存储的首选格式。然而&#xff0c;在浩瀚的文档海洋中&#xff0c;有时某个PDF文件中的某一页内容尤为重…

贪心算法day(1)

1.将数组和减半的最少操作次数 链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;创建大跟堆将最大的数进行减半 注意点&#xff1a;double t queue.poll()会将queue队列数字减少一个后再除以2&#xff0c;queue.offer(queue.poll(&#xff09;/…

Elasticsearch 解析:倒排索引机制/字段类型/语法/常见问题

Elasticsearch 是一个分布式的开源搜索引擎&#xff0c;广泛用于全文搜索、分析和数据存储。它基于 Apache Lucene 构建&#xff0c;支持 RESTful 风格的 API&#xff0c;使得开发者能够高效地存储和检索数据。本文将详细讲解 Elasticsearch 的基本原理&#xff0c;特别是其倒排…

本地服务器上搭建PPTist轻松实现跨地域的在线PPT制作与演示

文章目录 前言1. 本地安装PPTist2. PPTist 使用介绍3. 安装Cpolar内网穿透4. 配置公网地址5. 配置固定公网地址 前言 本文主要介绍如何在Windows系统环境本地部署开源在线演示文稿应用PPTist&#xff0c;并结合cpolar内网穿透工具实现随时随地远程访问与使用该项目。 PPTist …

member access within null pointer of type ‘ListNode‘

文章目录 前言一、空指针解引用二、访问已释放的内存三、 结构体定义问题四、错误的链表操作五、代码上下文六、示例代码七、调试建议 前言 p -> next p1; p1 p1 -> next; p p->next;runtime error: member access within null pointer of type ListNode如果出现…

js 实现自定义打印模板

1.创建一个print.js文件 //打印拣货单 import { tableStyle } from ./printStyle export const printToTable (data, callBack) > {//初始化打印内容console.log(data)let printNum 1const imgPath data:image/png;base64, data.barCodeconst img new Image()img.src…

Spring Cloud --- Sentinel 授权规则

授权规则概述 在某些场景下&#xff0c;需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用 Sentinel 提供的授权规则来实现&#xff0c;Sentinel 的授权规则能够根据请求的来源判断是否允许本次请求通过。 在 Sentinel 的授权规则中&#xff0c;提供了 白名单…

3GPP协议解读_NTN系列(一)_38.811_非地面网络(NTN)的背景、应用场景和信道建模

非地面网络 1. Scope4. 非地面网络背景介绍4.1 5G中的非地面网络4.2 非地面网络在5G中的用例4.3 卫星和空中接入网的架构4.4 卫星和空中接入网终端的特点4.5 空气/星载飞行器特性4.6 NTN的覆盖模式4.7 NTN网络架构选项4.8 频谱 5. 非地面网络应用场景5.1 应用场景概览5.2 属性介…