有限状态机实现方式不止一种,这里标题为第三版,不是说前面几版不好,而是又提供了一种新的有限状态机写法。
我之前对有限状态机的想法是,用来做敌人AI,现在再次开阔视野,可以用代码的形式来替代Animator窗口复杂的连线(之前也有人推荐我用Animancer插件,等我学到了我再学),简洁清晰地转换状态。
这个思路主要借鉴自Joker老师的RPG进阶教程,有能力的话大家可以去支持一下Joker老师。
思路分析
再开始写代码前,我们需要先分析自己的需求:
首先是状态机,大概分为敌人状态机、玩家状态机,那么就声明个基类状态机最好。状态机怎么写?首先我们需要一个字典容器来存储所有的状态,然后我们需要一个转换状态的方法,然后我们还需要存储下当前状态,就写一个GetState的方法。
状态怎么写?首先我们有个状态基类StateBase,记录所有状态都要有的方法;再分别写PlayerStateBase和EnemyStateBase继承StateBase,最后我们具体的状态再次分门别类的继承,这样就很有条理了。
实践过程
在设计有限状态机前,我们先设置好我们的状态基类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 Update() {
} public virtual void LateUpdate() {
} public virtual void FixedUpdate() {
} }
|
接下来我们去写我们的有限状态机,我们状态机大概分为两类,一类是敌人状态机,一类是玩家状态机,那就声明一个共同的接口或者抽象类。
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 77 78 79 80 81
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine;
//首先我们状态及可能分为敌人状态机、玩家状态机,我们声明个基类、接口、宿主 public interface IStateMachineOwner { } //这里我采用不继承MonoBehaviour的方式 public class StateMachine { //存储有限状态机 private IStateMachineOwner _owner; public void Init(IStateMachineOwner owner) { //保存我们的状态机 _owner = owner; }
//我们用Type进行状态存储 private Dictionary<Type,StateBase> StateDic = new Dictionary<Type,StateBase>(); //当前状态 private StateBase currentState;
public void ChangeState<T>()where T : StateBase, new() {
//执行完退出函数再卸载所有方法 if (currentState != null) {
//如果改变状态与当前状态重复就return; if (typeof(T) == currentState.GetType()) return; currentState.Exit(); //Mono管理器启动! MonoManager.GetInstance().RemoveUpdate(currentState.Update); MonoManager.GetInstance().RemoveFixedUpdate(currentState.FixedUpdate); MonoManager.GetInstance().RemoveLateUpdate(currentState.LateUpdate); } currentState = GetState<T>(); currentState.Enter(); MonoManager.GetInstance().AddUpdate(currentState.Update); MonoManager.GetInstance().AddFixedUpdate(currentState.FixedUpdate); MonoManager.GetInstance().AddLateUpdate(currentState.LateUpdate); }
//这样写的好处是不用在一开始就注册很多状态了 private StateBase GetState<T>() where T : StateBase,new() {
//先检查字典有没有这个状态 if(!StateDic.ContainsKey(typeof(T))) { StateDic.Add(typeof(T), new T());
//早在注册状态时我们就执行Init方法 StateDic[typeof(T)].Init(_owner); } return StateDic[typeof(T)]; }
//停止状态机 public void Stop() { if (currentState != null) { currentState.Exit(); MonoManager.GetInstance().RemoveUpdate(currentState.Update); MonoManager.GetInstance().RemoveFixedUpdate(currentState.FixedUpdate); MonoManager.GetInstance().RemoveLateUpdate(currentState.LateUpdate); } foreach (var items in StateDic.Values) { items.UnInit(); } StateDic.Clear(); } }
|
巧用有限状态机制作角色控制器
如何使用我们的有限状态机呢?这里Joker老师采用了类似MVC思想的套路,先写一个角色模型层Player_Model,里面包含我们的动画机。
1 2 3 4 5 6 7 8 9 10
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Player_Model : MonoBehaviour { [SerializeField,Header("拖入动画状态机")]private Animator _animator; public Animator Animator =>_animator; }
|
之后写一个角色控制器脚本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
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Player_Controller : MonoBehaviour,IStateMachineOwner { [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
| 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(); //检测攻击
//检测跳跃
//检测玩家移动 } }
|
最后捋一捋我们状态机运行的流程:开始执行Player_Controller的Start方法注册状态——调用了StateMachine里的ChangeState方法——执行了GetState方法——执行了PlayerStateBase的Init方法得到了Player_Contorrler脚本——执行了Enter方法——调用了Player_Contoller的PlayerAnimation方法和ChangeState方法——播放动画和状态转换。