Unity-FSM有限状态机
什么是有限状态机?
在编写一些需要判断多个条件的程序时,我们常常会用到 if-else 语句,这样能够很好的帮我们解决多数问题。但在游戏开发过程中,一个角色的行为不是一成不变的,需要实时的进行修改,此时如果我们使用的是 if-else 来判断角色所处状态,就需要修改整个代码体,十分麻烦。而有限状态机很好的解决了这一些问题。
有限状态机实现的方式是,将判断条件、角色状态、状态机分别封装成一个类,这样当我们需要增加或者减少角色的状态时,直接将对应的状态与相对应的条件删除即可,不会影响到所有的代码,极大的减少了开发时间。
下面将介绍有限状态机该如何编写。
有限状态机简单实现
一般的状态机代码,会先有一个抽象的状态父类,其中包含两个抽象函数,一个是需要在进入状态的一开始就执行,一个是需要在程序执行的时候每一帧都执行,并由子类继承这个抽象父类,实现对应的两个函数。切换条件需要写在每一帧都调用的函数中。
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
| public abstract class EnemyBaseState { public abstract void EnterState(Enemy enemy);
public abstract void OnUpdate(Enemy enemy); }
public class PatrolState : EnemyBaseState { public override void EnterState(Enemy enemy) { }
public override void OnUpdate(Enemy enemy) {
if(Mathf.Abs(enemy.transform.position.x - enemy.targetPoint.position.x) < 0.01f) { enemy.TransitionState(enemy.patrolState); }
if(enemy.attackList.Count != 0) { enemy.TransitionState(enemy.attackState); } } }
|
在使用状态机时,就可以只创建抽象父类实例对象,在程序执行的过程中将不同的子类赋值给该对象(里氏转换),并调用其中的两个函数。
当然,在执行各个状态中的函数时,不可避免的需要调用脚本中的参数,所以需要将脚本的引用传递给状态机。
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
| public abstract class Enemy : MonoBehaviour { [Header("状态机相关")] EnemyBaseState currentState; public PatrolState patrolState = new PatrolState(); public AttackState attackState = new AttackState();
protected virtual void Start() { TransitionState(patrolState); } public void TransitionState(EnemyBaseState state) { currentState = state; currentState.EnterState(this); } protected virtual void Update() { currentState.OnUpdate(this); } }
|
将条件写成类的有限状态机
如上所述,我们需要将代码变成三个部分,判断条件、角色状态、状态机,三个部分对应三个基类。这样的写法,需要将在状态类中实例化条件类的对象,从而进行条件的判断。
下面将一一介绍:
判断条件(Trigger)
条件类需要以下几个成员:
- 条件编号:属性成员代替,方便其他类查找条件
- 构造函数:强制要求对属性赋值
- 判断函数:需要传进一个状态机参数,因为条件类本身不自带参数,需要状态机的参数进行条件判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public abstract class Trigger { public TriggerID triggerID{get; set;} public Trigger() { Init(); } public abstract void Init(); public abstract bool HandleTrigger(Base fsmBase); }
|
角色状态(State)
状态类需要以下几个成员:
- 状态编号:用属性代替,方便查找对应的条件
- 条件集合:由于保存各种条件对象
- 字典集合:用于查询条件成立时对应的状态
- 构造函数:强制要求对属性赋值
- 配置字典函数:向字典集合中添加成员
- 判断函数:由状态机调用,判断此时有何条件成立
- 三个基本函数:进入状态、状态执行、退出状态
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
| public abstract class State {
private Dictionary<TriggerID,StateID> map;
public List<Trigger> Triggers;
public StateID stateID { get; set; }
public State() { map = new Dictionary<TriggerID, StateID>(); Init(); }
public abstract void Init();
public void Reason(Base fsmBase) { for(int i=0;i<Triggers.Count;i++) { if(Triggers[i].HandleTrigger(fsmBase)) { fsmBase.ChangeActiveState(map[Triggers[i].triggerID]); return; } } }
public void AddMap(TriggerID triggerID,StateID stateID) { map.Add(triggerID,stateID);
Type type = Type.GetType("FSM."+triggerID+"Trigger"); Trigger trigger = Activator.CreateInstance(type) as Trigger; Triggers.Add(trigger); }
public virtual void EnterState(Base fsmBase){} public virtual void ActionState(Base fsmBase){} public virtual void ExitState(Base fsmBase){} }
|
状态机(Base)
状态机所需成员:
- 默认状态ID:在Unity界面选择
- 状态列表:存储所有状态对象
- 初始化组件函数:初始化对应组件
- 初始化默认状态函数:根据默认状态ID设置默认状态
- 配置状态机函数:向条件列表与条件类中的字典集合中添加对象
- 切换状态函数:由状态类调用,条件满足后切换状态
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
| public class Base : MonoBehaviour { [Header("角色基本组件")] [HideInInspector]public Animator Anim; [HideInInspector]public Rigidbody2D rb; [Tooltip("初始状态")]public StateID defaultStateID; private List<State> states; public State currentState; public State defaultState;
public virtual void Start() { InitCommponent(); SetBase(); InitDefaultState(); }
public void InitCommponent() { Anim = GetComponentInChildren<Animator>(); rb = GetComponentInChildren<Rigidbody2D>(); }
public void InitDefaultState() { defaultState = states.Find(s => s.stateID == defaultStateID); currentState = defaultState; currentState.EnterState(this); }
private void SetBase() { states = new List<State>();
}
public virtual void Update() { currentState.Reason(this); currentState.ActionState(this); }
public void ChangeActiveState(StateID stateID) { currentState.ExitState(this); currentState = stateID == StateID.Default?defaultState:states.Find(s => s.stateID == stateID); currentState.EnterState(this); } }
|