上来可能有疑问
什么是状态机?能干什么?
第一次听说状态机 是从游戏开发技术中得来的。在游戏开发中,可以使用状态机控制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来表示。映射是当某一个条件满足时,从该映射中取出对应的状态,作为准备切换到的状态,交由状态机切换。
接下来来实现每个类
首先是状态机
public class BasicFSM implements IFSM {
List<IState> states = new ArrayList<>();
IState currentState;
public IState getCurrentState() {
return currentState;
}
public void setCurrentState(IState currentState) {
this.currentState = currentState;
}
@Override
public void changeState(int stateId) {
for (IState state:states) {
if(state.getId()==stateId){
if(currentState!=null){
currentState.onExit(this);
}
currentState = state;
currentState.onEnter(this);
break;
}
}
}
@Override
public void addState(IState state) {
states.add(state);
}
@Override
public void update() {
currentState.check(this);
currentState.onAction(this);
}
@Override
public void setupFSM() {
}
}
状态机的实现,包含了两个成员变量,一个是currentState 标记当前处于的状态,另一个是所有状态列表。当切换状态时,从列表中找到对应的状态设置为当前状态。
接下来是条件类(具体的条件类,需要继承该基类,来做条件判断)
public class BasicTrigger implements ITrigger {
int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public BasicTrigger(int id) {
this.id = id;
}
@Override
public boolean checkTrigger(IFSM fsm) {
return false;
}
}
条件类有一个id成员变量,来标记条件的类型,条件构造的时候需要指定。
然后是状态类基类的实现(具体的状态类,需要继承该基类,来做状态处理逻辑)
public class BasicState implements IState {
Map<Integer,Integer> stateTriggerMap = new HashMap<>();
List<ITrigger> triggers = new ArrayList<>();
int id;
public int getId() {
return id;
}
public BasicState(int id) {
this.id = id;
}
@Override
public void onAction(IFSM fsm) {
}
@Override
public void onEnter(IFSM fsm) {
}
@Override
public void onExit(IFSM fsm) {
}
@Override
public void addTrigger(ITrigger trigger) {
triggers.add(trigger);
}
@Override
public void addMap(int triggerId, int stateId) {
stateTriggerMap.put(triggerId,stateId);
}
@Override
public void check(IFSM fsm) {
for (ITrigger trigger:triggers) {
if(trigger.checkTrigger(fsm)){
int nextStateId = stateTriggerMap.get(trigger.getId());
fsm.changeState(nextStateId);
break;
}
}
}
}
状态类的成员变量triggers 保存了当前条件可以切换到的所有其他状态对应的条件,在状态机每次判断时会调用check()当前是否有其他条件满足,需要切换到其他状态。每次从条件列表中对每个条件进行判断,如果某个条件判断结果为true,则根据满足的条件id从保存的条件–状态映射stateTriggerMap中找到对应的状态的状态id,告诉状态机需要切换到新的状态了。
这时,状态机主题框架都有了,应用到实际因业务逻辑,还需要一些辅助。
首先是标明条件和状态类型的id值。这里其实可以使用枚举作为ID,但用Integer作为id 有一些好处,比如说多个状态机的情况下可以继承同一个基类。当然这里没那么复杂。
我们创建一个类保存状态id常量值
public class StateID {
public static int STATE_IDLE = 0;//空闲
public static int STATE_PERSUIT = 1;//追逐
public static int STATE_ATTACK = 2;//攻击
public static int STATE_DIE = 3;//死亡
}
还有一个保存条件id常量值
public class TriggerID {
public static int STATE_IDLE = 0;//空闲
public static int STATE_PERSUIT = 1;//追逐
public static int STATE_ATTACK = 2;//攻击
public static int STATE_DIE = 3;//死亡
}
有了id值,我们就可以创建具体的条件对象和状态对象了。以攻击作为例子
public class AttackState extends BasicState {
public AttackState(int id) {
super(id);
}
@Override
public void onAction(IFSM fsm) {
NPCControlFSM npcControlFSM = (NPCControlFSM) fsm;
npcControlFSM.getNpc().attack();
}
}
这里,处于攻击状态的时候,需要不停的攻击 所以 我们只复写了 onAction()方法,来处理处于该状态中的操作。就是不停的砍啊砍。
接下来实现具体的攻击条件类,也就是满足什么条件时,要进入攻击状态,也以它作为具体条件类来举个例子
public class AttackTrigger extends BasicTrigger {
public AttackTrigger(int id) {
super(id);
}
@Override
public boolean checkTrigger(IFSM fsm) {
NPCControlFSM npcControlFSM = (NPCControlFSM) fsm;
return npcControlFSM.
calculateDistance(npcControlFSM.getPlayer(),
npcControlFSM.getNpc())>NPCControlFSM.ATTACK_DISTANCE;
}
}
这里最重要的是 重写checkTrigger()方法,具体处理满足攻击状态切换的条件是什么。这里引用了状态机的公用方法做了逻辑判断。
现在可能发现了,在具体的条件类状态类中,接受的状态机引用被转换成了一另一个类型的状态机实例。是因为之前的状态机也是基础类,具体业务还要创建继承自基础状态机的状态机来使用。
进一步说明一下,这个状态机框架是一个通用的框架,不仅可以用于游戏中,你可以将它变成C#脚本放到Unity引擎中使用,也可以在Android应用开发中使用,比如在Android中可以用它来操作一些复杂逻辑的动画切换,或者带有状态切换性质的功能,比如视频播放器的控制。视频播放器的 暂停,停止,播放状态也可以使用这个框架来控制。这时,具体的状态机类就是MediaPlayerControlFSM了。
我们来看下NPCControlFSM的实现
public class NPCControlFSM extends BasicFSM {
public static final int ATTACK_DISTANCE = 200;
public Player getPlayer() {
return player;
}
public void setPlayer(Player player) {
this.player = player;
}
public Npc getNpc() {
return npc;
}
public void setNpc(Npc npc) {
this.npc = npc;
}
private Player player;
private Npc npc;
public int calculateDistance(Player player,Npc npc){
int distance = 256;//举个例子,这里根据游戏进行改写
return distance;
}
@Override
public void setupFSM() {
}
}
可以看到,具体功能的状态机,往往引用了一些业务逻辑相关的成员,可以通过状态机传给具体状态,条件类来进行业务逻辑判断,或者执行某种状态相关的操作。
具体状态有了,状态机也有了,最后就是给告诉状态机,需要负责管理哪些状态,最终才能让状态机自动地切换状态。
我们需要在具体状态机里增加一个配置状态机的方法 setupFSM();
下面是增加了配置的NPCControlFSM 类实现。
public class NPCControlFSM extends BasicFSM {
public static final int ATTACK_DISTANCE = 200;
/***
* 配置状态机需要增加状态机管理的所有
* 状态类,在生成每个状态类的同时
* 配置每个状态类的条件状态映射
*/
public void setupFSM(){
IdleState idleState = new IdleState(StateID.STATE_IDLE);//创建空闲状态类
PursuitTrigger pursuitTrigger = new PursuitTrigger(TriggerID.STATE_PURSUIT);
//空闲状态有切换到追逐状态的路线,需要在在空闲状态内部添加追逐切换条件
idleState.addTrigger(pursuitTrigger);
//空闲状态有切换到追逐状态的路线,需要在在空闲状态内部注册追逐状态映射
idleState.addMap(TriggerID.STATE_PURSUIT,StateID.STATE_PURSUIT);
//将空闲状态添加至状态机管理
addState(idleState);
//创建追逐状态,配置追逐状态能够切换到的其他条件
PursuitState pursuitState = new PursuitState(StateID.STATE_PURSUIT);
AttackTrigger attackTrigger = new AttackTrigger(TriggerID.STATE_ATTACK);
pursuitState.addTrigger(attackTrigger);
pursuitState.addMap(TriggerID.STATE_ATTACK,StateID.STATE_ATTACK);
IdleTrigger idleTrigger = new IdleTrigger(TriggerID.STATE_DIE);
pursuitState.addTrigger(idleTrigger);
pursuitState.addMap(TriggerID.STATE_IDLE,StateID.STATE_DIE);
addState(pursuitState);
//创建攻击状态,配置攻击状态能够切换到的其他条件
AttackState attackState = new AttackState(StateID.STATE_ATTACK);
DieTrigger dieTrigger = new DieTrigger(TriggerID.STATE_DIE);
attackState.addTrigger(dieTrigger);
attackState.addMap(TriggerID.STATE_DIE,StateID.STATE_DIE);
attackState.addTrigger(pursuitTrigger);
attackState.addMap(TriggerID.STATE_PURSUIT,StateID.STATE_PURSUIT);
addState(attackState);
//创死亡击状态,配置死亡状态能够切换到的其他条件 死亡状态不能切换到其他状态所以没有addTrigger和addMap
DieState dieState = new DieState(StateID.STATE_DIE);
addState(dieState);
//指定默认状态
setCurrentState(idleState);
}
public Player getPlayer() {
return player;
}
public void setPlayer(Player player) {
this.player = player;
}
public Npc getNpc() {
return npc;
}
public void setNpc(Npc npc) {
this.npc = npc;
}
private Player player;
private Npc npc;
public int calculateDistance(Player player,Npc npc){
int distance = 256;//举个例子,这里根据游戏进行改写
return distance;
}
}
在setupFSM() 设置了该状态机需要管理的状态,这里增加了:空闲状态,追逐状态,攻击状态,死亡状态。并且在末尾调用setCurrentState()将空闲状态当作当前状态。这样可以在状态机配置好时有一个默认的状态。在添加条件和状态的过程中,务必对照之前设计的状态图来添加,再次强调状态图的重要性。
到这里。具有控制npc自动对玩家做出反应的状态机已经实现,剩下要做的就是在系统中创建该状态机,并调用setupFSM()配置状态机,并在框架(游戏引擎提供的update机制)定时执行的更新方法中调用状态机的update(),状态机就能
进行NPC实时的行为更新了。
说了半天,跟Android如何结合呢?
这就需要利用AndroidSDK自带的机制去开启一个定时执行的方法来触发状态机框架的update()更新实时状态了。这里可以选择Handler来做这个事情。
public class FSMHelper {
private IFSM ifsm;
private Handler handler = new Handler();
public FSMHelper(IFSM ifsm) {
ifsm.setupFSM();//构造是,让状态机配置好
this.ifsm = ifsm;
}
public void start(){//让状态机运转起来,没0.2秒进行更新
Runnable runnable = new Runnable() {
@Override
public void run() {
ifsm.update();
handler.postDelayed(this, 200);//控制状态机0.2秒更新状态
}
};
handler.post(runnable);
}
}
上面的FSMHelper可以让状态机运转起来,使用Android的handler机制,可以让它在指定的时间间隔内更新一次状态机。这里每0.2秒更新一次(这个频率很低了其实,你可以自行调节 可以参考在游戏里一般0.01666秒(60FPS)甚至更少更新一次)。
在Android中,使用这个工具类FSMHelper 。可以在MainActivity 的onCreate时,新建一个状态机实例,并创建一个FSMHelper 实例传入状态机实例。并调用start()方法,就可以启动一个状态机了。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
NPCControlFSM npcControlFSM = new NPCControlFSM();
FSMHelper helper = new FSMHelper(npcControlFSM);
helper.start();
setContentView(R.layout.activity_main);
}
}
至此,Android应用系统就有一个定时更新的状态机系统在运行了,配置具体的状态,条件类,就可以控制需要实时改变状态的具体业务了,不管是媒体播放器播放,还是某个页面动画的切换。具体实现请自行探索。总之,如果涉及到时间相关的多个控制参数的自动切换状态,可以考虑使用这个机制来实现。
说个题外话,可能会想,Android中状态执行的onAction()时肯定要跟某个界面l例如Activity发生联系啊,当前的状态机并么有什么与Activity相关连的地方。这里推荐EventBus来实现。
当某个状态处于进入,执行,退出时,需要Activity有所响应,可以在状态的执行方法中post一个事件,Activity接收这个事件,从而做出界面的响应。