0.前言
状态模式是游戏开发中频繁应用的一种模式,它在以下情景中会被应用到
- 角色状态
- AI状态
- 账号登陆状态
- 场景状态
- 动画机状态
本篇主要采用游戏场景的转换作为示例,来说明状态模式在游戏开发中的应用。
1.场景的转换
在游戏开发中,通常都会设计多个场景,用来分离不同功能的执行,并且每次只有一个场景存在,常用的场景如下
- 登陆场景:用来负责显示游戏Logo、片头、玩家登陆和注册
- 主画面场景:用来选择游戏模式、商店入口、玩家信息、物品展示等
- 战斗场景:用来体验游戏的核心玩法
可以看到以上场景几乎存在于每一个游戏中,如果我们开发一套场景切换系统,是不是就可以达到代码复用的目的呢?
2.状态模式的定义
在GoF中的解释是:
让一个对象的行为随着内部状态的改变而变化,而该对象也是像换了类一样
说实话,笔者是看不太懂,那状态模式又是来解决什么问题的呢?
举个栗子来说明
一人对应多种状态,比如开心、伤心、激动、愤怒等,这些状态受外界的行为刺激产生变化。在传统的编程模式中,我们是用if else或者switch这样的条件判断语句来处理外界行为会导致哪种变化,当每产生一个新的状态时,我们都要增加或修改代码来应对需求变化,这违背了开闭原则,不利于程序扩展。
这时我们便需要状态模式,把判断逻辑抽离出来,放到一系列状态类中,这样既符合了开闭原则,又简化了臃肿的判断逻辑。
所以总结为人能看懂的版本:
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
3.状态模式的结构
结构如图
- Context(状态拥有者):持有状态属性的类,可以通过操作改变状态
- State(抽象状态类):具体状态的抽象类,用来解除状态拥有者和具体状态之间的耦合,定义了一个接口用来封装状态拥有者中的状态对应的行为。
- ConcreteState(具体状态类):继承自抽象状态类,用来实现特定状态下应该拥有的行为。
这里就简单介绍一下GoF中状态模式的结构,主要是为了方便对Unity版本实现的对比和理解,具体的实现方式网上和书上都有详细介绍,这里就不再介绍了。下面让我们来实现一个Unity版本的状态模式——游戏场景的转换。
4.Unity版本的状态模式——场景转换
首先介绍一下会使用到的类的作用
- GameLoop:游戏的主循环,继承自MonoBehavior,包含了游戏的初始化和循环更新操作,同时也是设置起始场景的地方。
- SceneStateController:场景状态的拥有者,对应上面说的Context(状态拥有者),执行场景转换的地方,在GameLoop中完成创建。
- SceneState:场景的抽象类,定义了场景转换和执行时所需的方法。
- StartState、MainMenuState、BattleState:分别对应了开始场景、主界面场景、战斗场景的具体实现类。
首先定义抽象类
// 场景状态抽象类
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()
{}
public override string ToString ()
{
return string.Format ("[I_SceneState: StateName={0}]", StateName);
}
}
- StateBegin:在场景跳转成功后,利用这个方法通知类对象,执行该场景中需要加载的资源和游戏参数等设置。
- StateEnd:在场景被释放时,利用这个方法通知类对象,卸载不再使用的资源等操作。
- StateUpdate:这个方法用来执行循环逻辑,并且不必继承MonoBehaviour。
- m_StateName:可以在Debug时使用
定义开始状态类StartState
// 开始状态
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");
}
}
定义主菜单状态MainMenuState
using UnityEngine.UI;
// 主菜单状态
public class MainMenuState : SceneState
{
public MainMenuState(SceneStateController Controller):base(Controller)
{
this.StateName = "MainMenuState";
}
// 开始
public override void StateBegin()
{
// 取得开始按钮
Button tmpBtn = UITool.GetUIComponent<Button>("StartGameBtn");
if(tmpBtn!=null)
tmpBtn.onClick.AddListener( ()=> OnStartGameBtnClick(tmpBtn) );
}
// 开始战斗
private void OnStartGameBtnClick(Button theButton)
{
//Debug.Log ("OnStartBtnClick:"+theButton.gameObject.name);
m_Controller.SetState(new BattleState(m_Controller), "BattleScene" );
}
}
定义战斗状态BattleState
// 战斗状态
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
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
// 场景状态控制者
public class SceneStateController
{
private SceneState m_State;
private bool m_bRunBegin = false;
public SceneStateController()
{}
// 设定状态
public void SetState(SceneState State, string LoadSceneName)
{
//Debug.Log ("SetState:"+State.ToString());
m_bRunBegin = false;
// 载入场景
LoadScene( LoadSceneName );
// 通知前一个State結束
if( m_State != null )
m_State.StateEnd();
// 设定
m_State=State;
}
// 载入
private void LoadScene(string LoadSceneName)
{
if( LoadSceneName==null || LoadSceneName.Length == 0 )
return ;
SceneManager.LoadSceneAsync(LoadSceneName);
}
// 更新
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();
}
}
SceneStateController类中有一个SceneState成员,用来代表当前游戏场景的状态。在SetState方法中,实现了转换场景状态的功能。
至于Update方法中,会先判断是否载入完成,成功之后才会调用当前游戏状态的StateBegin方法来初始化游戏场景的状态。
与游戏主循环结合GameLoop
using UnityEngine;
using System;
using System.Collections;
// 游戏主循环
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();
}
}
到此整个场景转换状态模式的实现已经完成了
5.使用方法
随着项目进行到后期,如果我们想增加新的功能,并且在当前提供的场景中无法实现,我们就可以使用新的场景来完成,并且在现有的架构下,我们只需要做以下几步就可以
- 增加一个新的场景
- 加入新场景对应的状态类,并实现相关功能
- 决定从哪个场景可以跳转到新的场景
- 觉得从新场景结束后可以跳转回哪个场景
就代码修改而言,只会新增一个类,并且只会修改一个现有的游戏状态,让其可以按照功能需求跳转到新的场景即可。
6.总结
这篇文章里,我们使用状态模式完成了场景切换的功能,虽然并不是十分完美,但是相比于传统的逻辑判断,已经算是比较好的设计了。在实际开发中,若多种设计模式互相配合,会得到更完美的设计。
在我看来,状态模式使用了中间层“抽象类”完成了判断逻辑之间的解耦,满足了开闭原则,使原本臃肿的判断类变成了利于维护和扩展的架构。
如果使用Unity中的Animator动画状态机来类比状态模式的概念或许大家会更直观:
Animator是状态持有者,里面拥有很多个状态。
每个状态之间都可以连线,这个连线是在各个状态之间产生的,这也就是状态模式所说的内部改变状态。
每个状态之间都可以添加转换条件,当达到条件的时候便进行状态切换,也就是在状态内部设置了转换条件。
让我们来聚焦于主界面场景到战斗场景之间的状态转换
using UnityEngine.UI;
// 主菜单状态
public class MainMenuState : SceneState
{
public MainMenuState(SceneStateController Controller):base(Controller)
{
this.StateName = "MainMenuState";
}
// 开始
public override void StateBegin()
{
// 取得开始按钮
Button tmpBtn = UITool.GetUIComponent<Button>("StartGameBtn");
if(tmpBtn!=null)
tmpBtn.onClick.AddListener( ()=> OnStartGameBtnClick(tmpBtn) );
}
// 开始战斗
private void OnStartGameBtnClick(Button theButton)
{
//Debug.Log ("OnStartBtnClick:"+theButton.gameObject.name);
m_Controller.SetState(new BattleState(m_Controller), "BattleScene" );
}
}
不难看出连线发生在SetState方法,是MainMenuState和BattleState之间的连线。而触发条件就是当按钮点击。