Unity中的有限状态机和有限状态模式

      游戏开发过程中,各种游戏状态的切换无处不在。但很多时候,简单粗暴的if else加标志位的方式并不能很地道地解决状态复杂变换的问题,这时,就可以运用到状态模式以及状态机来高效地完成任务。状态模式与状态机,因为他们关联紧密,常常放在一起讨论和运用。而本文将对他们在游戏开发中的使用,进行一些探讨。

      当然,要我一下讲的很明白,估计也不太可能,还是老样子,我们进行文章和代码分析,最后进行总结。首先我们先整一些小案例来进行相关说明,明白有限状态机有哪些运用,之后根据这些案例进行总结,最后规范unity中有限状态机和有限状态模式的下发以及作用。

案例之一:NPC发现Player发射子弹,简单的if(){ }elseif(){ } 也是一种状态机,只是比较简单

代码如下:


效果如下



案例二:枚举和Switch case的结合,这种模式可以采用,代码比较规整,也比较好理解:

首先用枚举把所有状态列举出来,然后写一个方法,进行switch  case进行每个状态绑定的函数进行列举。然后,对于每个枚举类型绑定的函数进行实际化。然后再Update函数当中进行监听。

其实也可以把Fsm_enum类似的脚本整成一个单例,然后可以进行全局调用。



以上两种是简单的有限状态机。在游戏中人物的状态是不断变化的,所以写一个FSM来管理状态是必要的。一个有限状态机是一个设备,或者是一个设备模型,具有有限数量的状态,它可以在任何给定的时间根据输入进行操作,使得一个状态变换到另一个状态,或者是使一个输入或者一种行为的发生。一个有限状态机在任何瞬间只能处在一种状态。

案例三:常见的状态机设计模式

有限状态机设计的核心原则就是:单一职责原则和里氏替换原则。单一职责就是每一个状态都有专门的一个脚本进行处理他的行为。里氏替换原则:所有具体状态类继承于一个抽象类,这样不管是那个状态实例化的对象,都可以借助基类进行。

大体上状态机模式的实现需要三个要点:1、为所有的状态定义一个接口或者基类别

                                                               2、为每个状态定义一个类

                                                               3、恰当进行状态的委托(关联起来,怎么实现类和方法的调用)

通常来说,状态模式中状态对象的存放有两种实现存放的思路:

1、静态状态,就是初始化的时候把所有可能的状态都new好,状态切换时通过赋值进行改变当前的状态

2、实例化状态,每次切换状态时动态的new 出新的状态

我们来分析下面的例子:有三个场景MainMenuSceneState、BattleScene、StartSceneState。一个基类ISceneState、一个场景控制类SceneStateManager、一个开启游戏(或者说所有脚本的类)GameContext类。脚本代码如下,我在GameContext类中做了代码的逐条分析。

ISceneState基类


BattleScene


MainMenuSceneState


StartSceneState


SceneStateManager


开始游戏场景的一个类,这个类继承于MonoBehaviour,挂载在开始场景的某一个游戏对象上


剩下的就是上面这个案例代码的全部解析

///<summary>
///各个场景要继承的基类
///</summary>
//ISceneState类中的内容
//protected string mSceneName{get; set;}
//public ISceneState(string sceneName){ this.mSceneName = sceneName; }
//在创建ISceneState类的时候,就会给string类型的mSceneName赋值

///<summary>
///各个场景的管理类SceneStateManager
///</summary>
//这个类是一个单例,SceneStateManager.GetInstance()进行获得SceneStateManager类中唯一的一个对象
//privete ISceneState mState{get; set;}   这个是用来赋值的。
//public void SetSceneState(ISceneState state)
//{
//    if (mState != null)  //如果当前场景不为空,就结束当前场景
//    {
//        mState.EndScene();
//    }
//    mState = state;  //同时开始传入要开始的场景   经过这一步mState肯定不为空了
//    if (mState != null)
//    {
//        mState.StartScene();
//    }
//}
//这个SetSceneState()方法中无形之间有了一个循环,当你调用这个方法传入一个ISceneState子类对象时候
//开始判断如果当前mState不为空,就表示已经有场景在运行了。那就当有调用SetScenenState()的时候mState.EndScenen;
//然后再给mState = state 相当于重新赋值
//最后在进行mState.StartScenen

///<summary>
///BattleScene继承ISceneState
///</summary>
//继承有参构造器public BattleScene() : base("BattleScene"){ },当创建BattleScene对象的时候就会给mScenenName赋值
//就可以执行
//public override void StartScene()
//{
//    SceneManager.LoadScene(this.mSceneName)
//}

//同理
///<summary>
///MainMenuSceneState类
///</summary>
//public class MainMenuSceneState : ISceneState
//{
//    public MainMenuSceneState() : base("MainMenuScene")
//    {
//    }
//    public override void StartScene()
//    {
//        SceneManager.LoadScene(this.mSceneName);
//    }
//}

///<summary>
///场景开始界面控制脚本StartSceneState
///</summary>
//同样StartSceneState继承ISceneState类,也是一个界面,只是是开始界面,所以有一些功能的集成
//首先还是继承ISceneState这个构造器,只要创建StartSceneState这个类的子类,就会给mSceneName进行赋值
//public StartSceneState():base("StartScene "){ }
//然后在开始界面中添加两个按钮事件    控制界面的转换  重写基累中的StartScene方法
//public override void StartScene()
//{
//    GameObject.FindGameObjectWithTag("Battle").GetComponent<Button>().onClick.AddListener(OnBtnBattle);
//    GameObject.FindGameObjectWithTag("MainMenu").GetComponent<Button>().onClick.AddListener(OnBtnMainMenu);
//}
//按钮点击事件
//private void OnBtnBattle()
//{
//      SceneStateManager.GetInstance().SetSceneSate(new BattleScene())
//}
//private void OnBtnMainMenu()
//{
//      SceneStateManager.GetInstance().SetSceneSate(new MainMenuSceneState())
//}

///<summary>
///创建一个基类调用SceneStateManager类,继承于MonoBehaviour开始调用脚本,挂载到开始场景中的游戏对象上
///开始运行程序
///</summary>
//class GameContext:MonoBehaviour
//{
//      void Start()   //刚一开始就开启StartScene场景
//      {
//            SceneStateManager.GetInstance.SetSceneState(new StartSceneState());
//      }
//      void Update()  //在Update函数函数中进行更细
//      {
//            SceneStateManager.GetInstance.UpdateSceneState();
//            这个更新函数是BattleScene类    StartSceneState类   MainMenuScene类这三个类里面需要重写具体实现的
//      }
//}

     在这个案例中,我们首先定义可一个基类ISceneState类,在这个基类中,定义了一个string类型的属性(mScenenName),以及一个带有string类型参数的构造器。当启动构造器的时候就给属性赋值。此外还包含三个虚函数SartScene(){ }、UpdateScene(){ }、EndScene(){ }。当三个子类MainMenuSceneState、BattleScene、StartSceneState继承以后、首先通过构造器给mSceneName赋值,然后就是可执行切换场景的功能。


       根据之前我们总结的这个案例符合状态模式的实现三个要点,在状态对象存放方式上采用了第二种:即实例化状态,每次切换状态的时候动态的new出来新的状态。这一篇就先到这里,我准备用两个篇幅专门用于解析状态机和状态模式。

猜你喜欢

转载自blog.csdn.net/qq_38651460/article/details/79522138