有限状态机机制介绍及在Android中应用的实例

上来可能有疑问
什么是状态机?能干什么?

第一次听说状态机 是从游戏开发技术中得来的。在游戏开发中,可以使用状态机控制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接收这个事件,从而做出界面的响应。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章