游戲編程模式(五):狀態(tài)模式
一、混亂的控制代碼
首先考慮一個簡單的“按B鍵觸發(fā)玩家跳躍的場景”:

好,它擁有了基本的跳躍功能,但是會有一些問題:當你瘋狂的按跳躍鍵時,主角就會一直浮空。最容易想到的處理辦法就是添加一個狀態(tài)字段,用來判斷主角是否處于跳躍狀態(tài):

現(xiàn)在如果想添加另外一個動作:當玩家按下方向鍵時,如果角色在地上,我們想要她臥倒,而松開按鍵時站起來:

好,現(xiàn)在又出現(xiàn)了另外一個問題:玩家可以先按下鍵臥倒,再按跳躍鍵跳起,在空中放開下鍵,這將導(dǎo)致主角在跳一半時貼圖變成了站立時的貼圖。如果我們要修復(fù),很容易想到:再加一個狀態(tài)字段用來標識:

這看起來目前還算過得去,但是一個游戲主角能做的動作可能多達十多個幾十個,每加入一個動作都會使代碼數(shù)量增加一個級別,使代碼可讀性和可維護性、可拓展性減少一個量級,最重要的是,它會帶來一堆BUG。
二、有限狀態(tài)機
有限狀態(tài)機(FSMs)可以很好地解決這一類問題,它有幾個關(guān)鍵點:
狀態(tài)機擁有所有可能狀態(tài)的集合
狀態(tài)機同時只能處于一個狀態(tài)
一連串的輸入和事件被發(fā)送給狀態(tài)機
每個狀態(tài)都有一系列的轉(zhuǎn)移,每個轉(zhuǎn)移與輸入和另一狀態(tài)相關(guān)
將上面的玩家控制的例子用狀態(tài)機來表示:

將上述的有限狀態(tài)機用代碼來表示,首先想到的會是用enum和switch來實現(xiàn):

四、狀態(tài)模式
用枚舉和分支來實現(xiàn)有限狀態(tài)機明顯會導(dǎo)致可讀性和可維護性差,對于對OOP非常熟悉的人來說,每一個case分支都可能是一個使用面向?qū)ο蟮臋C會,由此產(chǎn)生了狀態(tài)模式。在GoF中這樣描述狀態(tài)模式;
允許一個對象在其內(nèi)部狀態(tài)發(fā)生變化時改變自己的行為,該對象看起來好像修改了它的類型
把之前的枚舉都轉(zhuǎn)化成一個狀態(tài)類,把switch的每個分支轉(zhuǎn)換成一個共用方法(這里給臥倒狀態(tài)新添加了一個chargeTime來實現(xiàn)蓄力釋放技能的功能):

再將HeroineState作為Heroine類的一個字段,并將輸入?yún)?shù)委托給HeroineState的handleInput和update方法來處理:

好,現(xiàn)在我們要改變主角Heroine的狀態(tài),只需要將HeroineState字段指向不同的HeroineState對象。讓主對象通過改變委托的對象,來改變它的行為,這就是狀態(tài)模式的基本思想了。
五、關(guān)于狀態(tài)類的實例化
我們現(xiàn)在需要考慮如何來實例化狀態(tài)類,用于狀態(tài)之間的轉(zhuǎn)換。
靜態(tài)狀態(tài):如果狀態(tài)對象沒有其他數(shù)據(jù)字段,那么每個狀態(tài)類就只需要一個實例,因為每個實例都將完全一樣(享元模式)。當然,當狀態(tài)對象中有其他狀態(tài)相關(guān)的字段時,就不得不為這個狀態(tài)類實例化多個對象。
六、入口行為和出口行為
狀態(tài)模式的目標是將狀態(tài)的行為和數(shù)據(jù)封裝到單一類中。我們現(xiàn)在已經(jīng)將狀態(tài)的行為封裝好,但是在狀態(tài)的數(shù)據(jù)上還有一些問題。比如:

修改貼圖的代碼和狀態(tài)耦合在了一起,在DuckingState類中出現(xiàn)了IMAGE_STAND的貼圖代碼,此時可以給狀態(tài)一個入口行為來解決這個問題(出口行為同理):

七、并發(fā)狀態(tài)機
假設(shè)現(xiàn)在要給主角一個拿槍的能力,并且在拿槍的時候它也同時能做完全一樣的行為:跑動、跳躍、跳斬、臥倒、站立等,現(xiàn)在如何來設(shè)計狀態(tài)類?或許需要翻倍的狀態(tài)類:站立,持槍站立,跳躍,持槍跳躍……,此時不僅增加了大量的狀態(tài),而且增加了大量的冗余,持槍和不持槍的狀態(tài)是完全一樣的,只是多了一點負責(zé)射擊的代碼。
問題在于我們將兩種狀態(tài)綁定在了一個狀態(tài)機上,所以處理方法就很明顯了:使用兩個單獨的狀態(tài)機:
如果在做什么有n個狀態(tài),而攜帶了什么有m個狀態(tài),要塞到一個狀態(tài)機中,則需要n × m個狀態(tài)。使用兩個狀態(tài)機,就只有n + m個
實現(xiàn)很簡單,只是在原有基礎(chǔ)上添加了幾乎完全一樣的少量代碼:

八、分層狀態(tài)機
如果對OOP敏感,很容易發(fā)現(xiàn)可以繼續(xù)進行優(yōu)化,站立、行走、奔跑、滑鏟這些狀態(tài)或許有很多重復(fù)的代碼來處理與地面碰撞和跳躍的行為,此時可以定義一個地面類來處理這些行為,然后將它們繼承自這個類(當然繼承也會在這些類之間建立一些耦合,此時就需要進行平衡取舍):

九、下推自動機
考慮一個場景:給主角添加了一個射擊狀態(tài),不管現(xiàn)在是什么狀態(tài),都能在按下開火按鈕時跳轉(zhuǎn)為射擊狀態(tài),但關(guān)鍵問題是,如何在射擊后轉(zhuǎn)換為之前的狀態(tài)。
解決方法是下推自動機,有限狀態(tài)機有一個指向狀態(tài)的指針,下推自動機有一個棧指針,它也可以實現(xiàn)狀態(tài)之間的轉(zhuǎn)換,并且:
可以將新狀態(tài)壓入棧中
可以彈出最上面的狀態(tài)
