天天看点

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

继续阅读