LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

游戏编程模式③(单例模式,状态模式)

2021/8/19

本文作为《Game Programming Pattern》(游戏编程原理)的摘要总结,记录为期一个月的阅读感想与心得体会。

撰写本文时,希望可以尽量遵循star法则,此后的文章同样以这四个要点作为大纲。

situation(情境)

task(任务)

action(行为)

result(结果)

本文代码尽量以unity c#进行演示(最近看的都是cpp代码,时常有被带偏的情况,本人实在太菜菜子了)


单例模式

确保一个类只有一个实例,并为其提供一个全局访问入口

一些情况下一个类如果有多个实例,可能就不能正常运作。


situation

某个类用于保存一些持久化的数据,同时得保证它只有一个实例。最常见的情况是这个类与一个维护着自身全局状态的外部系统交互的情况。

比如一个封装了底层文件api的类,或者是在unity中使用各种 Manager 类用于管理一些全局的变量和方法,他们都是生命周期永不结束的对象。


task

需要只存在一个实例,提供一个对外访问的接口,使得全局可对该单例的唯一实例进行访问。该单例自行进行实例化。


action

public class Single
{
    public static Single GetInstance()
    {
        static Single Instance = new Single();
        return Instance
    }
}

不难看出,单例只在第一次访问它的时候被初始化,如果不使用就永远不会初始化

这多少节省了一点空间。


result

若能继承单例,就能把耦合的代码封装起来,这个特性值得好好利用。

单例模式同样也有不少的问题。甚至很多时候是利大于弊的。

过度使用单例模式会使代码的耦合度很高,后续拓展会变得很麻烦。这是因为单例是一个全局变量,这同样使得代码的可读性变低(需要在代码库里面翻找哪部分访问了全局状态)

而且对于并发并不友好,在多线程、高并发的情况下,可能同时产生多个实例,违背了单例模式。需要提到的是,unity中只有一个主线程和多个辅助线程,不大需要考虑多线程并发问题。


状态模式

允许一个对象在内部状态改变时改变自身的行为,对象看起来好像是在修改自身类。

状态模式的经典应用就是状态机啦,相信大家都很熟悉。


situation

有一定的分支结构,并且这些分支决定于对象的状态时。

因为每产生一个新的状态时,我们都要增加或修改代码来应对需求变化,这违背了开闭原则,不利于程序扩展。

我们使用状态模式,把判断逻辑抽离出来,放到一系列状态类中,这样既符合了开闭原则,又简化了臃肿的判断逻辑。


task

枚举可能的状态,在枚举状态之前需要确定状态种类。

将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。

允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。


action

之前写的最多的就是角色控制器,当状态不同时就修改自身的行为,这次试点新花样。

先看看GoF中状态模式的结构吧:

Context(状态拥有者):持有状态属性的类,可以通过操作改变状态,有request()和setstate()两个方法。

State(抽象状态类):具体状态的抽象类,用来解除状态拥有者和具体状态之间的耦合,定义了一个接口用来封装状态拥有者中的状态对应的行为。

ConcreteState(具体状态类):继承自抽象状态类,用来实现特定状态下应该拥有的行为。

我们设想一个场景切换系统,先编写抽象状态类SceneState,定义定义了场景转换和执行时所需的方法。

public abstract class SceneState
{
    // 状态名称
    private string m_StateName = "SceneState";
    public string StateName
    {
        get{ return m_StateName; }
        set{ m_StateName = value; }
    }

    // 状态拥有者
    protected SceneStateController m_Controller = null;
        
    // 构造
    public SceneState(SceneStateController Controller)
    { 
        m_Controller = Controller; 
    }

    // 开始
    public virtual void StateBegin()
    {
        //在场景跳转成功后,利用这个方法通知类对象,执行该场景中需要加载的资源和游戏参数等设置
    }

    // 結束
    public virtual void StateEnd()
    {
        //在场景被释放时,利用这个方法通知类对象,卸载不再使用的资源等操作。
    }

    // 更新
    public virtual void StateUpdate()
    {
        //这个方法用来执行循环逻辑,并且不必继承MonoBehaviour。
    }

    public override string ToString ()
    {
        return string.Format ("[I_SceneState: StateName={0}]", StateName);
    }

}

我们来定义开始状态类StartState主菜单状态MainMenuState战斗状态BattleState,他们都继承SceneState,实现开始结束更新的方法。

public class StartState : SceneState
{
    public StartState(SceneStateController Controller)
        :base(Controller)
    {
        this.StateName = "StartState";
    }

    // 开始
    public override void StateBegin()
    {
        // 可以在此进行游戏数据的加载和初始化等
    }

    // 更新
    public override void StateUpdate()
    {
        // 转换场景
        m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene");
    }
            
}
--------------------------------------------------------------
    public class MainMenuState : SceneState
{
    public MainMenuState(SceneStateController Controller)
        :base(Controller)
    {
        this.StateName = "MainMenuState";
    }

    // 开始
    public override void StateBegin()
    {
        // 获取开始按钮,加入相关事件
    }
            
    // 开始战斗
    private void OnStartGameBtnClick(Button theButton)
    {
        //Debug.Log ("OnStartBtnClick:"+theButton.gameObject.name);
        m_Controller.SetState(new BattleState(m_Controller), "BattleScene" );
    }
}
--------------------------------------------------------------
    public class BattleState : SceneState
{
    public BattleState(SceneStateController Controller)
        :base(Controller)
    {
        this.StateName = "BattleState";
    }

    // 开始
    public override void StateBegin()
    {
        PBaseDefenseGame.Instance.Initinal();
    }

    // 結束
    public override void StateEnd()
    {
        PBaseDefenseGame.Instance.Release();
    }
            
    // 更新
    public override void StateUpdate()
    {    
        // 游戏逻辑
        PBaseDefenseGame.Instance.Update();
        // Render由Unity負責

        // 游戏是否结束
        if( PBaseDefenseGame.Instance.ThisGameIsOver())
            m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene" );
    }
}

接着我们编写场景状态控制者SceneStateController,对应最上面说的Context(状态拥有者),是执行场景转换的地方,最后在GameLoop中完成创建。

public class SceneStateController
{
    private SceneState m_State;    
    private bool m_bRunBegin = false;

    // 设定状态
    public void SetState(SceneState State, string LoadSceneName)
    {
        m_bRunBegin = false;
        // 载入场景
        LoadScene( LoadSceneName );
        // 通知前一个State結束
        if( m_State != null )
            m_State.StateEnd();
        // 设定
        m_State=State;    
    }
    // 更新
    public void StateUpdate()
    {
        //检查是否还在载入
        if( Application.isLoadingLevel)
            return ;

        // 通知新的State開始
        if( m_State != null && m_bRunBegin==false)
        {
            m_State.StateBegin();
            m_bRunBegin = true;
        }

        if( m_State != null)
            m_State.StateUpdate();
    }
}

最后是最外层的游戏主循环GameLoop

public class GameLoop : MonoBehaviour 
{
    // 场景状态持有者
    SceneStateController m_SceneStateController = new SceneStateController();

    // 
    void Awake()
    {
        // 保证场景切换时不会被删除
        GameObject.DontDestroyOnLoad( this.gameObject );         
    }

    // Use this for initialization
    void Start () 
    {
        // 设定起始场景
        m_SceneStateController.SetState(new StartState(m_SceneStateController), "");
    }

    // Update is called once per frame
    void Update () 
    {
        m_SceneStateController.StateUpdate();    
    }
}

我们来盘一下

SceneStateController类中有一个SceneState成员,用来代表当前游戏场景的状态。

在GameLoop的update方法中调用了SceneStateController的StateUpdate方法,以随时更新游戏状态。

我们可以自己设定起始场景,一般设为startstate,这些类在自身运作的同时更新SceneStateController去持有其他的状态类,从而实现状态的更换。

如果我们想增加新的功能,并且在当前提供的场景中无法实现,我们可以增加一个新的场景,加入新场景对应的状态类,并实现相关功能,编写这个场景的相关跳转(什么场景能跳转到他,他能跳转到什么场景)


result

状态模式使用了中间层“抽象类”完成了判断逻辑之间的解耦,满足了开闭原则,使原本臃肿的判断类变成了利于维护和扩展的架构。

我们可以枚举我们需要的状态。

而且多个环境对象共享着一个状态对象,减少了系统中对象的个数。

但天下没有免费的午餐,状态模式同样有着他的缺陷:

新增状态需要修改负责状态转换的源码。修改状态类行为也需要修改对应类的源代码。

代码的结构会变得复杂些。