或許廣大程序員之前接觸過游戲狀態機,這已不是個新鮮的詞匯了。其重要性我也不必多說了,但今天我要講到的一個狀態機框架或許您以前並未遇到過。所以,我覺得有必要將自己的心得分享一下。下面是一個鏈接:
http://wiki.unity3d.com/index.php/Finite_State_Machine。
接下來我所要講的就是基於此狀態機框架。首先聲明一下,這個狀態機框架並不是我寫的(我現在還沒這個能力呢!),我只是想分享從中得到的一點點感悟,僅此而已。好了,我們開始吧!
首先從此鏈接上映入眼簾的是兩個腳本加一個例子,由於是全英文的,估計大部分人不願意碰這玩意,沒辦法,這就是瓶頸。如果你想更進一步的必須得越過這道坎,這就是核心競爭力!不過現在你不讀也行,因為我會一步一步為您解刨這個狀態機系統的。我想我這是幫人還是害人呢?您認為呢?腳本如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; public enum Transition { //定義了一個Transition(轉換)類型的枚舉變量,所以我們接下來要根據實際情況擴展此枚舉變量。 NullTransition = 0, } public enum StateID { //定義了一個StateId(狀態ID)類型的枚舉變量,所以我們接下來也要根據實際情況擴展此枚舉變量。 NullStateID = 0, } public abstract class FSMState//抽象類,我們必須繼承它才可以在腳本中實例化並使用它 { protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>(); /*這個成員變量是一個Dictionary類型,就相當於java中的Map類型,存儲的是一個個的關聯對。此刻我們存儲的關聯對類型就是上面我們定義的連個枚舉類型。那么接下來我們猜也能才出來我們一定會向其添加關聯對,可能還會移除此關聯對。那么這個東西的用處我們現在還是很迷茫,不要緊,繼續向下看吧!沒問題的。*/ protected StateID stateID; public StateID ID { get { return stateID; } } public void AddTransition(Transition trans, StateID id)//增加關聯對(轉換,狀態ID) { // Check if anyone of the args is invalid if (trans == Transition.NullTransition)//如果增加的轉換是個NullTransition(空轉換),直接Debug.LogError,然后返回 { Debug.LogError("FSMState ERROR: NullTransition is not allowed for a real transition"); return; } if (id == StateID.NullStateID)//如果狀態ID是NullStateID(空狀態ID),怎么辦?還是Debug.LoError,然后返回 { Debug.LogError("FSMState ERROR: NullStateID is not allowed for a real ID"); return; } if (map.ContainsKey(trans))//如果將要增加的關聯對是之前就存在與關聯容器中,也照樣Debug.LogError,之后返回被調用處 { Debug.LogError("FSMState ERROR: State " + stateID.ToString() + " already has transition " + trans.ToString() + "Impossible to assign to another state"); return; } map.Add(trans, id);//沖破了這些阻礙的話,終歸可以添加此關聯對了,下面的DeleteTransition函數就不用我寫注釋了吧! } public void DeleteTransition(Transition trans)//刪除關聯對函數,前提是里面要有這個關聯對啊! { if (trans == Transition.NullTransition) { Debug.LogError("FSMState ERROR: NullTransition is not allowed"); return; } if (map.ContainsKey(trans)) { map.Remove(trans); return; } Debug.LogError("FSMState ERROR: Transition " + trans.ToString() + " passed to " + stateID.ToString() + " was not on the state's transition list"); } public StateID GetOutputState(Transition trans)//此函數由下面這個腳本FSMSystem.cs中的PerformTransition函數調用。是用來檢索狀態的。 { if (map.ContainsKey(trans)) { return map[trans]; } return StateID.NullStateID; } public virtual void DoBeforeEntering() { }//從名字就可以看出它的作用是什么,但是我們得在FSMSystem.cs中得到答案。 public virtual void DoBeforeLeaving() { } public abstract void Reason(GameObject player, GameObject npc); /*這個函數與下面這個函數是這個類中最重要的函數。Reason函數負責監聽環境條件的改變並觸發相應的事件轉換。Act函數的作用在於表現當前狀態下NPC的行為。我們得在這個抽象類的子類中覆寫這兩個方法。 */ public abstract void Act(GameObject player, GameObject npc); }
using UnityEngine; using System.Collections; using System.Collections.Generic; public class FSMSystem { private List<FSMState> states;//此類中植入一個類型為FSMState的List容器 // The only way one can change the state of the FSM is by performing a transition //唯一你可以改變FSM中的狀態的方法是事先一個轉換,這樣講估計有點難以理解,不過我會通過例子來講解的。 // Don't change the CurrentState directly 不要直接修改CurrentState的值。 private StateID currentStateID ; public StateID CurrentStateID { get { return currentStateID; } }//記住,不要直接修改這個變量,之所以讓他公有是因為得讓其他腳本調用這個變量。 private FSMState currentState;//記錄當前狀態 public FSMState CurrentState { get { return currentState; } }//同上 public FSMSystem() { states = new List<FSMState>();//實例化states。 } public void AddState(FSMState s)//增加狀態轉換對 { if (s == null) { Debug.LogError("FSM ERROR: Null reference is not allowed"); } if (states.Count == 0)
/*第一次添加時必定執行這塊代碼,因為一開始states是空的,並且這塊代碼設置了第一次添加的狀態是默認的當前狀態。這一點讀者一定要理解,不然對於后面的東西讀者會非常困惑的,因為其他地方沒有地方設置運行后默認的當前狀態。*/ { states.Add(s); currentState = s; currentStateID = s.ID;//這里實例化了這兩個成員變量 return; } foreach (FSMState state in states)//排除相同的狀態 { if (state.ID == s.ID) { Debug.LogError("FSM ERROR: Impossible to add state " + s.ID.ToString() + " because state has already been added"); return; } } states.Add(s);//這一句代碼第一次不執行,因為第一次states是空的,執行到上面的if里面后立即返回了 } public void DeleteState(StateID id)//跟據ID來從容器states中定向移除FSMState實例 { if (id == StateID.NullStateID) { Debug.LogError("FSM ERROR: NullStateID is not allowed for a real state"); return; } foreach (FSMState state in states) { if (state.ID == id) { states.Remove(state); return; } } Debug.LogError("FSM ERROR: Impossible to delete state " + id.ToString() + ". It was not on the list of states"); } public void PerformTransition(Transition trans)//執行轉換 { if (trans == Transition.NullTransition) { Debug.LogError("FSM ERROR: NullTransition is not allowed for a real transition"); } // Check if the currentState has the transition passed as argument StateID id = currentState.GetOutputState(trans);//這下我們得回到當初我所說講到的FSMState.cs中的那個檢索狀態的函數。如果檢索不出來,就返回NullStateId,即執行下面if語句。 if (id == StateID.NullStateID) { Debug.LogError("FSM ERROR: State " + currentStateID.ToString() + " does not have a target state " + " for transition " + trans.ToString()); return; } currentStateID = id;//還是那句話,如果查到了有這個狀態,那么我們就將其賦值給成員變量currentStateID。 foreach (FSMState state in states)//遍歷此狀態容器 { if (state.ID == currentStateID) { currentState.DoBeforeLeaving();//我們在轉換之前或許要做點什么吧!,所以我們如有需要,得在FSMState實現類中覆寫一下這個方法 currentState = state;//好了,做完了轉換之前的預備工作(DoBeforeLeaving),是時候該轉換狀態了 currentState.DoBeforeEntering();//狀態轉換完成之后,有可能得先為新狀態做點事吧,那么我們也得DoBeforeEntering函數 break; } } } }
我想大家對此腳本已有了一定的理解了,但是估計還不知道怎么用吧!我給的鏈接上有一個Example例子,但是光看這個要想想熟練運用這個狀態機系統確實得花一番心思。所以我來一步一步地解剖這個例子:
using System; using System.Collections.Generic; using System.Text; using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class NPCControl : MonoBehaviour { public GameObject player;//主角 public Transform[] path;//多個尋路點 private FSMSystem fsm;//內置一個fsm public void SetTransition(Transition t) //轉換狀態 { fsm.PerformTransition(t); } public void Start() { MakeFSM();//首先初始化狀態機,執行MakeFSM函數 } public void FixedUpdate()//作為驅動源 { fsm.CurrentState.Reason(player, gameObject);//定期(默認是0.02秒,在Edit->rojectSetting->Time中可以發現)調用當前FSMState中的Reason函數,用以檢測外界環境是否發生變化,並且根據發生的變化來執行某些事件 fsm.CurrentState.Act(player, gameObject);//定期執行當前狀態下的某些行為 } // The NPC has two states: FollowPath and ChasePlayer // If it's on the first state and SawPlayer transition is fired, it changes to ChasePlayer // If it's on ChasePlayerState and LostPlayer transition is fired, it returns to FollowPath private void MakeFSM() { FollowPathState follow = new FollowPathState(path);//定義並實例化FSMState follow.AddTransition(Transition.SawPlayer, StateID.ChasingPlayer);//向其添加轉換對 ChasePlayerState chase = new ChasePlayerState(); chase.AddTransition(Transition.LostPlayer, StateID.FollowingPath); //我畫一張圖,你們就明白了這句話了:
那個實心的箭頭代表的代碼就是上面圓角矩形里面的代碼。看了之后我們因該明白了那兩句代碼的現實意義了吧!即定義轉換,也就是floow狀態可以與chase互相轉換,如果我們填充的狀態中出現了別的狀態比如說:state0,此時狀態floow就不能轉換到state0了,同樣state0也無法轉換到floow。
***************************************************
fsm = new FSMSystem();//實例化fsm fsm.AddState(follow);//將follow裝載到fsm中 fsm.AddState(chase);//將chase裝載到fsm中 } } public class FollowPathState : FSMState /*繼承抽象類FSMState,但是得注意一點:我們得在抽象類FSMState腳本中的兩個枚舉變量分別加入對應的枚舉變量,比如在Transition中加入SawPlayer,LostPlayer;在StateID中加入ChasingPlayer,FollowingPath。*/ { private int currentWayPoint; private Transform[] waypoints; public FollowPathState(Transform[] wp) { waypoints = wp; currentWayPoint = 0; stateID = StateID.FollowingPath; } public override void Reason(GameObject player, GameObject npc) { // If the Player passes less than 15 meters away in front of the NPC RaycastHit hit; if (Physics.Raycast(npc.transform.position, npc.transform.forward, out hit, 15F)) { if (hit.transform.gameObject.tag == "player") npc.GetComponent<NPCControl>().SetTransition(Transition.SawPlayer);//當射線射到的物體的標簽為Player時,觸發轉換。 } } public override void Act(GameObject player, GameObject npc)//當NPC當前狀態為follow時不斷執行以下行為。下面那個類的用法也是一樣的。 { // Follow the path of waypoints // Find the direction of the current way point Vector3 vel = npc.rigidbody.velocity; Vector3 moveDir = waypoints[currentWayPoint].position - npc.transform.position; if (moveDir.magnitude < 1) { currentWayPoint++; if (currentWayPoint >= waypoints.Length) { currentWayPoint = 0; } } else { vel = moveDir.normalized * 10; // Rotate towards the waypoint npc.transform.rotation = Quaternion.Slerp(npc.transform.rotation, Quaternion.LookRotation(moveDir), 5 * Time.deltaTime); npc.transform.eulerAngles = new Vector3(0, npc.transform.eulerAngles.y, 0); } // Apply the Velocity npc.rigidbody.velocity = vel; } } // FollowPathState public class ChasePlayerState : FSMState//同上。 { public ChasePlayerState() { stateID = StateID.ChasingPlayer; } public override void Reason(GameObject player, GameObject npc) { // If the player has gone 30 meters away from the NPC, fire LostPlayer transition if (Vector3.Distance(npc.transform.position, player.transform.position) >= 30) npc.GetComponent<NPCControl>().SetTransition(Transition.LostPlayer); } public override void Act(GameObject player, GameObject npc) { // Follow the path of waypoints // Find the direction of the player Vector3 vel = npc.rigidbody.velocity; Vector3 moveDir = player.transform.position - npc.transform.position; // Rotate towards the waypoint npc.transform.rotation = Quaternion.Slerp(npc.transform.rotation, Quaternion.LookRotation(moveDir), 5 * Time.deltaTime); npc.transform.eulerAngles = new Vector3(0, npc.transform.eulerAngles.y, 0); vel = moveDir.normalized * 10; // Apply the new Velocity npc.rigidbody.velocity = vel; } }
我來總結一下,此狀態機框架的用法如下:首先我們得填充抽象類FSMState中的兩個枚舉類型,然后針對具體情況繼承此抽象類並設計腳本,且腳本中必須有一個FSMSystem類型成員變量(可以仿照上面的例子),並且要在Update或FIxedUpdate等函數中不斷驅動此狀態機運行。且首先我們得用一些FSMState實例來裝載此狀態機系統實例。而且我們得對每一個FSMState實例添加轉換對,控制該狀態轉換的方向。最后在每個FSMState子類中覆寫Reson與Act函數。其中Reson是監聽外界條件變化的並且執行某些轉換,而Act是表現當前狀態行為的函數。
了解了這些,你還覺得自己不會用這個FSMSystem嗎?多用用就好了,下次見!
原文鏈接:http://www.narkii.com/club/thread-272375-1.html