Unity 运行机制_ide


0.前言

状态模式是游戏开发中频繁应用的一种模式,它在以下情景中会被应用到

  • 角色状态
  • AI状态
  • 账号登陆状态
  • 场景状态
  • 动画机状态

本篇主要采用游戏场景的转换作为示例,来说明状态模式在游戏开发中的应用。

1.场景的转换

在游戏开发中,通常都会设计多个场景,用来分离不同功能的执行,并且每次只有一个场景存在,常用的场景如下

  • 登陆场景:用来负责显示游戏Logo、片头、玩家登陆和注册
  • 主画面场景:用来选择游戏模式、商店入口、玩家信息、物品展示等
  • 战斗场景:用来体验游戏的核心玩法

可以看到以上场景几乎存在于每一个游戏中,如果我们开发一套场景切换系统,是不是就可以达到代码复用的目的呢?

2.状态模式的定义

在GoF中的解释是:

让一个对象的行为随着内部状态的改变而变化,而该对象也是像换了类一样

说实话,笔者是看不太懂,那状态模式又是来解决什么问题的呢?

举个栗子来说明

一人对应多种状态,比如开心、伤心、激动、愤怒等,这些状态受外界的行为刺激产生变化。在传统的编程模式中,我们是用if else或者switch这样的条件判断语句来处理外界行为会导致哪种变化,当每产生一个新的状态时,我们都要增加或修改代码来应对需求变化,这违背了开闭原则,不利于程序扩展。

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

所以总结为人能看懂的版本:

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

3.状态模式的结构

结构如图


Unity 运行机制_抽象类_02


  • 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之间的连线。而触发条件就是当按钮点击。