上来可能有疑问
什么是状态机?能干什么?
第一次听说状态机 是从游戏开发技术中得来的。在游戏开发中,可以使用状态机控制npc的行为,控制角色在 站立,追踪敌人,攻击敌人,逃跑等状态进行切换。每一个状态都会执行该状态进行时执行的一系列操作。
简而言之,就是控制对象状态的切换。
用图示来表示一下
这是一个游戏demo,注意敌人的各种行为
敌人没有发现玩家时,呆立不动,这时,敌人可以说是处于 “空闲” 的状态
当敌人发现玩家,会向玩家靠近,这时,可以说敌人处于 “追击” 状态
当敌人追上玩家,就会对玩家进行攻击,这时,可以说敌人处于 “攻击” 状态。
当然 玩家不是吃素的,当玩家反击并击败敌人,这时 敌人说敌人处于 “死亡” 状态。
当然 ,玩家可以不选择击败敌人,也可以战斗中逃离,当玩家逃出一定视野范围,npc无法看到玩家,则npc 又可以回到 “空闲” 状态。
注意到,NPC在某个状态中,还需要进行各种行为,例如 “空闲”状态时,会不停的 东张西望 。 “追击” 状态时,会朝向敌人奔跑。“攻击”状态时,会向玩家挥动武器。 “死亡”状态时 会倒地不起。
以上的这些行为是文字描述的,是游戏策划设计的。
作为程序员,是有职责将上述的设计转换成代码实现的。
那么,代码将如何实现这些设计呢?可能有人会想,那么我就设计一个控制类,里面有许多控制变量,通过判断控制变量来进行控制,例如下面伪代码
if(distance(player,npc)<SEE_DISTANCD){
npc.move(player)
}
if(distance(player,npc)<ATTACK_DISTANCD){
mpc.attack(player)
}
if(npc.health<=0){
mpc.death()
}
if(distance(player,npc)>=SEE_DISTANCD)){
npc.idle();
}
...
通过一定数量的if-else来控制这些行为。然鹅,实际情况下这种做法是不可行的,因为实际的状态转换条件有很多种,必须进行if-else嵌套才能完成所有的逻辑,如果写全这些逻辑的话,代码是不可维护的(不必怀疑,自己试一下便知)。
这次的主角要登场了 这就是有限状态机(finite state machine)简称为FSM。
在介绍状态机之前,我们要用一个图示来表示上述游戏中的NPC的各种状态之间的关系。
图中,蓝色框代表有限的所有的状态,绿色有向箭头代表状态间可以进行切换的条件 ,整个图示叫做状态图
这个图非常重要。在编写状态机代码前,必须先画出这个状态图。
状态机 可以理解为一台不停工作的机器,运行一段循环执行的代码,来控制状态间的切换和处于每个状态期间需要执行的操作
一个状态机的接口是这样的
interface IFSM{
update();
addState(State state);
changeState(int stateID);
setupFSM();
}
其中 update()是系统提供的更新机制(例如Unity中的 update()方法,Android 中的 Handle.postDelay())
每当达到更新时机时,通知状态机更新当前状态,也即是调用状态机的update()。
更新当前状态包括 检查当前状态是否满足条件,查询另一个满足条件的状态,切换当前状态到应该到达的状态,每当update()时,就完成了状态图的一个蓝框到另一个蓝框的过程,当然状态没有变化则继续处于当前的蓝框状态。
addState()提供了一个交由状态机管理的所有状态的添加功能,状态机本质上是管理了所有的状态,并把某一个设置为当前状态。
再回到代码层面
实现状态机,还有两个类必不可少
状态类
代表状态图中的蓝框,职责是处于某种状态时,执行状态中的操作,比如 攻击玩家 或者 寻找玩家位置,向玩家前进。
一个状态类的接口是这样的
interface IState{
void check(IFSM fsm);
void onEnter(IFSM fsm);
void onAction(IFSM fsm);
void onExit(IFSM fsm);
}
其中,
onEnter()提供了一个刚进入状态中执行的一次性操作 比如 开始循环播放空闲动画。
onAction()提供了一个处于状态中需要持续进行的操作 比如 攻击玩家。
onExit()提供了一个状态转换时本状态退出时需要进行的操作 比如 停止攻击。
check()提供了当当前状态已经不满足条件时,通过满足条件的条件编号找到本状态可以切换的下一个条件,来交给状态机设置为当前状态。
函数参数中都有一个状态机的引用,是方便再各种操作时,能方便访问到状态机管理的外部引用(例如player 玩家,npc 等)
以上是状态类,接下来是最后一个关键类
条件类条件类代表了状态图中的的绿色箭头,职责是判断对应的状态是否满足,比如追击这个条件是否满足,如果满足则返回true。
条件类的接口如下所示
interface ITrigger{
boolean checkTrigger(IFSM fsm);
}
我们把条件类命名为Trigger 因为条件如果满足的话,说明会触发一次条件的转换。所以相当于条件转换是由条件类触发的,所以叫Trigger.
checkTrigger()提供了一个判断某个状态对应的条件是否满足的依据,例如攻击状态对应的条件攻击条件中,checkTrigger()应该如下所示
AttackTrigger implements ITrigger{
@Override
boolean checkTrigger(IFSM fsm){
return distance(fsm.getPlayer(),fsm.getNpc())<=ATTACK_DISTANCE;
}
}
这里,大家看到了最初庞大的的if-else语句中的某些代码了。这里体现了状态机机制相比大块if-else逻辑的一个优点,就是单一职责原则,每个条件通过一个类来负责,各条件之负责本条件对应的判断。
状态类的方法中也有一个状态机参数,同样是为了方便访问到公用的数据(这里是游戏对象player及npc)。
让我们把 状态机类,条件类,状态类整合起来,让整个系统运行起来吧。
到这里,相信大部分人还是不懂状态机是怎么运作的,能控制NPC做出上面的反应。这就需要将上述的重要的类整合起来了。
总体来说,上述的类放在一起是这样的
各类之间是包含的关系。
首先 状态机有一个状态列表,保存了管理的所有的有限的状态。
状态机还负责切换状态,切换状态的意思是 把某一个新状态设为当前状态。
每一个状态类都管理了一个条件类列表,代表该状态能够到达的其他状态应该满足的条件。
还包括了一个条件-状态映射 一般使用Map来表示。映射是当某一个条件满足