這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
有限狀態機器 又簡稱FSM(Finite-State Machine的首字母縮寫)。這個在離散數學裡學過了,它是電腦領域中被廣泛使用的數學概念。是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。編譯原理學得好的童鞋應該對FSM不陌生,因為編譯器就用了FMS來做詞法掃描時的狀態轉移。
FSM的概念在網上一搜可以搜一大堆出來,但估計您也看不大明白。本文將以不一樣的方式來講述FSM的概念以及實現。
現實生活中,狀態是隨處可見的,並且通過不同的狀態來做不同的事。比如冷了加衣服;餓了吃飯;困了睡覺等。這裡的冷了、餓了、困了是三種不同的狀態,並且根據這三個狀態的轉變驅動了不同行為的產生(加衣服、吃飯和睡覺)。
FSM是什麼
所謂有限狀態機器,就是由有限個狀態組成的機器。再看上面舉到的例子:人就是一部機器,能感知三種狀態(冷、餓、困)。由於氣溫降低所以人會覺得冷;由於到了吃飯的時間所以覺得餓;由於晚上12點所以覺得困。狀態的產生以及改變都是由某種條件的成立而出現的。不考慮FSM的內部結構時,它就像是一個黑箱子,如:
左邊是輸入一系列條件,FSM通過判定,然後輸出結果。
FSM的處理流程
FSM屏蔽了判定的過程,事實上FSM是由有限多個狀態組成的,每個狀態相當於FSM的一個組件。比如要判斷一個整數是否偶數,其實只需要判斷這個整數的最低位是否為0就行了,代碼如下:
$GOPATH/src/fsm_test
----main.go
package mainimport ("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_test4 is even? true5 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也是如此。
但是為什麼最開始FSM接收輸入的0後會交給S0去處理呢?這是因為FSM的預設狀態是S0。就像是有一台電視機,它總是有預設的頻道的,您一開啟電視機就可以看到影像,即使是滿屏的雪花點。而且可以在按下電視機的開關前預先調整頻道,之後也可以調整頻道。
如何用程式建模
FSM持有有限多個狀態集合,有目前狀態、預設狀態、接收的外部資料等。並且FSM有一系列的行為:啟動FSM、退出FSM以及狀態轉移等。State(狀態)也會有一系列的行為:進入狀態,轉移狀態等。並且State還有Action行為,比如電視機目前通道現正播放西遊記,切換頻道後就變成了播放封神榜,原理上是一樣的。代碼定義如下:
package main// 介面type IFSMState interface {Enter()Exit()CheckTransition()}// State父structtype 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{}}// 初始化FSMfunc (this *FSM) Init() {//}// 添加狀態到FSMfunc (this *FSM) AddState(key string, state IFSMState) {//}// 設定預設的Statefunc (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 mainimport ("fmt")// 介面type IFSMState interface {Enter()Exit()CheckTransition(hour int) boolHour() int}// State父structtype FSMState struct{}// 進入狀態func (this *FSMState) Enter() {//}// 退出狀態func (this *FSMState) Exit() {//}// 狀態轉移檢測func (this *FSMState) CheckTransition(hour int) {//}// 打坐type ZazenState struct {hour intFSMState}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 intFSMState}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}// 初始化FSMfunc (this *FSM) Init() {this.Reset()}// 添加狀態到FSMfunc (this *FSM) AddState(key string, state IFSMState) {if this.states == nil {this.states = make(map[string]IFSMState, 2)}this.states[key] = state}// 設定預設的Statefunc (this *FSM) SetDefaultState(state IFSMState) {this.default_state = state}// 轉移狀態func (this *FSM) TransitionState() {nextState := this.default_stateinput_data := this.input_dataif this.inited {for _, v := range this.states {if input_data == v.Hour() {nextState = vbreak}}}if ok := nextState.CheckTransition(this.input_data); ok {if this.current_state != nil {// 退出前一個狀態this.current_state.Exit()}this.current_state = nextStatethis.inited = truenextState.Enter()}}// 設定輸入資料func (this *FSM) SetInputData(inputData int) {this.input_data = inputDatathis.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_testZazenState: 開始打坐ZazenState: 退出打坐WorkerState: 開始工作WorkerState: 退出工作WorkerState: 開始工作WorkerState: 退出工作ZazenState: 開始打坐ZazenState: 退出打坐WorkerState: 開始工作
關於對FSM的封裝
FSM主要是處理感知外部資料而產生的狀態轉變,所以別打算去封裝它。不同的條件,不同的狀態以及不同的處理方式令FSM基本上不太可能去封裝,至也多隻是做一些文法上的封裝罷了。
結束語
真實的情境中,這個NPC所做的工作可能會非常多。比如自動判斷周邊的環境,發現怪物就去打怪,沒血了就自動補血,然後實在打不過就逃跑等等。上例中的SetInputData()就是用於類比周邊環境的資料對NPC的影響,更複雜的情況還在於NPC有時候執行的動作是不能被打斷的(上例中的Exit()方法),它只有在完成某個周期的行為才能被終止。這個很容易理解。比如NPC發送網路資料包的時候就不能輕易的被中斷,那這個時候其實是可以實現同步原語,狀態之間互相wait。
FSM被廣泛用於遊戲設計和其它各方面,的確是個比較重要的數學模型。