微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Golang: 有限状态自动机

有限状态机 又简称FSM(Finite-State Machine的首字母缩写)。这个在离散数学里学过了,它是计算机领域中被广泛使用的数学概念。是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。编译原理学得好的童鞋应该对FSM不陌生,因为编译器就用了FMS来做词法扫描时的状态转移。

FSM的概念在网上一搜可以搜一大堆出来,但估计您也看不大明白。本文将以不一样的方式来讲述FSM的概念以及实现。

现实生活中,状态是随处可见的,并且通过不同的状态来做不同的事。比如冷了加衣服;饿了吃饭;困了睡觉等。这里的冷了、饿了、困了是三种不同的状态,并且根据这三个状态的转变驱动了不同行为的产生(加衣服、吃饭和睡觉)。

FSM是什么

所谓有限状态机,就是由有限个状态组成的机器。再看上面举到的例子:人就是一部机器,能感知三种状态(冷、饿、困)。由于气温降低所以人会觉得冷;由于到了吃饭的时间所以觉得饿;由于晚上12点所以觉得困。状态的产生以及改变都是由某种条件的成立而出现的。不考虑FSM的内部结构时,它就像是一个黑箱子,如下图:

左边是输入一系列条件,FSM通过判定,然后输出结果。

FSM的处理流程

上图FSM屏蔽了判定的过程,事实上FSM是由有限多个状态组成的,每个状态相当于FSM的一个部件。比如要判断一个整数是否偶数,其实只需要判断这个整数的最低位是否为0就行了,代码如下:

$GOPATH/src/fsm_test

----main.go

packagemain

import(
	"fmt"
)

funcIsEven(numint)bool{
	ifnum&0x1==0x0{
		returntrue
	}

	returnfalse
}

funcmain(){
	fmt.Printf("%diseven?%t\n",4,IsEven(4))
	fmt.Printf("%diseven?%t\n",5,IsEven(5))
}
$cd$GOPATH/src/fsm_test
$gobuild
$./fsm_test
4iseven?true
5iseven?false

对数字5来说,它的二进制表示为0101。二进制只能为0或1,所以二进制的字符集合为:{0,1},对应到FSM来说,就是有2种状态,分别为S0和S1。如果用FSM来处理,它总是从左边读取(当然也可以把FSM反过来),也就是从0101最左边那位开始输入:首先输入左边第一位0,停留在S0状态,然后输入第二位1,转到S1状态,再输入第三位0,则又回到S0状态,最后输入是后一位1则又回到S1状态。如下图所示:

上图忽略了一个很重要的细节,就是0和1是怎么输入的。状态S0和状态S1是FSM里的2个小部件,它们分别关联了0和1(也可以说是特定的输入语句),所以只能通过FSM来输入。当FSM接收到0时,它就交给S0去处理,这时S0就变成当前状态,然后对S0输入1,S0则将它交给S1去处理,这时S1就变成当前状态。如此这般,FSM持有有限多个状态,它可以接收输入并执行状态转移(比如将最初的0交给S0去处理)。状态S0和状态S1也是如此。

但是为什么最开始FSM接收输入的0后会交给S0去处理呢?这是因为FSM的认状态是S0。就像是有一台电视机,它总是有认的频道的,您一打开电视机就可以看到影像,即使是满屏的雪花点。而且可以在按下电视机的开关前预先调整频道,之后也可以调整频道。

如何用程序建模

FSM持有有限多个状态集合,有当前状态、认状态、接收的外部数据等。并且FSM有一系列的行为:启动FSM、退出FSM以及状态转移等。State(状态)也会有一系列的行为:进入状态,转移状态等。并且State还有Action行为,比如电视机当前频道正在播放西游记,切换频道后就变成了播放封神榜,原理上是一样的。代码定义如下:

packagemain

//接口
typeIFSMStateinterface{
	Enter()
	Exit()
	CheckTransition()
}

//State父struct
typeFSMStatestruct{}

//进入状态
func(this*FSMState)Enter(){
	//
}

//退出状态
func(this*FSMState)Exit(){
	//
}

//状态转移检测
func(this*FSMState)CheckTransition(){
	//
}

typeFSMstruct{
	//持有状态集合
	statesmap[string]IFSMState
	//当前状态
	current_stateIFSMState
	//认状态
	default_stateIFSMState
	//外部输入数据
	input_datainterface{}
}

//初始化FSM
func(this*FSM)Init(){
	//
}

//添加状态到FSM
func(this*FSM)AddState(keystring,stateIFSMState){
	//
}

//设置认的State
func(this*FSM)SetDefaultState(stateIFSMState){
	//
}

//转移状态
func(this*FSM)TransitionState(){
	//
}

//设置输入数据
func(this*FSM)SetInputData(inputDatainterface{}){
	//
}

//重置
func(this*FSM)Reset(){
	//
}

funcmain(){
}

以上代码只是初略的定义。我们知道FSM不是直接去选择某种状态,而是根据输入条件来选择的。所以可以定义一张输入语句和状态的映射表,本文仅仅简单实现。

NPC例子

游戏中一个玩家可以携带宠物,那么这个 宠物(NPC)就可以看作是FSM。比如这个宠物在每天8点钟开始工作(挣金币),中午12点钟开始打坐练功。8点钟和12点钟就是对这个FSM的输入语句,对应的状态则是开始工作和开始打坐练功。代码实现如下:

packagemain

import(
	"fmt"
)

//接口
typeIFSMStateinterface{
	Enter()
	Exit()
	CheckTransition(hourint)bool
	Hour()int
}

//State父struct
typeFSMStatestruct{}

//进入状态
func(this*FSMState)Enter(){
	//
}

//退出状态
func(this*FSMState)Exit(){
	//
}

//状态转移检测
func(this*FSMState)CheckTransition(hourint){
	//
}

//打坐
typeZazenStatestruct{
	hourint
	FSMState
}

funcNewZazenState()*ZazenState{
	return&ZazenState{hour:8}
}

func(this*ZazenState)Enter(){
	fmt.Println("ZazenState:开始打坐")
}

func(this*ZazenState)Exit(){
	fmt.Println("ZazenState:退出打坐")
}

func(this*ZazenState)Hour()int{
	returnthis.hour
}

//状态转移检测
func(this*ZazenState)CheckTransition(hourint)bool{
	ifhour==this.hour{
		returntrue
	}

	returnfalse
}

//工作
typeWorkerStatestruct{
	hourint
	FSMState
}

funcNewWorkerState()*WorkerState{
	return&WorkerState{hour:12}
}

func(this*WorkerState)Enter(){
	fmt.Println("WorkerState:开始工作")
}

func(this*WorkerState)Exit(){
	fmt.Println("WorkerState:退出工作")
}

func(this*WorkerState)Hour()int{
	returnthis.hour
}

//状态转移检测
func(this*WorkerState)CheckTransition(hourint)bool{
	ifhour==this.hour{
		returntrue
	}

	returnfalse
}

typeFSMstruct{
	//持有状态集合
	statesmap[string]IFSMState
	//当前状态
	current_stateIFSMState
	//认状态
	default_stateIFSMState
	//外部输入数据
	input_dataint
	//是否初始化
	initedbool
}

//初始化FSM
func(this*FSM)Init(){
	this.Reset()
}

//添加状态到FSM
func(this*FSM)AddState(keystring,stateIFSMState){
	ifthis.states==nil{
		this.states=make(map[string]IFSMState,2)
	}
	this.states[key]=state
}

//设置认的State
func(this*FSM)SetDefaultState(stateIFSMState){
	this.default_state=state
}

//转移状态
func(this*FSM)TransitionState(){
	nextState:=this.default_state
	input_data:=this.input_data
	ifthis.inited{
		for_,v:=rangethis.states{
			ifinput_data==v.Hour(){
				nextState=v
				break
			}
		}
	}
	
	ifok:=nextState.CheckTransition(this.input_data);ok{
		ifthis.current_state!=nil{
			//退出一个状态
			this.current_state.Exit()
		}
		this.current_state=nextState
		this.inited=true
		nextState.Enter()
	}
}

//设置输入数据
func(this*FSM)SetInputData(inputDataint){
	this.input_data=inputData
	this.TransitionState()
}

//重置
func(this*FSM)Reset(){
	this.inited=false
}

funcmain(){
	zazenState:=NewZazenState()
	workerState:=NewWorkerState()
	fsm:=new(FSM)
	fsm.AddState("ZazenState",zazenState)
	fsm.AddState("WorkerState",workerState)
	fsm.SetDefaultState(zazenState)
	fsm.Init()
	fsm.SetInputData(8)
	fsm.SetInputData(12)
	fsm.SetInputData(12)
	fsm.SetInputData(8)
	fsm.SetInputData(12)
}
$cd$GOPATH/src/fsm_test
$gobuild
$./fsm_test
ZazenState:开始打坐
ZazenState:退出打坐
WorkerState:开始工作
WorkerState:退出工作
WorkerState:开始工作
WorkerState:退出工作
ZazenState:开始打坐
ZazenState:退出打坐
WorkerState:开始工作

关于对FSM的封装

FSM主要是处理感知外部数据而产生的状态转变,所以别打算去封装它。不同的条件,不同的状态以及不同的处理方式令FSM基本上不太可能去封装,至也多只是做一些语法上的包装罢了。

结束语

真实的场景中,这个NPC所做的工作可能会非常多。比如自动判断周边的环境,发现怪物就去打怪,没血了就自动补血,然后实在打不过就逃跑等等。上例中的SetInputData()就是用于模拟周边环境的数据对NPC的影响,更复杂的情况还在于NPC有时候执行的动作是不能被打断的(上例中的Exit()方法),它只有在完成某个周期的行为才能被终止。这个很容易理解。比如NPC发送网络数据包的时候就不能轻易的被中断,那这个时候其实是可以实现同步原语,状态之间互相wait。

FSM被广泛用于游戏设计和其它各方面,的确是个比较重要的数学模型。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


1、Golang指针 在介绍Golang指针隐式间接引用前,先简单说下Go 语言的指针 (Pointer),一个指针可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。大致上理解如下: 变量名前的
1、概述 1.1 Protocol buffers定义 Protocol buffers 是语言中立、平台中立、可扩展的结构化数据序列化机制,就像 XML,但是它更小、更快、更简单。你只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取各种数据流,支持各
判断文件是否存在,需要用到"os"包中的两个函数: os.Stat()和os.IsNotExit() func Stat(name string) (FileInfo, error) Stat返回描述文件f的FileInfo类型值。如果出错,错误底层类型是*PathError。 func IsNot
1、编译环境 OS :Loongnix-Server Linux release 8.3 CPU指令集 : loongarch64 平台 : 龙芯 go版本 : go version go1.15.6 linux/loong64 2、go和docker安装 docker安装: y
1、概述 Golang是一种强类型语言,虽然在代码中经常看到i:=12这种写法,这其实是编译器在编译期间自动做了类型推断。编译器会对数据进行类型检查,不同类型的数据不能赋值,不能在函数中传参。强类型语言有一些优势,很多的错误会在编译期间被检查出来,不像php和python等弱类型语言,很多错误只有运
1、概述 在《Golang常用语法糖》这篇博文中我们讲解Golang中常用的12种语法糖,在本文我们主要讲解下接收者方法语法糖。 在介绍Golang接收者方法语法糖前,先简单说下Go 语言的指针 (Pointer),大致上理解如下: 变量名前的 & 符号,是取变量的内存地址,不是取
1、概述 1.1 什么是gRPC RPC的全称是Remote Procedure Call,远程过程调用。RPC是一种协议,它实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。 而gRP
1、概述 在Golang语言中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。 2、Go语言函数变量详解 定义 func fun() { } var f func() f = fun 说明 我们首先定义了一个 fun 的函数,接着我们声明了一个
1、概述 Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,支持从设计和文档到测试和部署的整个API生命周期的开发。Swagger是目前最受欢迎的RESTful Api文档生成工具之一,主要的原因如下: 跨平台、跨语言的支持 强大的社区 生态圈 Swagger Tools(S
1、 概述 Protocol buffers 是语言中立、平台中立、可扩展的结构化数据序列化机制,就像 XML,但是它更小、更快、更简单。你只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取各种数据流,支持各种语言。因为profobuf是二进制数据格式,需要编码