U3D的有限狀態機系統


或許廣大程序員之前接觸過游戲狀態機,這已不是個新鮮的詞匯了。其重要性我也不必多說了,但今天我要講到的一個狀態機框架或許您以前並未遇到過。所以,我覺得有必要將自己的心得分享一下。下面是一個鏈接:
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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM