Golang: 有限狀態自動機

有限狀態機 又簡稱FSM(Finite-State Machine的首字母縮寫)。這個在離散數學裏學過了,它是計算機領域中被普遍使用的數學概念。是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學模型。編譯原理學得好的童鞋應該對FSM不陌生,由於編譯器就用了FMS來作詞法掃描時的狀態轉移。shell

FSM的概念在網上一搜能夠搜一大堆出來,但估計您也看不大明白。本文將以不同的方式來說述FSM的概念以及實現。網絡

現實生活中,狀態是隨處可見的,而且經過不一樣的狀態來作不一樣的事。好比冷了加衣服;餓了吃飯;困了睡覺等。這裏的冷了、餓了、困了是三種不一樣的狀態,而且根據這三個狀態的轉變驅動了不一樣行爲的產生(加衣服、吃飯和睡覺)。ui

FSM是什麼

所謂有限狀態機,就是由有限個狀態組成的機器。再看上面舉到的例子:人就是一部機器,能感知三種狀態(冷、餓、困)。因爲氣溫下降因此人會以爲冷;因爲到了吃飯的時間因此以爲餓;因爲晚上12點因此以爲困。狀態的產生以及改變都是由某種條件的成立而出現的。不考慮FSM的內部結構時,它就像是一個黑箱子,以下圖:this

左邊是輸入一系列條件,FSM經過斷定,而後輸出結果。spa

FSM的處理流程

上圖FSM屏蔽了斷定的過程,事實上FSM是由有限多個狀態組成的,每一個狀態至關於FSM的一個部件。好比要判斷一個整數是否偶數,其實只須要判斷這個整數的最低位是否爲0就好了,代碼以下:設計

$GOPATH/src/fsm_testcode

----main.go接口

package main

import (
	"fmt"
)

func IsEven(num int) bool {
	if num&0x1 == 0x0 {
		return true
	}

	return false
}

func main() {
	fmt.Printf("%d is even? %t\n", 4, IsEven(4))
	fmt.Printf("%d is even? %t\n", 5, IsEven(5))
}

$ cd $GOPATH/src/fsm_test
$ go build
$ ./fsm_test
4 is even? true
5 is even? 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也是如此。ip

可是爲何最開始FSM接收輸入的0後會交給S0去處理呢?這是由於FSM的默認狀態是S0。就像是有一臺電視機,它老是有默認的頻道的,您一打開電視機就能夠看到影像,即便是滿屏的雪花點。並且能夠在按下電視機的開關前預先調整頻道,以後也能夠調整頻道。

如何用程序建模

FSM持有有限多個狀態集合,有當前狀態、默認狀態、接收的外部數據等。而且FSM有一系列的行爲:啓動FSM、退出FSM以及狀態轉移等。State(狀態)也會有一系列的行爲:進入狀態,轉移狀態等。而且State還有Action行爲,好比電視機當前頻道正在播放西遊記,切換頻道後就變成了播放封神榜,原理上是同樣的。代碼定義以下:

package main

// 接口
type IFSMState interface {
	Enter()
	Exit()
	CheckTransition()
}

// State父struct
type FSMState struct{}

// 進入狀態
func (this *FSMState) Enter() {
	//
}

// 退出狀態
func (this *FSMState) Exit() {
	//
}

// 狀態轉移檢測
func (this *FSMState) CheckTransition() {
	//
}

type FSM struct {
	// 持有狀態集合
	states map[string]IFSMState
	// 當前狀態
	current_state IFSMState
	// 默認狀態
	default_state IFSMState
	// 外部輸入數據
	input_data interface{}
}

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

// 添加狀態到FSM
func (this *FSM) AddState(key string, state IFSMState) {
	//
}

// 設置默認的State
func (this *FSM) SetDefaultState(state IFSMState) {
	//
}

// 轉移狀態
func (this *FSM) TransitionState() {
	//
}

// 設置輸入數據
func (this *FSM) SetInputData(inputData interface{}) {
	//
}

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

func main() {
}

以上代碼只是初略的定義。咱們知道FSM不是直接去選擇某種狀態,而是根據輸入條件來選擇的。因此能夠定義一張輸入語句和狀態的映射表,本文僅僅簡單實現。

NPC例子

遊戲中一個玩家能夠攜帶寵物,那麼這個 寵物(NPC)就能夠看做是FSM。好比這個寵物在天天8點鐘開始工做(掙金幣),中午12點鐘開始打坐練功。8點鐘和12點鐘就是對這個FSM的輸入語句,對應的狀態則是開始工做和開始打坐練功。代碼實現以下:

package main

import (
	"fmt"
)

// 接口
type IFSMState interface {
	Enter()
	Exit()
	CheckTransition(hour int) bool
	Hour() int
}

// State父struct
type FSMState struct{}

// 進入狀態
func (this *FSMState) Enter() {
	//
}

// 退出狀態
func (this *FSMState) Exit() {
	//
}

// 狀態轉移檢測
func (this *FSMState) CheckTransition(hour int) {
	//
}

// 打坐
type ZazenState struct {
	hour int
	FSMState
}

func NewZazenState() *ZazenState {
	return &ZazenState{hour: 8}
}

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

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

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

// 狀態轉移檢測
func (this *ZazenState) CheckTransition(hour int) bool {
	if hour == this.hour {
		return true
	}

	return false
}

// 工做
type WorkerState struct {
	hour int
	FSMState
}

func NewWorkerState() *WorkerState {
	return &WorkerState{hour: 12}
}

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

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

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

// 狀態轉移檢測
func (this *WorkerState) CheckTransition(hour int) bool {
	if hour == this.hour {
		return true
	}

	return false
}

type FSM struct {
	// 持有狀態集合
	states map[string]IFSMState
	// 當前狀態
	current_state IFSMState
	// 默認狀態
	default_state IFSMState
	// 外部輸入數據
	input_data int
	// 是否初始化
	inited     bool
}

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

// 添加狀態到FSM
func (this *FSM) AddState(key string, state IFSMState) {
	if this.states == nil {
		this.states = make(map[string]IFSMState, 2)
	}
	this.states[key] = state
}

// 設置默認的State
func (this *FSM) SetDefaultState(state IFSMState) {
	this.default_state = state
}

// 轉移狀態
func (this *FSM) TransitionState() {
	nextState := this.default_state
	input_data := this.input_data
	if this.inited {
		for _, v := range this.states {
			if input_data == v.Hour() {
				nextState = v
				break
			}
		}
	}
	
	if ok := nextState.CheckTransition(this.input_data); ok {
		if this.current_state != nil {
			// 退出前一個狀態
			this.current_state.Exit()
		}
		this.current_state = nextState
		this.inited = true
		nextState.Enter()
	}
}

// 設置輸入數據
func (this *FSM) SetInputData(inputData int) {
	this.input_data = inputData
	this.TransitionState()
}

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

func main() {
	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
$ go build
$ ./fsm_test
ZazenState: 開始打坐
ZazenState: 退出打坐
WorkerState: 開始工做
WorkerState: 退出工做
WorkerState: 開始工做
WorkerState: 退出工做
ZazenState: 開始打坐
ZazenState: 退出打坐
WorkerState: 開始工做

關於對FSM的封裝

FSM主要是處理感知外部數據而產生的狀態轉變,因此別打算去封裝它。不一樣的條件,不一樣的狀態以及不一樣的處理方式令FSM基本上不太可能去封裝,至也多隻是作一些語法上的包裝罷了。

結束語

真實的場景中,這個NPC所作的工做可能會很是多。好比自動判斷周邊的環境,發現怪物就去打怪,沒血了就自動補血,而後實在打不過就逃跑等等。上例中的SetInputData()就是用於模擬周邊環境的數據對NPC的影響,更復雜的狀況還在於NPC有時候執行的動做是不能被打斷的(上例中的Exit()方法),它只有在完成某個週期的行爲才能被終止。這個很容易理解。好比NPC發送網絡數據包的時候就不能輕易的被中斷,那這個時候實際上是能夠實現同步原語,狀態之間互相wait。

FSM被普遍用於遊戲設計和其它各方面,的確是個比較重要的數學模型。

相關文章
相關標籤/搜索