有限状态机

有限状态机

有限状态机(Finite State Machine) 缩写为 FSM。以下简称为状态机。
状态机有 3 个组成部分:状态、事件、动作。

  • 状态:所有可能存在的状态。包括当前状态和条件满足后要迁移的状态。
  • 事件:也称为转移条件,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是* 必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。

状态机的表示

状态转移图

我们举个例子,马里奥共有三种状态,可执行三个动作,如图中所示。
从图中可以看到,

  1. 在Small Mario状态下,A3,没有触发状态转移,也没有触发任何动作。
  2. 在Fire Mario状态下,A3,没有触发状态转移,触发了发射火球动作。
  3. 在Super Mario状态下,A2,触发状态转移到Small Mario,没有触发任何动作。
  4. 在Small Mario状态下,A1,触发状态转移到Super Mario,触发增加1000积分。
    可见:动作的执行,和状态的转移,是相对独立的。并不是必须有状态转移,或必须触发动作。状态转移和动作之间也没有必然联系。
    在这里插入图片描述

二维表

第一维表示当前状态,第二维表示事件,表格内容表示当前状态经过事件之后,转移到的新状态 或 要执行的动作,理论上状态转移和执行的动作可以是两个独立的表格。

合并后的:

吃蘑菇碰到怪物攻击
Small MarioSuper Mario/+1000Dead/--/-
Super MarioFire Mario/+1000Small Mario/--/-
Fire Mario-/+1000Small Mario/--/发射火球
Dead Mario-/--/--/-

状态转移表:

吃蘑菇碰到怪物攻击
Small MarioSuper MarioDead-
Super MarioFire MarioSmall Mario-
Fire Mario-Small Mario-
Dead Mario---

动作表:

吃蘑菇碰到怪物攻击
Small Mario+1000--
Super Mario+1000--
Fire Mario+1000-发射火球
Dead Mario---

实现

定义State枚举,定义四种状态。
定义三个函数,对应三个事件。我们首先给出简单的测试程序。三个函数有待实现。

typedef enum State{
	DEAD = 0,
    SMALL,
    SUPER,
    FIRE
}State;
    
class Mario{
private:
    State   state;  //状态
    int     score;  //积分   
public:
    Mario():state(SMALL), score(0){}
    //吃蘑菇
    void eatMushRooms(){
        //todo something
    }   
    
    //碰到怪物
    void contactMonsters(){
        //todo something
    }   
    
    //攻击
    void attack(){
        //todo something
    }   
};

int main(int argc, char *argv[]){
    Mario m;
    m.eatMushRooms();//吃蘑菇
    m.eatMushRooms();//吃蘑菇
    m.eatMushRooms();//吃蘑菇
    m.attack();//发射火球
    m.contactMonsters();//变小
    m.attack();//nothing
    m.contactMonsters();//died
    return 0;
}

穷举法

参照状态转移图,代码直译的方式,用if-else或switch-case,简单堆砌完成。穷举所有状态下发声任何事件的所有可能。

class Mario{
private:
        State   state;  //状态
        int     score;  //积分
    void printState(){
        switch(state){
            case DEAD:
                cout << "died!" << endl;
                break;
            case SMALL:
                cout << "small!" << endl;
                break;
            case SUPER:
                cout << "super!" << endl;
                break;
            case FIRE:
                cout << "fire!" << endl;
                break;
            default:

                break;
        }
        cout << "score is " << score << endl;
    }

public:
        Mario():state(SMALL), score(0){}
        //吃蘑菇
        void eatMushRooms(){
        cout << "eatMushRooms begin:" << endl;
        switch(state){
            case DEAD:
                break;
            case SMALL:
                state = SUPER;
                score += 1000;
                printState();
                break;
            case SUPER:
                state = FIRE;
                score += 1000;
                printState();
                break;
            case FIRE:
                score += 1000;
                printState();
                break;
            default:
                
                break;
        }
        cout << endl;
        }
        
        //碰到怪物
        void contactMonsters(){
        cout << "contactMonsters begin:" << endl;
        switch(state){
            case DEAD:
                break;
            case SMALL:
                state = DEAD;
                printState();
                break;
            case SUPER:
                state = SMALL;
                printState();
                break;
            case FIRE:
                state = SMALL;
                printState();
                break;
            default:
                
                break;
            }
        cout << endl;
    }
        //攻击
        void attack(){
        cout << "attack begin:" << endl;
        switch(state){
            case DEAD:
                cout << "do nothing!" << endl;
                break;
            case SMALL:
                cout << "do nothing!" << endl;
                break;
            case SUPER:
                cout << "do nothing!" << endl;
                break;
            case FIRE:
                cout << "shoot firebool!" << endl;
                break;
            default:
                
                break;
            }
        cout << endl;
    }
};

eatMushRooms begin:
super!
score is 1000

eatMushRooms begin:
fire!
score is 2000

eatMushRooms begin:
fire!
score is 3000

attack begin:
shoot firebool!

contactMonsters begin:
small!
score is 3000

attack begin:
do nothing!

contactMonsters begin:
died!
score is 3000

对于简单的分支逻辑,这种方式是可以接受的。对于复杂的状态机来说,容易漏写或者错写状态转移
目前四种状态,三种事件,代码中就已经充斥着大量的case语句。粗略估计要处理的分支数量大概是3*4。如果是10种状态,10种事件呢,就需要有100个分支需要处理,可读性和可维护性就会变得很差,修改也更容易出错

查表法

结合二维表表示,查表法的代码更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 状态转移表 和 动作表 即可。
而且我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。

需要注意的是,C++中,我们使用状态枚举编号和动作编号来定位数组对应的值。

State transitionTable[4][3] = {
    /*A1	A2		A3*/
    {DEAD,  DEAD,   DEAD},	//DEAD
    {SUPER, DEAD,   SMALL},	//SMALL
    {FIRE,  SMALL,  SUPER},	//SUPER
    {FIRE,  SMALL,  FIRE}	//FIRE
};

int actionTable[4][3] = {
	/*A1	A2	A3*/
    {0,     0,  0},		//DEAD
    {1000,  0,  0},		//SMALL
    {1000,  0,  0},		//SUPER
    {1000,  0,  0}		//FIRE
};

bool actionFireboolTable[4][3] = {
	/*A1	A2		A3*/
    {false, false,  false},	//DEAD
    {false, false,  false},	//SMALL
    {false, false,  false},	//SUPER
    {false, false,  true},	//FIRE
};

class Mario{
private:
    State   state;  //状态
    int     score;  //积分
    void printState(){
        switch(state){
            case DEAD:
                cout << "died!" << endl;
                break;
            case SMALL:
                cout << "small!" << endl;
                break;
            case SUPER:
                cout << "super!" << endl;
                break;
            case FIRE:
                cout << "fire!" << endl;
                break;
            default:

                break;
        }
        cout << "score is " << score << endl;
        cout << endl;
    }


    void action(int index){
        if(index == 2){
            if(actionFireboolTable[state][index]){
                cout << "shoot firebool!" << endl;
            }else{
                cout << "do nothing!" << endl;
            }
        }
        score += actionTable[state][index];
    }


public:
    Mario():state(SMALL), score(0){

    }   
    //吃蘑菇
    void eatMushRooms(){
        cout << "eatMushRooms begin:" << endl;
        action(0);
        state = transitionTable[state][0];
        printState();
    }   
        
    //碰到怪物
    void contactMonsters(){
        cout << "contactMonsters begin:" << endl;
        action(1);
        state = transitionTable[state][1];
        printState();
    }   
    //攻击                                                                                          
    void attack(){
        cout << "attack begin:" << endl;
        action(2);
        state = transitionTable[state][2];
        printState();
    }   
};
eatMushRooms begin:
super!
score is 1000

eatMushRooms begin:
fire!
score is 2000

eatMushRooms begin:
fire!
score is 3000

attack begin:
shoot firebool!
fire!
score is 3000

contactMonsters begin:
small!
score is 3000

attack begin:
do nothing!
small!
score is 3000

contactMonsters begin:
died!
score is 3000

查表法明显在函数实现部分比较简洁。数组表格和状态、动作的对应顺序,需要对照二维表完成。如本例中,如果是简单的积分加减,不需要引入actionFireboolTable,代码就会更加简洁。对于更复杂的动作,我们可能要引入更多地表格和判断。这里就是查表法的局限,查表法仅能处理简单的 动作表
当各个动作需要执行很多差异化很明显的代码(代码间难有共性,无法提炼)时。表格法的 动作表或者动作部分 就会变得复杂。

状态模式

状态模式通过将事件触发的状态转移和动作执行逻辑,分别放到不同的状态类中。转移到何种状态和执行何种动作,统一交个具体的状态类来处理。

定义状态类接口:

#include <string>

//所有状态类的接口
class IMarioState{
public:
    IMarioState(){}
    virtual ~IMarioState(){}

    //输出状态名称
    virtual std::string getName() = 0;

    //吃蘑菇
    virtual void eatMushRooms() = 0;
    
    //碰到怪物
    virtual void contactMonsters() = 0;

    //攻击
    virtual void attack() = 0;
};

状态机(Mario)依赖状态类接口(IMarioState)。

//mario.h

//Mario状态机,状态机将具体的事件响应交给IMarioState来完成
class Mario{
        IMarioState     *state;  //状态
        int             score;  //积分
public:
    Mario();
    ~Mario();
    void setCurrentState(IMarioState *st);

    void addScore(int s);

    void shootFirebool();

    void printState();

        //吃蘑菇
    void eatMushRooms();
        
        //碰到怪物
    void contactMonsters();

        //攻击
    void attack();


};

//mario.cpp

Mario::Mario():state(NULL), score(0){
    state = new SmallMario(this);
}
Mario::~Mario(){
    if(this->state != NULL)
      delete this->state;
}
void Mario::setCurrentState(IMarioState *st){
    if(this->state != NULL)
      delete this->state;
    this->state = st;
}

void Mario::addScore(int s){
    score += s;
}

void Mario::shootFirebool(){
    cout << "shoot firebool!" << endl;
}

void Mario::printState(){
    cout << state->getName() << endl;
    cout << "score is " << score << endl;
    cout << endl;
}

//吃蘑菇
void Mario::eatMushRooms(){
    cout << "eatMushRooms begin:" << endl;
    state->eatMushRooms();
    printState();
}

//碰到怪物
void Mario::contactMonsters(){
    cout << "contactMonsters begin:" << endl;
    state->contactMonsters();
    printState();
}

//攻击
void Mario::attack(){
    cout << "attack begin:" << endl;
    state->attack();
    printState();
}

具体的状态类(SmallMario、SurperMario、FireMario、DeadMario)同样依赖状态机(Mario),具体状态类的某些行动,需要通过调用Mario来实现。以SmallMario为例。

//smallMario.h

class SmallMario: public IMarioState{
private:
    Mario *mario;

public:

    SmallMario(Mario* mario);

    virtual std::string getName() override;

    virtual void eatMushRooms() override;
    
    virtual void contactMonsters() override;

    virtual void attack() override;
};

//smallMario.cpp

#include "smallMario.h"
#include "superMario.h"
#include "deadMario.h"

SmallMario::SmallMario(Mario* mario):mario(mario){}

std::string SmallMario::getName()   {
    return "small!";
}

void SmallMario::eatMushRooms()  {
    mario->setCurrentState(new SuperMario(mario));
    mario->addScore(1000);
}
        
void SmallMario::contactMonsters()  {
    mario->setCurrentState(new DeadMario(mario));
}

void SmallMario::attack()  {
}

测试代码:
```c++
#include <iostream>
#include "mario.h"

int main(int argc, char *argv[]){
    Mario m;
    m.eatMushRooms();//吃蘑菇
    m.eatMushRooms();//吃蘑菇
    m.eatMushRooms();//吃蘑菇
    m.attack();//发射火球
    m.contactMonsters();//变小
    m.attack();//nothing
    m.contactMonsters();//died
    return 0;
}

结果:

eatMushRooms begin:
super!
score is 1000

eatMushRooms begin:
fire!
score is 2000

eatMushRooms begin:
fire!
score is 3000

attack begin:
shoot firebool!
fire!
score is 3000

contactMonsters begin:
small!
score is 3000

attack begin:
small!
score is 3000

contactMonsters begin:
dead!
score is 3000

如果状态比较多,状态模式会引入非常多的状态类。
当然,状态模式有很多的变种,比如各个状态不需要每次new出来,可以采用单例模式等。这里就不深入讨论了。

总结

  • 穷举法
    优点:简单直接。
    缺点:代码容易冗长,导致可读性和可维护性都很差。
  • 查表法
    优点:清晰,可读性和可维护性更好。
    缺点:只适合执行的动作及其简单的情况。
  • 状态模式:
    优点:通过拆分,避免了大量if else或者switch case。
    缺点:状态过多,状态类数量爆炸。阅读代码时需要打开各个状态类文件。

到这里,做了一个表格。如有错误,还请多指正。状态多暗含了状态转移复杂,动作多暗含了动作逻辑复杂。

动作少动作多
状态多查表法状态模式
状态少穷举法状态模式
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值