由於臨近年關工做繁忙,已經有一段時間沒有更新博客了。到了元旦終於有時間來寫點東西,既是積累也是分享。如題目所示,本文要來聊一聊在遊戲開發中常常會涉及到的話題——遊戲AI。設計遊戲AI的目標之一是要找到一種便於使用並容易拓展的的方案,常見的一些遊戲AI方案包括了有限狀態機(FSM)、分層有限狀態機(HFSM)、面向目標的動做規劃(GOAP)以及分層任務網絡(HTN)和行爲樹(BT)等等。下面咱們就來聊一聊比較有表明性的遊戲AI方案——狀態機。設計模式
有限狀態自動機 (Finite State Machine,FSM)是表示有限多個狀態以及在這些狀態(State)之間轉移(Transition)和動做(Action)的數學模型。有限狀態機的模型體現了兩點:網絡
從它的定義,咱們能夠看到有限狀態機的幾個重要概念:ide
而狀態機即是用來控制對象狀態的管理器。在知足了某種條件或者說在某個特定的事件被觸發以後,對象的狀態便會經過轉換來變成另一種狀態,而對象在不一樣的狀態之下也有可能會有不一樣的行爲和屬性。
固然,有限狀態機的應用範圍很廣,可是顯然遊戲開發是有限狀態機最爲成功的應用領域之一。除了遊戲AI的實現能夠依靠有限狀態機以外,遊戲邏輯以及動做切換均可以藉助有限狀態機來實現。所以遊戲中的每一個角色或者器件或者邏輯都有可能內嵌一個狀態機。模塊化
若是咱們仔細觀察一個有限狀態機的話,能夠發現它在邏輯結構上是沒有層次的,若是和行爲樹來作對比的話能夠發現這一點十分明顯。在行爲樹中,節點是有層次(Hierarchical)的,子節點由其父節點來控制。例如行爲樹中有一種節點叫作「序列(Sequence)節點」,它的做用是順序執行全部子節點(若是某個子節點失敗返回失敗,不然返回成功)。而將行爲樹的這個優點應用到有限狀態機上,分層有限狀態機HFSM便誕生了。函數
那麼引入了分層以後的HFSM到底帶來了什麼好處呢?
最大的好處即是在必定程度上規範了狀態機的狀態轉換,從而有效地減小了狀態之間的轉換。
舉一個簡單的小例子:例如RTS遊戲中的士兵。若是邏輯沒有層次上的劃分,那麼咱們對士兵所定義的若干狀態,例如前進、尋敵、攻擊、防護、逃跑等等,就須要在這些狀態之間定義轉移,由於它們是平級的,所以咱們須要考慮每一組狀態的關係,並維護一大堆沒有側重點的轉移。
若是在邏輯上是分層的,咱們就能夠將士兵的這些狀態進行一個分類,把幾個低級的狀態歸併到一個高級的狀態中,而且狀態的轉移只發生在同級的狀態中。
例如高級狀態包括戰鬥、撤退,而戰鬥狀態中又包括了尋敵、攻擊等幾個小狀態;撤退狀態中又包括了防護、逃跑這幾個小狀態。
總而言之,分層狀態機HFSM從某種程度上規範了狀態機的狀態轉移,並且狀態內的子狀態不須要關心外部狀態的跳轉,這樣也作到了無關狀態間的隔離。this
那麼到底如何實現一個有限狀態機呢?主要有兩種方式來實現,即集中管理控制以及模塊化管理。具體來講,這兩種方式的實現以下:設計
瞭解了有限狀態機大致上能夠分爲這兩種實現方式,那麼接下來咱們就具體來看一看這兩種方式是如何實現的。code
在實現有限狀態機時,使用switch語句是最簡單同時也是最直接的一種方式。這種方式的基本思路是爲狀態機中的每一種狀態都設置一個case分支,專門用來對該狀態進行控制。
上圖是一個具體的使用有限狀態機實現遊戲AI的場景,描述的是一個遊戲單位的AI,下面咱們就使用switch語句來實現圖中的狀態機。對象
switch (state) { // 處理狀態Waiting的分支 case State.Waiting: // 執行等待 wait(); // 檢查是否有能夠攻擊 if (canAttack()){ // 當前狀態轉換爲Attacking changeState(State.Attacking); } // 若不可攻擊,則檢查是否有能夠移動 else if (canMove()) { // 當前狀態轉換爲Moving changeState(State.Moving) } break; // 處理狀態Moving的分支 case State.Moving: // 執行動做move move(); // 檢查是否能夠攻擊敵人 if (canAttack()) { // 當前狀態轉換爲Attacking changeState(State.Attacking); } // 若不可攻擊,則檢查是否能夠等待 else if (canWait()) { // 當前狀態轉換爲Waiting changeState(State.Waiting); } break; // 處理狀態Attacking的分支 case State.Attacking: // 執行攻擊attack attack(); // 檢查是否能夠等待 if (canWait()) { // 當前狀態轉換爲Waiting changeState(State.Waiting); } break; }
經過這個小例子,咱們能夠看到使用switch語句實現的有限狀態機的確能夠很好的運行。不過咱們還能夠發現這種方式在實現狀態之間的轉換時,1.檢查轉換條件以及2.進行狀態轉換的代碼都是混雜在當前的狀態分支中來完成的,這樣就會致使代碼的可讀性下降甚至會增長往後的維護成本。
這是由於在每一個具體的狀態下,都須要檢查多個具體的轉換條件,對符合條件的還須要轉移到新的具體的狀態,這樣的代碼是難以維護的,由於它們須要在具體的狀況下處理具體的事物。即使咱們將檢查轉換條件和進行狀態轉換的代碼分別封裝成兩個專門的函數FuncA(檢查轉換條件)和FuncB(進行狀態轉換),switch語句中各個具體狀態的代碼可能會更加清晰。可是隨着邏輯複雜度的增長,FuncA和FuncB這兩個函數自己的複雜度可能也會增長,甚至最後變得臃腫不堪。blog
當控制一個對象狀態轉換的條件表達式過於複雜時,把狀態的判斷邏輯轉移到一系列類當中,能夠把複雜的邏輯判斷簡單化。所以,使用狀態模式來實現狀態機雖然不如直接使用switch語句來的直接,可是對於狀態更易維護也更易拓展。下面咱們就來看一看狀態模式中的角色:
1. 上下文環境(Context):它定義了客戶程序須要的接口並維護一個具體狀態的實例,將與狀態相關的操做(1.檢查轉換條件;2.進行狀態轉換)交給當前的具體狀態對象來處理。
2. 抽象狀態(State):定義一個接口以封裝使用上下文環境的的一個特定狀態相關的行爲。
3. 具體狀態(Concrete State):實現抽象狀態定義的接口。
下面,咱們就按照這三個角色來實現上一小節圖中的狀態機吧。
context類:
public class Context { private State state; public Context(State state) { this.state = state; } public void Do() { state.CheckAndTran(this); } }
抽象狀態類:
public abstract class State { public abstract void CheckAndTran(Context context); }
具體狀態類
public class WaitingState : State { public override void CheckAndTran(Context context) { //執行等待動做 Wait(); //檢查是否能夠攻擊敵人 if (canAttack()){ // 當前狀態轉換爲Attacking context.State = new AttackingState(); } // 若不可攻擊,則檢查是否有能夠移動 else if (canMove()) { // 當前狀態轉換爲Moving context.State = new MovingState(); } } } ...
雖然看似狀態模式緩解了使用switch語句那種代碼臃腫、可讀性維護性差的問題,可是狀態模式並不是沒有本身的缺點。能夠看出狀態模式的使用必然會增長類和對象的個數,若是使用不當將致使程序結構和代碼的混亂。
在遊戲開發中使用狀態機顯然不失爲一種不錯的選擇,首先它的概念並不複雜,其次它的實現也十分簡單而直接。但它的缺點卻也十分明顯,例如難以複用,由於它每每須要根據具體的狀況來作出反應,固然當狀態機的模型複雜到必定的程度以後,也會帶來實現和維護上的困難。如何選擇,可能就是一個仁者見仁智者見智的問題了。