有限状态机

article/2025/9/29 17:43:17

文章目录

  • 有限状态机
  • 状态机的表示
    • 状态转移图
    • 二维表
  • 实现
    • 穷举法
    • 查表法
    • 状态模式
  • 总结

有限状态机

有限状态机(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();//nothingm.contactMonsters();//diedreturn 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 1000eatMushRooms begin:
fire!
score is 2000eatMushRooms begin:
fire!
score is 3000attack begin:
shoot firebool!contactMonsters begin:
small!
score is 3000attack 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 1000eatMushRooms begin:
fire!
score is 2000eatMushRooms begin:
fire!
score is 3000attack begin:
shoot firebool!
fire!
score is 3000contactMonsters begin:
small!
score is 3000attack begin:
do nothing!
small!
score is 3000contactMonsters 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.cppMario::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.hclass 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();//nothingm.contactMonsters();//diedreturn 0;
}

结果:

eatMushRooms begin:
super!
score is 1000eatMushRooms begin:
fire!
score is 2000eatMushRooms begin:
fire!
score is 3000attack begin:
shoot firebool!
fire!
score is 3000contactMonsters begin:
small!
score is 3000attack begin:
small!
score is 3000contactMonsters begin:
dead!
score is 3000

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

总结

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

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

动作少动作多
状态多查表法状态模式
状态少穷举法状态模式

http://chatgpt.dhexx.cn/article/XPotL9wA.shtml

相关文章

什么是状态机?

前言 状态机在实际工作开发中应用非常广泛&#xff0c;在刚进入公司的时候&#xff0c;根据公司产品做流程图的时候&#xff0c;发现自己经常会漏了这样或那样的状态&#xff0c;导致整体流程会有问题&#xff0c;后来知道了状态机这样的东西&#xff0c;发现用这幅图就可以很…

什么是状态机(Finite-state machine)?

有限状态机 有限状态机(FSM)1、 什么是“状态”2、什么是状态机&#xff1f;3、状态机图怎么画&#xff1f;参考 有限状态机(FSM) 1、 什么是“状态” 先来解释什么是“状态”&#xff08; State &#xff09;。现实事物是有不同状态的&#xff0c;例如一个自动门&#xff0c…

什么是状态机?用C语言实现进程5状态模型

前言 状态机在实际工作开发中应用非常广泛&#xff0c;在刚进入公司的时候&#xff0c;根据公司产品做流程图的时候&#xff0c;发现自己经常会漏了这样或那样的状态&#xff0c;导致整体流程会有问题&#xff0c;后来知道了状态机这样的东西&#xff0c;发现用这幅图就可以很…

状态机(state machine)

一、状态机分类 Mealy状态机:输出取决于输入和当前状态 状态寄存器:由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳变沿。状态寄存器由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳变沿。 状态是否改变、如何…

状态机(有限状态自动机 FSM)介绍以及常用状态机种类对比

目录 状态机概念 : 为什么需要状态机: 使用场景 状态机四要素: 常见类型状态机: Squirrel State Machine Spring Statemachine 状态机概念 : 概念 : 状态机是有限状态自动机&#xff08;英语&#xff1a;finite-state machine&#xff0c;缩写&#xff1a;FSM&#xff…

STM32状态机编程----什么是状态机?

万事万物都有其状态 什么是状态 状态是人或事物表现出来的形态。是指现实&#xff08;或虚拟&#xff09;事物处于生成、生存、发展、消亡时期或各转化临界点时的形态或事物态势。 通过上面那句话&#xff0c;我们知道了状态就是一个对象在不同情况下对应的各种形态 做产品的…

什么是状态机?一篇文章就够了

1 概述 状态机[1]一般指有限状态机&#xff08;英语&#xff1a;finite-state machine&#xff0c;缩写&#xff1a;FSM&#xff09;又称有限状态自动机&#xff08;英语&#xff1a;finite-state automaton&#xff0c;缩写&#xff1a;FSA&#xff09;&#xff0c;是表示有限…

C语言_有限状态机(FSM)

C语言_有限状态机&#xff08;Finite State Machine&#xff09; 基本介绍 许多小型或复杂的应用程序都使用有限状态机 (FSM)&#xff0c;C 语言中的有限状态机是嵌入式系统的流行设计模式之一&#xff0c;有限状态机使开发变得容易和顺利。 有很多设备使用事件基态&#xf…

Unity字体展示下载

Unity字体种类展示 这是字体包里面的图片,是不是很多种字体. 下载链接: https://download.csdn.net/download/qq_42603590/12001130 这是下载字体包的地方,很便宜.没有积分的可以留言,我发给你 有时候可能回复的不是很快(抱拳了) 喜欢的话点个赞,关注一下再走吧,谢谢

Unity 之 官网下载地址,方便各个版本的 Unity 安装包下载

Unity 之 官网下载地址&#xff0c;方便各个版本的 Unity 安装包下载 目录 Unity 之 官网下载地址&#xff0c;方便各个版本的 Unity 安装包下载 一、简单介绍 二、各个版本下载入口网址 一、简单介绍 在 Unity 的下载地址现在不是很好找&#xff0c;这里保存一下 Unity 各…

Unity入门之路0-Unity下载安装以及版本选择

文章目录 下载链接Unity Hub和Unity的关系UnityHub下载(Win)两者比较 Unity版本选择许可证问题 下载链接 一定不要百度或者去垃圾网站下载盗版网站 &#xff0c;Unity是正版免费的&#xff0c;官方很关注使用者的感受&#xff0c;所以下载官网的就没问题。 https://unity.cn/re…

UnityHub下载缓存位置

一、下载Unity各版本的编辑器 C:\Users\XXX\AppData\Local\Temp\unityhub-xxx-xxx-xxx-xxx 我电脑是 C:\Users\Administrator\AppData\Local\Temp\unityhub-xxxx-xxxx 如果你不需要备份安装包&#xff0c;那么这个缓存的文件夹&#xff0c;就与你无关了&#xff0c;因为安装完…

使用UnityHub下载任意版本Unity

目录 方法一 使用链接方法二 官网下载(适用于2018.4.23及以上版本) unityHub上只能下载官方指定的版本,很多其他版本不能下载,下面介绍的是在unityHub下载任意版本的方法 方法一 使用链接 举例: 2019.2.11f1版本的unity----> unityhub://2019.2.11f1/5f859a4cfee5 格式 unit…

Unity给游戏对象贴图、从官网下载资源、导入导出

1、新建项目、在项目场景中创建几何对象并修改参数 在层级“”中创建一个立方体&#xff08;3D对象&#xff09;&#xff0c;同理也创建一个球体 创建好的立方体会显示在场景视图中 &#xff08;从场景视图或层级视图中&#xff09;选中几何体&#xff0c;选择场景视图中竖排工…

unity下载网页所有图片

用unity的c#脚本批量下载网页上的所有图片 1、将网页的html保存到本地 在网页上鼠标右击另存为如下图所示 保存html文件 2、通过截取<img“”>获取图片存储的地址 经过两个步骤之后就可以开始着手敲代码了 代码 html下载的本地地址和要保存的图片地址 //保存在本地ht…

Unity下载方法(超详细)

一、进入官网&#xff0c;点击[下载Unity]&#xff0c;点击右上角的小人头像&#xff0c;点击[创建Unity ID](创建ID的方法你点进去按照它要求你的一步一步做就行啦)。 二、创建完Unity ID并登录(或已有Unity ID并登录)后&#xff0c;下拉网页&#xff0c;点击[下载Unity Hub]&…

Unity 改变下载资源商店中资源默认路径的方法

Unity 改变下载资源商店中资源默认路径的方法 Unity资源商店中免费资源可以被我们很好的使用&#xff0c;尤其对于暂时还不会自己设计资源的创作者。但是&#xff0c;unity默认是将资源商店的下载路径设置在了C:\Users\操作系统当前用户\AppData\Roaming\Unity\Asset Store-5.x…

Unity 各版本下载方法

开发Unity的&#xff0c;获取不同版本Unity版本和了解Unity最新动态很重要&#xff0c;现在更新迭代很频繁&#xff0c;在开发时&#xff0c;不论遇到项目升级&#xff0c;还是插件要求&#xff0c;还是老项目运行&#xff0c;总是在多个版本间切换。 是不是经常遇到&#xff0…

Unity3d_NGUI和UGUI的学习

由于之前刚入门的时候&#xff0c;应Unity3d整体发展的要求我们自学了UGUI(相对来说UGUI比NGUI做得更好一些&#xff0c;后面会有2者对比)&#xff0c;但是后来公司要求使用NGUI&#xff0c;所以我这边把之前学习UGUI&#xff08;不全&#xff0c;当时资源有限&#xff09;和NG…

NGUI与UGUI的区别及其优缺点

UIGUI与BGUI 的区别 首先说一下NGUI NGUI是严格遵循KISS原则并用C#编写的Unity&#xff08;适用于专业版和免费版&#xff09;插件&#xff0c;提供强大的UI系统和事件通知框架。其代码简洁&#xff0c;多数类少于200行代码。这意味着程序员可以很容易地扩展NGUI的功能或调节已…