老实说,很久前学习了多种有限状态机写法,有学习打工人小祺的、有跟着鬼鬼鬼ii的,有学习Joker老师的…学得越多反而越来越不知道怎么做。但是分析这次需求,我们决定玩家和敌人都用有限状态机,我觉得Joker老师写得最好。既然Joker老师造好了轮子,我们直接拿来用最方便了。
实践过程
在设计有限状态机前,我们先设置好我们的状态基类StateBase,记录下我们所有要执行的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public abstract class StateBase { //状态机初始化 public virtual void Init(IStateMachineOwner owner) {
}
//卸载资源 public virtual void UnInit() {
}
public virtual void Enter() {
} public virtual void Exit() {
} public virtual void OnUpdate() {
} public virtual void OnLateUpdate() {
} public virtual void OnFixedUpdate() {
} }
|
接下来我们去写我们的有限状态机,我们状态机大概分为两类,一类是敌人状态机,一类是玩家状态机,那就声明一个共同的接口或者抽象类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine;
//状态机基类 public interface StateMachineOwner {
} public class StateMachine { private StateMachineOwner owner; private StateBase currentState; //存储状态 private Dictionary<Type, StateBase> stateDic = new Dictionary<Type, StateBase>(); public void Init(StateMachineOwner owner) { //初始化先保存自己 this.owner = owner; } //记录当前状态
public void ChangeState<T>() where T : StateBase, new() { if (currentState!= null && typeof(T) == currentState.GetType()) { Debug.Log("你切换的状态与当前状态相同"); return; } if(currentState != null) { currentState.OnExit(); //移除方法 MonoManager.MainInstance.RemoveUpdate(currentState.OnUpdate); MonoManager.MainInstance.RemoveFixedUpdate(currentState.OnFixedUpdate); MonoManager.MainInstance.RemoveLateUpdate(currentState.OnLateUpdate); } //切换了状态,并执行一系列函数 currentState = GetState<T>(); if (currentState == null) Debug.Log("currentState为空,没有找到该状态"); currentState.OnEnter(); MonoManager.MainInstance.AddUpdate(currentState.OnUpdate); MonoManager.MainInstance.AddFixedUpdate(currentState.OnFixedUpdate); MonoManager.MainInstance.AddLateUpdate(currentState.OnLateUpdate); }
//获得状态 private StateBase GetState<T>() where T : StateBase, new() { // 如果不包含,就添加进去 if (!stateDic.ContainsKey(typeof(T))) { // 添加状态 stateDic.Add(typeof(T), new T()); // 初始化状态 stateDic[typeof(T)].Init(owner); } return stateDic[typeof(T)]; } //状态机清除 public void ClearFSM() { //执行反初始化卸载函数 currentState.UnInit(); //移出方法 MonoManager.MainInstance.RemoveUpdate(currentState.OnUpdate); MonoManager.MainInstance.RemoveFixedUpdate(currentState.OnFixedUpdate); MonoManager.MainInstance.RemoveLateUpdate(currentState.OnLateUpdate); //清除状态机 stateDic.Clear(); }
}
|
巧用有限状态机制作角色控制器
如何使用我们的有限状态机呢?这里Joker老师采用了类似MVC思想的套路,先写一个角色模型层Player_Model,里面包含我们的动画机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Player_Model : MonoBehaviour { [SerializeField,Header("拖入动画状态机")]private Animator _animator; public Animator Animator =>_animator;
private void Awake() { if (_animator == null) Debug.Log("你忘记拖入Aniamto组件啦"); }
//动画事件注册 }
|
之后写一个角色控制器脚本Player_Controller,我们分析,需要写播放动画的方法、状态转换的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Player_Controller : MonoBehaviour,StateMachineOwner { //拖入角色模型 [SerializeField] private Player_Model player_Model; private StateMachine stateMachine;
//初始化状态机 private void Start() { stateMachine = new StateMachine(); stateMachine.Init(this);
//注册默认状态 stateMachine.ChangeState<Player_IdleState>(); } //提供模型层播放动画的方法 public void PlayAnimation(string animation,float fixedtime = 0.25f) { player_Model.Animator.CrossFadeInFixedTime(animation,fixedtime); }
//提供改变状态的方法,利用枚举 public void ChangeState(PlayerState state) { switch (state) { case(PlayerState.Idle): stateMachine.ChangeState<Player_IdleState>(); break; } } }
|
接下来针对不同状态不同的写,但是我们状态转换多是需要播放动画的,必然需要PlayerAnimaton方法,那就想办法得到我们的Player_Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class PlayerStateBase : StateBase { //所有状态都需要先得到我们的控制器 protected Player_Controller player_Controller;
//重写初始化方法 public override void Init(IStateMachineOwner owner) { base.Init(owner); player_Controller = (Player_Controller)owner; }
}
//进入时默认播放待机动画 public class Player_IdleState : PlayerStateBase { public override void Enter() { //播放角色待机动画 player_Controller.PlayAnimation("Idle"); } public override void Update() { base.Update(); //检测攻击
//检测跳跃
//检测玩家移动 } }
|
PlayerState脚本部分:
1 2 3 4 5 6 7 8 9
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public enum PlayerState { Idle, Move, }
|
解释部分?
这个状态机的组成部分,最少共调用了六个脚本,StateBase、Player_Controller、PlayerState、PlayerStateBase、Player_Model、StateMachine,最初我是看得一愣一愣。这里稍微解释一下,不想看解释、想要直接上手用可以往下看、跳过。
首先StateBase是所有状态的基类,是我们每个状态都会用的方法。Init是初始化方法,传入我们的状态机、进行初始化,UnInit是卸载资源用的方法。
StateMachine是我们的状态机主逻辑脚本,因为分有敌人状态机、玩家状态机,所以再写一个接口,两种状态机继承该接口。
PlayerState是把状态封装成枚举类型。
PlayerStateBase是我们玩家的状态基类,我们所有玩家的状态都要继承它。由于我们的每个状态都要用到角色控制器,角色控制器又是状态机的子类,所以要引入控制器。Init方法是在StateMachine的GetState里调用的,每注册一个状态都会初始化一次。
Player_Controller是我们的玩家状态机,主要通过模型层和状态机处理逻辑。
Player_IdleState是写的一个示例状态脚本,Player_Model是我们的模型层上文件,拖给角色模型。
如何使用?
首先我们打开PlayerState,在枚举中添加状态。
接着我们创建新的状态脚本,继承PlayerStateBase,书写对应方法。通过player_Controller.ChangeState(PlayerState.枚举状态)来转换状态。
最后打开Player_Controller,在ChangeState方法里写新的case,stateMachine.ChangeState<状态脚本文件>();
1 2 3
| case(PlayerState.枚举状态): stateMachine.ChangeState<状态脚本>(); break;
|