此文章原文来源于《设计模式与完美游戏开发》(蔡升达著),笔者只是在学习过程中受益颇多,从而进行了总结,有兴趣的读者可以去阅读原书。
1.场景的转换
当游戏比较复杂的时候,通常会设计多个场景,让玩家在几个场景之间切换,某一个场景可能是角色在一个大地图上行走,另一个场景则可能是在洞穴探险,这样的设计方式类似于舞台剧,一幕幕的呈现,但对于观众来说,同一时间只能看到演员在某一个场景中的演出。
当我们打开一款游戏的时候,可能会遇到以下场景:出现游戏Logo、播放游戏片头、加载游戏数据、出现游戏主画面、等待玩家登录游戏、进入游戏主画面、进入副本打怪······。
我们可以利用“状态图”将各场景的关系连接起来,并且说明他们之间的转换条件以及状态转换的流程,如图所示:
切分场景的好处:将游戏中不同的功能分类在不同的场景中来执行,除了可以将游戏功能执行时需要的环境明确分类之外,"重复使用"也是使用场景转换的好处之一。
范例场景的规划:
2.游戏场景可能的切换方式
public class SceneManager {
private string m_state = "开始";
// 改换场景
public void ChangeScene(string StateName) {
m_state = StateName;
switch (m_state) {
case "菜单":
Application.LoadLevel("MainMenuScene");
break;
case "主场景":
Application.LoadLevel("GameScene");
break;
}
}
// 更新
public void Update() {
switch (m_state) {
case "开始":
// ...
break;
case "菜单":
// ...
break;
case "主场景":
// ...
break;
}
}
}
缺点:
只要增加一个状态,则所有switch(m_state)的程序代码都需要增加对应的程序代码。
与每一个状态有关的对象, 都必须在SceneManager类中被保留,当这些对象被多个状态共享时,可能会产生混淆,不太容易识别是由哪个状态设置的,造成游戏程序调试上的困难。
每一个状态可能使用不同的类对象,容易造成StageManager类过度依赖其他类,让SceneManager类不容易移植到其他项目中。
每一个状态可能使用不同的类对象,容易造成StageManager类过度依赖其他类,让SceneManager类不容易移植到其他项目中。
为了避免出现上述缺点,修正的目标会希望使用一个"场景类"来负责维护一个场景,让与此场景相关的程序代码和对象能整合在一起.这个负责维护的"场景类",其主要工作如下:
场景初始化。
场景结束后,负责清除资源。
定时更新游戏逻辑单元。
转换到其他场景。
其他与该场景有关的游戏实现。
我们可以使用GoF的状态模式(State)来解决这些问题。
3.状态模式(State)的定义
状态模式(State), 在GoF中的解释是:“让一个对象的行为随着内部状态的改变而变化,而该对象也像是换了类一样”。
可以将GoF对状态模式(State)的定义用游戏的方式来解释:
“当德鲁伊(对象)由人性改变为兽性状态(内部改变)时,他所施展的技能(对象的行为)也会有所变化,玩家此时就像是在操作另外一个不同的角色(像是换了类)”。
“德鲁伊”是一种经常出现在RPG游戏中的角色,变换外形是他们的能力。当玩家决定施展外形转换能力时,德鲁伊会进入“兽形状态”,这时德鲁伊会以“兽形”来表现其行为,包含移动和攻击的方式;当玩家决定转换为人形时,德鲁伊会恢复为一般形态。
所以,变换外形的能力可以看成是一种“内部状态的转换”。通过变化外形,角色展现出另外一种行为模式,而这一切的转化过程都是由德鲁伊内部控制功能来完成,玩家不需要去理解这个转化过程。但无论怎么变化,玩家操作的角色都是德鲁伊,并不会因为他内部状态的转变而有所差异。
当某个对象改变状态时,虽然它“表现的行为”有所变化,但是对于客户端来说,并不会因为这样的变化,而改变对他的“操作方法”或“信息沟通”的方式。也就是说,这个对象与外界的对应方式不会有任何改变。但是,对象的内部确实是会通过“更换状态类对象”的方式来进行状态的装换。当状态对象更换到另一个类时,对象就会通过新的状态类,表现出它在这个状态下该有的行为。但这一切只会发生在对象内部,对客户端来说,完全不需要了解这些状态的转换过程以及对应的方式。
4.状态模式(State)的说明
参与者的说明如下:
Context(状态拥有者):
是一个具有"状态"属性的类, 可以制定相关的接口, 让外界能够得知状态的改变或通过操作让状态改变。
有状态属性的类, 例如: 游戏角色有潜行, 攻击, 施法等状态; 好友上线, 脱机, 忙碌等状态; GoF使用TCP联网为例, 有已连接, 等待连接, 断线等状态. 这些类中会有一个ConcreteState[X]子类的对象为其成员, 用来代表当前的状态。
State(状态接口类) :
制定状态的接口, 负责规范Context(状态拥有者)在特定状态下要表现的行为。
ConcreteState(具体状态的类):
继承自State(状态接口类)。
实现Context(状态拥有者)在特定状态下该有行为.例如, 实现角色在潜行状态时该有的行动变缓, 3D模型变半透明, 不能被敌方角色察觉等行为。
5.状态模式(State)的实现范例
首先定义Context类(State.cs):
public class Context {
State m_state = null;
public void Request(int Value) {
m_State.Handle(Value);
}
public void SetState(State theState) {
Debug.Log("Context.SetState:" + theState);
m_state = theState;
}
}
Context类中,拥有一个State属性用来代表当前的状态,外界可以通过Request方法,让Context类呈现当前状态下的行为。SetState方法可以指定Context类当前的状态,而State状态接口类则用来定义每一个状态该有的行为:
定义State类(State.cs):
public abstract class State {
protected Context m_Context = null;
public State(Context theContext) {
m_Context = theContext;
}
public abstract void Handle(int Value);
}
在产生State类对象时,可以传入Context类对象,并将其指定给State的类成员m_Context,让State类在后续的操作中,可以获取Context对象的信息或者操作Context对象。然后定义Handle抽象方法,让继承的子类可以重新定义该方法,来呈现各自不同的状态行为。
最后定义3个继承自State类的子类(State.cs):
public class ConcreteStateA: State {
public ConcreteStateA(Context theContext): base(theContext) {
}
public override void Handle(int Value) {
Debug.Log("ConcreteStateA.Handle");
if (Value > 10) {
m_Context.SetState(new ConcreteStateB(m_Context));
}
}
}
public class ConcreteStateB: State {
public ConcreteStateB(Context theContext): base(theContext) {
}
public override void Handle(int Value) {
Debug.Log("ConcreteStateB.Handle");
if (Value > 20) {
m_Context.SetState(new ConcreteStateC(m_Context));
}
}
}
public class ConcreteStateC: State {
public ConcreteStateC(Context theContext): base(theContext) {
}
public override void Handle(int Value) {
Debug.Log("ConcreteStateC.Handle");
if (Value > 30) {
m_Context.SetState(new ConcreteStateA(m_Context));
}
}
}
上述三个子类,都要重新定义父类State的Handle抽象方法,用来表示各自状态下的行为。在范例中,我们先让他们各自显示不同的信息(代表当前的状态行为),再按照本身状态的行为定义来判断是否要通过Context对象转换到另一个状态。
State的测试范例(StateTest.cs):
void UnitTest() {
Context theContext = new Context();
theContext.SetState(new ConcreteStateA(theContext));
theContext.Request(5);
theContext.Request(15);
theContext.Request(25);
theContext.Request(35);
}
首先产生Context对象theContext ,并立刻设置为ConcreteStateA状态,然后调用Context类的Request方法,并传入作为“状态转换判断”用途的参数,让当前状态(ConcreteStateA)判断是否转换到ConcreteStateB,最后多调用几次Request方法,并传入不同的参数。
State测试范例的结果
Context.SetState:DesignPattern_State.ConcreteStateA
ConcreteStateA.Handle
ConcreteStateA.Handle
Context.SetState:DesignPattern_State.ConcreteStateB
Context.SetState:DesignPattern_State.ConcreteStateC
Context.SetState:DesignPattern_State.ConcreteStateA
6.使用状态模式(State)的优点
使用状态模式(State)来实现游戏场景转换,有下列优点:
减少错误的发生并降低维护难度
不再使用switch(m_state)来判断当前的状态,这样可以减少新增游戏状态时,因未能检查到所有switch(m_state)程序代码而造成的错误。
状态执行环境单一化
与每一个状态有关的对象及操作都被实现在一个场景状态类下,对程序设计师来说,这样可以清楚地了解每一个状态执行时所需要的对象及配合的类。
项目之间可以共享场景
7.状态模式(State)面对变化时
随着项目开发进度进入中后期,游戏企划可能会提出新的系统功能来增加游戏内容.这些提案可能是增加小游戏关卡,提供查看角色信息图鉴,玩家排行等功能.当程序人员在分析这些新增的系统需求后,如果觉得无法在现有的场景(Scene)下实现,就必须使用新的场景来完成.而在现有的架构下,程序人员只需要完成下列几项工作:
在Unity3D编辑模式下新增场景。
加入一个新的场景状态类对应到新的场景,并在其中实现相关功能。
决定要从哪个现有场景转换到新的场景。
决定新的场景结束后要转换到哪一个场景。
上述流程,就程序代码的修改而言,只会新增一个程序文件(.cs)用来实现新的场景状态类,并修改一个现有的游戏状态,让游戏能按照需求转换到新的场景状态.除此之外,不需要修改其他任何的程序代码。
8.结论
在本章中,我们利用状态模式(State)实现了游戏场景的切换,这种做法并非全然都是优点,但与传统的switch(state_code)相比,已经算是更好的设计,使用状态模式(State)可以清楚地了解某个场景状态执行时所需要配合使用的类对象,并且减少因新增状态而需要大量修改现有程序代码的维护成本。
状态模式(State)的其他应用方式:
角色AI: 使用状态模式(State)来控制角色在不同状态下的AI行为。
游戏服务器连线状态: 网络游戏的客户端,需要处理与游戏服务器的连线状态,一般包含开始连线,连线中,断线等状态,而在不同的状态下,会有不同的封包信息处理方式,需要分别实现。
关卡进行状态: 如果是通关型游戏,进入关卡时通常会分成不同的阶段,包含加载数据,显示关卡信息,倒数通知开始,关卡进行,关卡结束和分数计算,这些不同的阶段可以使用不同的状态类来负责实现。