天天看點

Unity3D-FSM有限狀态機的簡單設計

在之前的文章裡介紹了一個基礎U3D狀态機架構(Unity3D遊戲開發之狀态流架構)即大Switch的枚舉狀态控制。這種方法雖然容易了解,程式設計方法也相對簡單,但是弊端是當狀态變得複雜之後,或需要添加一種新的狀态時,會顯得非常混亂并且難以下手。故我們需要引進一種更進階的狀态機技術來避免這些問題。網上有一些講述U3D-FSM狀态機的文章,但都不針對基礎講解,而且大多帶有備援的與狀态機不相關的代碼,基礎不好的讀者容易看不清FSM狀态機的核心所在。這裡針對網上的一些文章和代碼做了一個整理,意圖使之簡單易懂。

這裡關于FSM有限狀态機這類名詞的解釋這裡就不再說明了,感興趣的朋友可以自己去百度下(度娘連結),本文隻說重點。

首先是狀态機基類State.cs

/**
 * 狀态基類 
 */
public class State[entity_type>
{
    public entity_type Target;
    //Enter state  
    public virtual void Enter (entity_type entityType)
    {
        
    }
    //Execute state
    public virtual void Execute (entity_type entityType)
    {
        
    }
    //Exit state
    public virtual void Exit (entity_type entityType)
    {
        
    }

}
           

基類之是以設計成含有3個小的狀态方法是因為,通常在遊戲中有些行為都隻是在進入或退出某個狀态時出現的,并不會發生在通常的更新步驟中。這樣設計就可以有效的将持續性調用語句和一次性調用語句有效的區分開來。(舉例:發送技能時的特效,有些是持續性而有些又是一次性的)

接下來我們編寫狀态機代碼,來使直接的這個基類的各個方法運作起來:

using UnityEngine;
using System.Collections;

public class StateMachine[entity_type>
{
    private entity_type m_pOwner;

    private State[entity_type> m_pCurrentState;//目前狀态
    private State[entity_type> m_pPreviousState;//上一個狀态
    private State[entity_type> m_pGlobalState;//全局狀态

    /*狀态機構造函數*/
    public StateMachine (entity_type owner)
    {
        m_pOwner = owner;
        m_pCurrentState = null;
        m_pPreviousState = null;
        m_pGlobalState = null;
    }
    
    /*進入全局狀态*/
    public void GlobalStateEnter()
    {
        m_pGlobalState.Enter(m_pOwner);
    }
    
    /*設定全局狀态*/
    public void SetGlobalStateState(State[entity_type> GlobalState)
    {
        m_pGlobalState = GlobalState;
        m_pGlobalState.Target = m_pOwner;
        m_pGlobalState.Enter(m_pOwner);
    }
    
    /*設定目前狀态*/
    public void SetCurrentState(State[entity_type> CurrentState)
    {
        m_pCurrentState = CurrentState;
        m_pCurrentState.Target = m_pOwner;
        m_pCurrentState.Enter(m_pOwner);
    }

    /*Update*/
    public void SMUpdate ()
    {

        if (m_pGlobalState != null)
            m_pGlobalState.Execute (m_pOwner);
        
        if (m_pCurrentState != null)
            m_pCurrentState.Execute (m_pOwner);
    }

    /*狀态改變*/
    public void ChangeState (State[entity_type> pNewState)
    {
        if (pNewState == null) {
            Debug.LogError ("can't find this state");
        }
        
                //觸發退出狀态調用Exit方法
        m_pCurrentState.Exit(m_pOwner);
        //儲存上一個狀态 
        m_pPreviousState = m_pCurrentState;
        //設定新狀态為目前狀态
        m_pCurrentState = pNewState;
        m_pCurrentState.Target = m_pOwner;
        //進入目前狀态調用Enter方法
        m_pCurrentState.Enter (m_pOwner);
    }

    public void RevertToPreviousState ()
    {
        //切換到前一個狀态
        ChangeState (m_pPreviousState);
        
    }

    public State[entity_type> CurrentState ()
    {
        //傳回目前狀态
        return m_pCurrentState;
    }
    public State[entity_type> GlobalState ()
    {
        //傳回全局狀态
        return m_pGlobalState;
    }
    public State[entity_type> PreviousState ()
    {
        //傳回前一個狀态
        return m_pPreviousState;
    }

}
           

這個狀态機其實還不是最簡的,全局和上一個狀态的相關部分都可以去掉,但同時功能上就會被削減,故這裡将其保留。

現在狀态基類和狀态機類都有了,我們可以開始編寫遊戲對象的獨立狀态類,先編寫遊戲的總流程狀态類,這裡命名為MainState.cs

/**
 * 全局狀态
 */
public class MainState : State[Main>
{

  
    public static MainState instance;

    /*構造函數單例化*/
    public static MainState Instance()
    {
        if (instance == null)
            instance = new MainState();

        return instance;
    }


    public override void Enter(Main Entity)
    {
        //這裡添加進入此狀态時執行的代碼
    }

    public override void Execute(Main Entity)
    {
        //這裡添加持續此狀态重新整理代碼
    
    }

    public override void Exit(Main Entity)
    {
        //這裡添加離開此狀态時執行代碼
    }

}



/**
 * Ready狀态
 */
public class MainState_Ready : State[Main>
{

    public static MainState_Ready instance;

    /*構造函數單例化*/
    public static MainState_Ready Instance()
    {
        if (instance == null)
            instance = new MainState_Ready();

        return instance;
    }


    public override void Enter(Main Entity)
    {
        //這裡添加進入此狀态時執行的代碼
    }

    public override void Execute(Main Entity)
    {
        //這裡添加持續此狀态重新整理代碼
        //這裡是重點 當滿足某條件後 我們可以進行狀态切換 執行如下代碼 切換到 Run狀态
        Entity.GetFSM().ChangeState(MainState_Run.Instance());  
    }
    public override void Exit(Main Entity)
    {
        //這裡添加離開此狀态時執行代碼
    }
}


/**
 * Run狀态
 */
public class MainState_Run : State[Main>
{
    public static MainState_Run instance;
    /*構造函數單例化*/
    public static MainState_Run Instance()
    {
        if (instance == null)
            instance = new MainState_Run();
        return instance;
    }

    public override void Enter(Main Entity)
    {
        //這裡添加進入此狀态時執行的代碼
    }

    public override void Execute(Main Entity)
    {
        //這裡添加持續此狀态重新整理代碼
        //當滿足某條件後 我們可以繼續進行狀态切換 執行如下代碼 切換到 Over狀态
        Entity.GetFSM().ChangeState(MainState_Over.Instance()); 
    }

    public override void Exit(Main Entity)
    {
        //這裡添加離開此狀态時執行代碼
    }
}

/**
 * Over狀态
 */
public class MainState_Over : State[Main>
{
    public static MainState_Over instance;
    /*構造函數單例化*/
    public static MainState_Over Instance()
    {
        if (instance == null)
            instance = new MainState_Over();
        return instance;
    }

    public override void Enter(Main Entity)
    {
        //這裡添加進入此狀态時執行的代碼
    }

    public override void Execute(Main Entity)
    {
       //這裡添加持續此狀态重新整理代碼
       //如之前兩個狀态類一樣 同理 當滿足一定狀态後 可以切換回Ready狀态
       Entity.GetFSM().ChangeState(MainState_Ready.Instance()); 
    }

    public override void Exit(Main Entity)
    {
        //這裡添加離開此狀态時執行代碼
    }
}
           

代碼有點長,主要是為了讓大家能夠看清楚如何進行一個狀态的編寫,其實基類都是一樣的,都是重複内容。

這裡我們看到,除了定義一個全局的狀态類之外,我們還添加了Ready、Run、Over三個狀态。重點注意一下Execute函數,這裡是狀态切換的關鍵,當帶此狀态綁定的對象Update時就在不停的執行Execute裡的代碼段,當滿足一定條件後,即達成狀态的切換。

這裡我們看一下之前的狀态機代碼裡的ChangeState方法,就知道整個狀态切換是如何工作的了:

/*狀态改變*/
    public void ChangeState (State[entity_type> pNewState)
    {
        if (pNewState == null) {
            Debug.LogError ("can't find this state");
        }
        
        //觸發退出狀态調用Exit方法
        m_pCurrentState.Exit(m_pOwner);
        //儲存上一個狀态 
        m_pPreviousState = m_pCurrentState;
        //設定新狀态為目前狀态
        m_pCurrentState = pNewState;
        m_pCurrentState.Target = m_pOwner;
        //進入目前狀态調用Enter方法
        m_pCurrentState.Enter (m_pOwner);
    }
           

可以看到當狀态切換時,會自動觸發目前狀态的Exit方法和目标狀态的Enter方法。這樣就完成了一整個狀态的切換過程。

到這裡整個有限狀态機體系基本就算完工了,剩下的是如何在Main裡進行MainState類的建立及使用,Main.cs代碼如下:

using UnityEngine;
using System.Collections;

public class Main : MonoBehaviour{
    
    StateMachine[Main> m_pStateMachine;//定義一個狀态機

    void Start () {
            
        m_pStateMachine = new StateMachine[Main>(this);//初始化狀态機
        m_pStateMachine.SetCurrentState(MainState_Ready.Instance()); //設定一個目前狀态
        m_pStateMachine.SetGlobalStateState(MainState.Instance());//設定全局狀态
    }
    
    void Update ()
    {   
        m_pStateMachine.SMUpdate();
    }

        /*傳回狀态機*/
    public StateMachine[Main> GetFSM ()
    {
        return m_pStateMachine;
    }
    
}
           

寫到這裡我們整個狀态機的架構及使用流程就基本結束了,這裡要注意幾個問題:

①不要在SetCurrentState()方法調用前,調用ChangeState()方法,否則會出現null對象錯誤,具體原因很簡單,看一下ChangeState()裡的代碼調用了哪些變量就知道了。

②狀态間的通信,這個狀态機其實還是有未完善的地方的,目前狀态間的通知是通過直接調用其他狀态機的ChangeState()方法實作的,這樣勢必要先擷取該對象的腳本,這個功能待完善吧。

③在U3D裡每個遊戲對象初始化并調用Start()方法的時機是不一樣的,是以要注意,開始遊戲時不要直接進入開始狀态,而是要有一個等待态來讓所有的遊戲對象完成Start()方法後再調用這些對象的狀态機。

另外,多個狀态機間的通信,就像上文②中所述那樣,僅僅是通過調用ChangeState()方法來實作,并不是非常完善,是以暫時不做講解,以免誤導大家,待日後有較好解決方案再另行開篇。

此FSM狀态機僅為一個雛形,還有很多功能及優化要做,但對于入門FSM有限狀态機來說,已經實作了其最主要的功能。不足之處歡迎大家提出讨論,并幫助加以完善。

轉載自:http://coder.beitown.com/archives/592

繼續閱讀