在上一周的读书笔记中,我介绍了设计模式的概念、目的是什么,以及最后的最重要的面向对象的七大原则,在这篇读书笔记中,我要介绍其中的一种设计模式:状态模式。
PS:这本书主要是以一个小游戏《p阵地》作为一个实战例子进行强行拆分,把游戏开发的每一个小部分都套用了一种设计模式,我个人认为这种方式非常好,虽然看着可能很生硬,但是确实是有效果的,但鉴于游戏开发使用Unity引擎及其使用的部分API可能都被弃用,所以我不采用实战代码,而是自己贴上自己理解的模拟代码。
前言
既然设计模式要与游戏开发完美贴合,那么我就先来说说游戏。相信很多人都玩过游戏,就算再不喜欢玩游戏的人,你的手机里面可能都有一个天天消消乐(一款三消游戏),那么平常玩的时候你肯定是没有注意到游戏是怎么开始的。可能是由于手机网络、qq等的普及,你下意识的就是知道,我要玩游戏,首先要用手机绑定账号,然后还有进入选择角色界面,然后进入不同关卡进行游玩。而这些东西在玩家意识中就是一个流程而已,而在我们游戏程序员眼里,这种东西叫做游戏场景,比如登录场景,战斗场景,结算场景,退出游戏场景。一些更复杂的游戏,比如MMORPG(大型多人社交角色扮演在线网游),里面有不同的副本,就可能有多个战斗场景。这就是我们游戏上面所说的当一个游戏比较复杂的时候,我们通常会设计成多个场景,让玩家在几个场景之间转换,某一个场景可能是角色在一个大地图上行走,而另外一个场景则是在地下洞穴探险。本书中的形容非常恰当——“这样的设计方式其实很像是舞台剧的呈现方式,编剧们设计了一幕幕的场景让演员们在其间穿越演出,而每幕之间的差异,可能是在布景摆设或参与演出角色的不同,但对于观众来说,同时间只会看到演员们在某一个场景中的演出。
**eg:拿最简单的“三场景的游戏来讲”,三场景就是:登陆场景、主画面场景、战斗场景。
登陆场景:负责游戏片头、加载游戏数据、出现游戏主画面、等待玩家登陆游戏。
主画面场景:负责进入游戏画面、玩家在主城/主画面中的操作、在地图上打怪掉宝…
战斗场景:负责与玩家组队之后进入副本关卡、挑战王怪…
在游戏场景规划完成后,就可以利用“状态图”将各个场景的关系连接起来,并且说明它们之间的转化条件以及状态转换的流程,如下图所示:
就算一个游戏类型发生改变,游戏的大致流程都不会发生改变。那么我们这样费尽心机的分割场景有啥好处呢?
切分场景的好处
将游戏中不同的功能分类在不同的场景中来执行,除了可以将游戏功能执行时所需要的环境明确进行分类以外,“重复使用”也是使用场景的好处之一。几乎每一款游戏都有登录场景。而在一般的登录场景之中,会实现游戏初始化的功能或者玩家登录游戏的功能。因为这个通用性能,所以大部分的游戏公司都希望这个东西都可以复用在多个项目之中,可以大大减少开发时间,以及降低代码的错误率。(一个已经被使用的功能,和一个正在开发却没有测试过的功能相比,错误率自然是大大降低了)
游戏场景可能的实现方式
我们先来讲一下一般的实现方法代码如下:
public class SceneManager
{
private string m_state = "开始";
public void ChangeScene(string StateName)
{
m_state = StateName;
switch(m_state)
{
//根据m_state更换功能
//如果要更新新的模块就要在这里面添加更多的内容
}
}
}
的确这种常规的写法非常容易让人理解,实现也是非常容易,但是他的缺点也是不容小觑的:
只要增加一个状态,则所有switch(m_stated)的程序代码都需要增加对应的程序代码。
与每一个状态有关的对象,都必须要在SceneManager类中被保留,当这些对象被多个状态共享时候,可能造成混淆,不容易区分是由哪个状态进行设置的(这句话这样来理解,比如在我战斗场景中我可能要和别人聊天,也可能要打开商城场景,这样在这一个主状态——战斗场景中我就引用了多个状态对象,如果一旦场景发生混乱,我们是很不容易找到具体到底是哪个状态设置产生的),造成了程序调试上的困难。
每一个状态,比如战斗状态要调用数值类,动画类,也就是要使用不同的类对象,容易造成SceneManger类过度依赖其他类,让SceneManger类不容易移植到其他项目中。(我曾经练习写斗地主时候,使用了strangloc框架已经是解耦合很强了,都让我头绕晕了,不敢想象,如果这样写会怎样,哈哈)
状态模式
接下来就轮到“主角”——状态模式登场了,在此我引用本书中的话(个人觉得非常好)。
状态模式(state),在GOF(最最最原始的“四人帮”的设计模式)中解释是:
“让一个对象的行为随着内部状态的改变而变化,而该对象也像是换了类一样”。
可能这样理解的不太方便,再一次用到本书中的举例了、
““德鲁伊”是游戏中经常出现的一个角色,他的能力就是变化外形,当德鲁伊(对象)由人形变化为兽形状态(内部状态改变时候),他所施展的技能(对象的行为)也会有所变化,玩家此时就像是在操作另外一个完全不同的角色一样(像是换了类 )”
当某个对象状态改变时,虽然它“表现的行为”会有所变化,但是对于客户端来说,并不会因为这样的变化,而改变对它的“操作方法”或者“信息沟通”的方式。也就是说,这个对象与外界的对应方式不会有任何改变。但是,对象的内部确实会通过“更好状态类对象”的方式进行状态的转换。当状态对象更换到另外一个类时候,对象就会通过新的状态类,表现出它在这个状态下该有的行为。但是这一切只会发生对象内部,对于客户端来说,完全不需要了解这些状态转换的过程及对应的方式。
也就是说,只需要一个类,对外表现方法都不会发生改变,但是他内部就会使用不同的状态类的对象,模拟代码如下:
using System;
/// <summary>
/// 状态模式 把一个对象的多种状态分割开来 与传统的switch相比 各个状态类的属性和行为都是相对独立的代码的独立性很好
/// switch就纯粹把其他状态的类对象放在自己的管理类里面最后会造成混乱 每次增加也不是特别方便
/// 而状态模式就是真正的实现了同一个对象 的多种状态的切换
/// </summary>
namespace 状态模式
{
class Program
{
/// <summary>
/// 对象类
/// </summary>
class Context
{
State m_state = null;//表示当前对象的状态
/// <summary>
/// 使得外部能够访问该对象目前的状态 并且根据当前状态做出操作
/// </summary>
/// <param name="a">状态参数</param>
public void Request(string a)
{
m_state.Handle(a);
}
/// <summary>
/// 使得外部能够设置该对象目前的状态
/// </summary>
/// <param name="state">状态参数</param>
public void setState(State state)
{
Console.WriteLine("当前状态:{0}",state);
m_state = state;
}
}
/// <summary>
/// 状态基类
/// </summary>
abstract class State
{
//状态要对应一个对象 此对象的状态随时在变化
protected Context m_context = null;
//给此对象进行初始化
public State(Context context)
{
m_context = context;
}
//在此状态下的行为
public abstract void Handle(string a);
}
class StateA : State
{
public StateA(Context context) : base(context) { }
/// <summary>
/// A状态的行为
/// </summary>
public override void Handle(string a)
{
Console.WriteLine("boss执行a状态的技能");
if (a == "b")
{
m_context.setState(new StateB(m_context));//生成新的状态 就是把当前对象状态给覆盖了 同一个对象放入不同的状态
}
}
}
class StateB : State
{
public StateB(Context context) : base(context) { }
/// <summary>
/// A状态的行为
/// </summary>
public override void Handle(string a)
{
Console.WriteLine("boss执行b状态的技能");
if (a == "c")
{
m_context.setState(new StateC(m_context));
}
}
}
class StateC : State
{
public StateC(Context context) : base(context) { }
/// <summary>
/// A状态的行为
/// </summary>
public override void Handle(string a)
{
Console.WriteLine("boss执行c状态的技能");
if (a == "a")
{
m_context.setState(new StateA(m_context));
}
}
}
static void Main(string[] args)
{
Context context = new Context();//生成一个具有多状态的对象
context.setState(new StateA(context));//给这个对象一个初始状态A 不然state对象是空
context.Request("b");
context.Request("c");
context.Request("a");
Console.ReadKey();
}
}
}
那么以上就是本次的读书笔记