命令模式:游戲開發設計模式之命令模式(unity3d 示例實現)
對象池模式:游戲開發設計模式之對象池模式(unity3d 示例實現)
原型模式:游戲開發設計模式之原型模式 & unity3d JSON的使用(unity3d 示例實現)
說 起狀態模式游戲開發者們第一個想到的一定是AI的有限狀態機FSMs,狀態模式確實是實現有限狀態機的一種方法。之后還會講狀態機的進階分層狀態機 (hierarchical state machines),和pushdown自動機(pushdown automata), 本文就拿人物控制的有限狀態機來講講狀態機模式,本文例子其實是狀態模式和觀察者模式的組合,通過獲取玩家按鍵消息來改變狀態。
如果不使用有限狀態機
如果想要你實現一個2d或3d游戲中的人物控制(本文拿2d游戲舉例),可以讓人物走、跑,相信不少人會這么寫:
if (Input.GetKey(KeyCode.D)) { …設置方向向右.. if (Input.GetKey(KeyCode.LeftShift)) { ..移動..播放跑步動畫.. } else { ..移動..播放走路動畫.. } } else if (Input.GetKey(KeyCode.A)) { …設置方向向左.. if (Input.GetKey(KeyCode.LeftShift)) { ..移動..播放跑步動畫.. } else { ..移動..播放走路動畫.. } }
然后再加上跳躍功能,怎么辦呢,跳躍時肯定不能執行走路的操作播放走路的動畫啊,加一個bool判斷吧,然后代碼變成了這樣:
bool isJump = false; if (Input.GetKeyDown(KeyCode.W)) { isJump = true; } if (Input.GetKey(KeyCode.D)) { …設置方向向右.. if (Input.GetKey(KeyCode.LeftShift)) { if(!isJump) ..移動..播放跑步動畫.. else ..移動..播放跳躍動畫.. } else { if(!isJump) ..移動..播放走路動畫.. else ..移動..播放跳躍動畫.. } } else if (Input.GetKey(KeyCode.A)) { …設置方向向左.. if(!isJump) ..移動..播放跑步動畫.. else ..移動..播放跳躍動畫.. } else { if(!isJump) ..移動..播放走路動畫.. else ..移動..播放跳躍動畫.. } }
然后我們又希望人物按D鍵能夠實現蹲走,怎么辦,再加一個bool,再加判斷!
bool isCrouch = false; if (Input.GetKeyDown(KeyCode.S)) { isCrouch = true; } bool isJump = false; if (Input.GetKeyDown(KeyCode.W)&&! isCrouch) { isJump = true; } if (Input.GetKey(KeyCode.D)) { …設置方向向右.. if (Input.GetKey(KeyCode.LeftShift)) { if(!isJump&&! isCrouch) ..移動..播放跑步動畫.. else if(!isCrouch) ..移動..播放跳躍動畫.. else ..移動..播放蹲走動畫.. } else { if(!isJump&&! isCrouch) ..移動..播放跑步動畫.. else if(!isCrouch) ..移動..播放跳躍動畫.. else ..移動..播放蹲走動畫.. } } else if (Input.GetKey(KeyCode.A)) { …設置方向向左.. if(!isJump&&! isCrouch) ..移動..播放跑步動畫.. else if(!isCrouch) ..移動..播放跳躍動畫.. else ..移動..播放蹲走動畫.. } else { if(!isJump&&! isCrouch) ..移動..播放跑步動畫.. else if(!isCrouch) ..移動..播放跳躍動畫.. else ..移動..播放蹲走動畫.. } }
然后再加入攻擊,跳劈,潛襲,站防,蹲防,還要再繼續添加if else 和bool嗎?我們究竟能容忍多少這樣糾纏在一起的的ifelse? 稍有不慎會出多少錯誤?調試起來復雜不?。。。這種方法顯然是錯誤的!有大量復雜的分支,極易出現bug。不過,救星來了,就是有限狀態機
一個最簡單的有限狀態機
阿 蘭圖靈提出的圖靈機就是一種狀態機,就是指一個抽象的機器,它有一條無限長的紙帶TAPE,紙帶分成了一個一個的小方格,每個方格有不同的顏色。有一個讀 寫頭HEAD在紙帶上移來移去。機器頭有 一組內部狀態,還有一些固定的程序。在每個時刻,機器頭都要從當前紙帶上讀入一個方格信息,然后結合自己的內部狀態查找程序表,根據程序輸出信息到紙帶方 格上,並轉換自己的內部狀態,然后進行移動。
准備工作
狀態機需要滿足的條件:
1. 一組固定的狀態(空閑,行走,跳躍,攻擊。。。)
2. 狀態機一次只能處在一種狀態,最簡單的例子,我們不能跳的同時蹲下。
3. 一些玩家輸入或者事件發送到狀態機,來改變現有狀態
4. 狀態與狀態之間的轉換都有一個過渡,在這個過渡中不接受玩家的任何輸入,比如從站立到蹲下的過渡中玩家在此時按下跳躍或行走等是沒有響應的(被忽略)。
根據上面的條件,我們在寫一個狀態機之前必須要做的一件事就是—畫狀態圖,這樣既可以理清你的思路,方便添加狀態與功能,又能使你編程的遺漏減少。
比如我們想實現一個人的走、跑、跳、攻擊、防御,狀態圖可以這么畫:


enum&switch
然后我們完成最精簡的狀態機,就是enum和switch的組合。
我們需要把一組狀態放在enum里,命名就按你需要的狀態的名字來命名,比如空閑-idle,還需要一個變量來儲存當前控制人物的狀態:
public enum CharacterState { Idling = 0, Walking = 1, Jumping = 2, acting= 3, defending= 4, } public CharacterState heroState = CharacterState. Idling;
設置一個函數handleInput來專門處理判斷玩家的輸入與狀態操作,把這個函數放在update中每幀輪詢。
void handleInput() { switch(heroState) { case CharacterState. Idling: …播放空閑動畫.. if…Input.GetKey –A,D. this. heroState = CharacterState. Walking; else if…Input.GetKey –w. this. heroState = CharacterState. Jumping; else if…Input.GetKey –J. this. heroState = CharacterState. acting; else if…Input.GetKey –I. this. heroState = CharacterState. defending; break; case CharacterState. Walking: if…Input.GetKey –A,D. …CharacterController移動操作.. else…Input.GetKeyUp – A,D… this. heroState = CharacterState. Idling; break; case CharacterState. Jumping: if(Input.GetKeyUp(KeyCode.W)) …CharacterController移動操作.. if(CharacterController.isGrounded) { this. heroState = CharacterState. Idling; } break; case CharacterState. acting: …播放攻擊動畫. chargeTime += Time.timeScale / Time.deltaTime; if(chargeTime>maxTime) { this. heroState = CharacterState. Idling; chargeTime = 0; } break; case CharacterState. defending: …播放防御動畫. if(Input.GetKeyUp(KeyCode.I)) this. heroState = CharacterState. Idling; break; } }
這里又需要通過時間來轉換狀態的,比如攻擊狀態,我們按下攻擊鍵,執行一個攻擊動畫,當動畫結束時,我們希望人物 能回到空閑狀態,此時就需要一個chargeTime變量來記錄攻擊狀態持續了多長時間,在每一幀chargeTime加上這一幀的運行時間,也就是這個 動畫現在播放了多久,我們獲取動畫總時間長度來作為maxTime,當chargeTime達到maxTime也就是說明一個攻擊動作做完了(一個動畫播 完了)就會轉換到空閑狀態,在轉換到空間狀態時再重置chargeTime為0。
這樣可以實現一個簡單的狀態機,所有狀態操作都被整合在一起,原理是輪詢當前的狀態,處理當前狀態操作,接受玩家輸入來轉換狀態。
此時這樣一個狀態機會出現一個問題,我們控制的人物不能跳起來時不能攻擊,而且狀態機規定一次只能處在一種狀態,所以解決辦法為拆分成幾個狀態機,比如負責移動的為一個狀態機,攻擊防御為另一個狀態機,這兩個狀態機並發運行,就可以實現跳起攻擊,或者潛襲等操作了。
當然,這樣的狀態機有很多缺點,比如不方便添加新狀態,這個函數最終也會越寫越長越亂。一種好的實現方式就是把每一個狀態和它的操作還有在這個狀態時對用戶輸入的判斷,也就是對這一個狀態的所有處理都封裝在一個類中,這就是狀態模式。
在實現狀態模式之前,讓我們先來了解一下c#的委托與事件。
c#的委托與事件
說起委托與事件,就肯定與觀察者模式掛鈎了。
delegate委托就是可以用這個委托調用別的類中的一個或多個函數
event事件通常與委托聯用,就是事件發送者,通過委托調用事件接收者中的函數,來發送事件到事件接收者
在有限狀態機中,我們的發送者是update中的按鍵,接收者就是等待處理按鍵的狀態對象(后面會講)。在update中判斷玩家輸入,再把輸入的按鍵作為事件發送給狀態對象處理。就是這么一個過程。
此處就拿攻擊方面狀態舉例
首先我們先寫一個EventArgs事件數據類的子類,是事件要傳送的消息,也就是我們的接收者-狀態對象想要接收的消息,這個消息可以自己定義,在此處,我們希望傳送按鍵信息在接受者中處理
using UnityEngine; using System.Collections; using System; public class InputEventArgs : EventArgs { public string input; public string addition1; public string addition2; public InputEventArgs(string _input, string _addition1, string _addition2) { this.input = _input; this.addition1 = _addition1; this.addition2 = _addition2; } }
類中的參數用來存儲信息此處的input為按鍵,addition1,addition2,留空,以便以后開發需要新的信 息。作為攻擊方面狀態addition1,addition2可以是武器的種類(不同的攻擊方式,不同的播放動畫,不同的技能等等)也可以對組合鍵加以判 斷(有些游戲中組合鍵可以讓人物發出某些特殊技能)
然后來看看事件發送者,新建一個hero類專門控制人物狀態操作,heroStateActVer儲存着當前狀態,在update函數中
public delegate void InputEventHandler(object sender, InputEventArgs e); public event InputEventHandler InputTran; InputEventArgs inputArgs = new InputEventArgs("","",""); State heroStateActVer; void Start() { personCtrl = this.GetComponent<HeroCtrl>(); heroStateActVer = new IdleStateActVer(this.gameObject); InputTran += new InputEventHandler(heroStateActVer.handleInput); } void Input_events(InputEventArgs e) { if (e != null) { InputTran(this, e); } } void Update() { if (Input.anyKeyDown) { foreach (char c in Input.inputString) { if (c != null) { inputArgs.input = c.ToString(); Input_events(inputArgs); } } } heroStateActVer.UpDate(); }
state的UpDate()存儲了對該狀態的實時操作,heroStateActVer.UpDate();為處理當前狀態該有的操作,比如行走狀態就是CharacterController.move,之后我們會講解
我們在發送類中定義委托與事件,我們接收用戶按鍵,以字符串的形式存儲在事件信息類InputEventArgs中把它發給接受者state對象
我們之所以使用foreach,是因為Input.inputString可以接收多個按鍵,比如A鍵和D鍵同時按,Input.inputString就是“ad”,如下圖所示,所以我們遍歷一幀按下的所有鍵,來發送消息。

然后就是我們的接收類,也是訂閱者state,state對象中有一個方法用來接收發送者發來的事件信息,也就是當事件發生時執行的函數
public void handleInput(object sender, InputEventArgs e) { input = e.input; switch (input) { case "j"://攻擊 …轉為攻擊狀態.. break; case "i"://防御 …轉為防御狀態… break; } }
狀態模式
四人幫說:
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
當一個對象的狀態改變時來改變他的行為。對象將會改變它的類。
繼承於父類狀態的各個子狀態其中的每一個把它的操作還有在這個狀態時對用戶輸入和外部事件的判斷,也就是對這一個狀態的所有處理都封裝在一個類中,對用戶輸入的判斷和外部事件進行處理,如果需要則改變對象的狀態。
在 這個類中,我們需要,接受事件的方法handleInput(),實時處理當前狀態操作的(如move狀態就是處理人物移動)Update(),注意我們 並沒有繼承於 MonoBehaviour,所以這里的Update()不是繼承於MonoBehaviour的,是我們自己定義的,之后要放在人物發送類中的 MonoBehaviour的Update()中執行 。進入狀態時的操作Start(),退出狀態時的操作Exit()(根據需要可加可不加)
首先我們需要定義一個父類State,讓各種狀態都繼承於他,我們保存人物發送類和操作類的對象,便於在狀態中update()里對人物的控制。chargeTime上面講過,就是一個狀態的持續時間。
using UnityEngine; using System.Collections; public class State { protected static string input; protected GameObject Person; protected Hero Person_ctrl; protected float chargeTime; protected float MaxTime; public State(GameObject _Person) { this.Person = _Person; this.Person_ctrl = _Person.GetComponent<Hero>(); } public virtual void handleInput(object sender, InputEventArgs e) { } public virtual void UpDate() { } public virtual void UpDate() { } public virtual void Start() { } }
Start()函數是在上一個狀態結束,開始下一個狀態時調用的函數,之調用一次,UpDate()是要放在人物發送類中的UpDate()中實時運行,handleInput在玩家輸入按鍵時被調用處理,負責轉換狀態,我們把該狀態中對應的所有按鍵的判斷放在里面。
然后在來看看子類攻擊狀態
using UnityEngine; using System.Collections; public class ActState : State { public ActState(GameObject _Person) : base(_Person) { } public override void Start() { this.chargeTime = 0.0f; this.MaxTime =..攻擊動畫時間.. ..播放攻擊動畫.. } public override void handleInput(object sender, InputEventArgs e) { input = e.input; switch (input) { case "j"://連擊 if (chargeTime > MaxTime - 0.1f) { Person_ctrl.GetActState(1).Start(); } break; case "i": //轉換為防御狀態 if (chargeTime > MaxTime - 0.1f) { Person_ctrl.SetActState(2); Person_ctrl.GetNowActState().Start(); } break; } } public override void UpDate() { if (chargeTime < MaxTime) chargeTime += Time.timeScale / Time.deltaTime; else { this.chargeTime = 0.0f; Person_ctrl.SetActState(0); Person_ctrl.GetNowActState().Start(); } } }
可以看到,在start函數中也就是狀態開始,刷新chargeTime,播放攻擊動畫,在update函數中,更新時間 chargeTime,判斷是否超過指定時間MaxTime(此處為一次攻擊動畫時間),如果超過則切換當前狀態為空閑,handleInput接收事件 輸入,object sender是事件發送者,就是例子中的Hero類, InputEventArgs e是傳入的按鍵信息,在該函數中判斷是否需要轉換狀態,或者作出相應操作。所以,狀態轉換是在我們封裝的狀態對象中實現的。
我們有兩種方法獲取狀態對象。一種是定義一個狀態類,把狀態聲明為靜態來獲取, 不需要消耗內存來實例化,想要攻擊狀態就AllState. actState就可以,:
using UnityEngine; using System.Collections; public class AllState { public static State actState = new ActState(); public static State jumpState = new JumpState(); ……. }
但是這種方法並不能兩個人物共用,狀態時間chargeTime是無法公用的,這個問題很關鍵,所以,還有一種就是在Hero類里實例化狀態對象,可以實現多任務共用狀態,因為在每個人物類里都有狀態的實例。
private State[] hero_act_state = new State[3]; hero_act_state[0] = new IdleStateActVer(this.gameObject); hero_act_state[1] = new ActState(this.gameObject); hero_act_state[2] = new DefenseState(this.gameObject);
然后再寫一個get,set方法,使狀態變量更加安全,這里不做代碼示范了。
總結
在 有限狀態機中,一般都是觀察者模式與狀態模式連用,狀態模式把每個狀態封裝成類,來對每個輸入消息(按鍵)處理,完全擺脫了大量if else的糾纏,減輕了大量邏輯錯誤出現的可能,但是本身也有很多缺點,因為狀態畢竟是有限的,,當狀態少的時候可以運用自如,當狀態多的時候10個以上 就已經結構非常復雜,而且容易出錯,之前在游戲人工智能開發之6種決策方法提到如果用在AI上會產生一些問題,所以之后會發文講狀態機的進階-分層有限狀態機。
本來是寫狀態模式的,所以不得不提有限狀態機,有限狀態機當然要包括觀察者模式了,所以寫本篇的時候思路有些混亂,委托和事件也只是稍微提了一下。。如果有錯誤,或者是建議請評論或私信。
部分代碼已共享至github
命令模式:游戲開發設計模式之命令模式(unity3d 示例實現)
對象池模式:游戲開發設計模式之對象池模式(unity3d 示例實現)
原型模式:游戲開發設計模式之原型模式 & unity3d JSON的使用(unity3d 示例實現)
博主近期渲染:最近用unity5弄的一些渲染
---- by wolf96 http://blog.csdn.net/wolf96
