目录
- 一 内容摘要
- 二 前言
- 三 状态模式的必要性
- 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,比如:
- editor频繁hold on报rider相关的东西
- 添加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.Translate
为Rigidbody.MoveRotation
和Rigidbody.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插件等方式)
-
角色本身抖动,分动画抖动和移动抖动
-
动画抖动:将动画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);
-
-
镜头抖动
- 如果使用了cinemachine 插件,那可能就是与
RigidBody.Interpolation
兼容问题。cinemachine brain update method为fixedupdate/smart 都可以(lateupdate背景依然抖),但RigidBody.Interpolation
一定要none。
- 如果使用了cinemachine 插件,那可能就是与
最终丝滑效果:
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~