提示:设计模式与游戏完美开发
目录
前言
状态模式(State)
状态模式(State),在大多数的设计模式书籍中都会提及,它也是游戏程序设计中应用最频繁的一种模式。主要是因为“状态”经常被应用在游戏设计的诸多环节中,包含AI人工智能状态,账号登陆状态,角色状态等。
一、状态模式(State)的定义
状态模式(State),在GoF中的解释是:
“让一个对象的行为随着内部状态的改变而变化而该对象也像是换了类一样”。
如果将GoF对状态模式(State)的定义改以游戏的方式来解释,就像下面这样:
”当奈德丽(对象)由人形变化为兽性状态(内部状态改变)时,他所施展的技能(对象的行为)也会有所变化,玩家此时就像是在操作另一个不同的角色(像是换了类)“。
“奈德丽”是英雄联盟中游戏角色名称。变化外形是他的能力,通过外形的变化,使奈德丽具备了转换为其他形体的能力,而变化为“兽形”是比较常见的游戏设计。当玩家决定施展外形转换能力时,奈德丽会进入“兽形状态”,这时候的奈德丽会以“兽形”来表现其行为,包含移动和攻击施展的方式:当玩家决定转换回人形时,奈德丽会复原为人形状态,继续与游戏世界互动。
所以,变换外形的能力可以看成是奈德丽的一种“内部状态的转换”。通过变化外形的结果,角色表现出另一种行为模式,而这一切的转化过程都可以由奈德丽的内部控制功能来完成,玩家不必理解这个转化过程。但无论怎样改变,玩家操作的角色都是奈德丽,并不会因为他内部状态的转变而有所差异。
但某个对象状态改变时,虽然它“表现的行为”会有所变化,但是对于客户端来说,并不会因为这样的变化,而改变对他的“操作方式”或“信息沟通”的方式。也就是说,这个对象与外界的队形方式不会有任何改变。但是,对象的内部确实会通过“更换状态类对象”的方式来进行状态的转换。当状态对象更换到另一个类时,对象就会通过新的状态类,表现出它在这个状态下该有的行为。但这一切只会发生在对象内部,对于客户端来说,完全不需要了解这些状态转换的过程及对应的方式。
二、状态模式(State)的说明
1.结构图
为了更明确地表达,此图非标准UML类图
2.说明
Context(状态拥有者)
是一个具有“状态”属性的类,可以定制相关的接口,让外界能够得知状态的改变或通过操作让状态改变。
有状态属性的类,例如:游戏角色有潜心、攻击、施法等状态;好友上线、脱机、忙碌等状态;GoF使用TCP联网为例,有已连接、等待连接、断线等状态。这些类中会有一个ConcreteState[X]子类的对象为其成员,用来代表当前的状态。
State(具体状态类)
制定状态的接口,负责规范Context(状态拥有着)在特定状态下要表现的行为。
ConcreteState(具体状态类)
继承自State(状态接口类)
实现Context(状态拥有者)在特定状态下该有的行为,例如,实现角色在潜行状态时该有的行动变缓,3D模型变半透明、不能被敌方角色察觉等行为。
三、状态模式(State)的实现范例
首先定义Context类:
1.Context类
// 持有目前的状态,并将有的讯息传给状态
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状态接口类则用来定义每一个状态该有的行为:
2.State类
// 负责封装当Context处于特定状态时所该展现的行为
public abstract class State
{
protected Context m_Context = null;
public State(Context theContext)
{
m_Context = theContext;
}
public abstract void Handle(int Value);
}
在产生State类对象,可以传入Context类对象,并将其指定给Stater的类成员m_Context让State类在后续的操作中,可以获取Context对象的信息或操作Context对象。然后定义Handle抽象方法,让继承的子类可以重新定义该方法,来呈现各自不同的行为状态。
最后定义3个继承自State类的子类:
3.三个状态类
// 状态A
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));
}
}
// 状态B
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));
}
}
// 状态C
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));
}
}
上述3个子类,都要重新定义父类State的Handle抽象方法,用来表示在各种状态下的行为。在范例中,我们先让它们各自显示不同的信息(代表当前的状态行为),再按照本身状态的行为定义来判断是否要通知Context对象转换到另一个状态。
Context类中提供了一个SetState方法,让外界能够设置Context对象当前的状态,而所谓的“外界”,也可以是由另一个State状态来调用。所以实现上,状态的转换可以有一下两种方式:
交由Context类本身,按条件在各种状态之间转换;
产生Context类对象时,马上指定初始状态给Context对象,而在后续执行过程中的状态转换则交由给State对象负责,Context对象不再介入。
大部分情况下会选择第二种方式,原因在于:
状态对象本身比较清楚“在什么条件下,可以让Context对象转移到另一个State状态“。所以在每个ConcreteState类的程序代码中,可以看到”状态转换条件“的判断,以及设置哪一个ConcreteState对象成为新的状态。
每个ConcreteState状态都可以保持自己的属性值,作为状态转换或展现行为的依据,不会与其他的ConcreteState对象成为新的状态。
每个ConcreteState状态都可以保持自己的属性值,作为状态转换或展现状态行为的依据,不会与其他的ConcreteState状态混用,在维护时比较容易理解。
因为判断条件及状态属性都被转换到ConcreteState类中,故而可缩减Context类的大小。
4个类定义好之后,我们可以通过测试范例来看看客户端程序会怎样利用这个设计:
void Start ()
{
UnitTest();
}
//
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,并传入不同的参数。
从输出的信息可以看到,Context对象的状态由ConcreteStateA按序转换到ConcreteStateB、ConcreteStateC状态,最后回到ConcreteStateA状态。
总结
Context类要有对State的引用,State类也要有对Context的引用,是一种双向引用。
Context中对State的引用是为了在Request方法中调用Handle方法。
State中对Context的引用是为了将Context对象传递给成员,方便成员自己调用Context中的方法,获取或修改状态。