C#常用设计模式(Unity)——游戏场景的转换——状态模式(State)

14 篇文章 2 订阅

此文章原文来源于《设计模式与完美游戏开发》(蔡升达著),笔者只是在学习过程中受益颇多,从而进行了总结,有兴趣的读者可以去阅读原书。

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行为。

游戏服务器连线状态: 网络游戏的客户端,需要处理与游戏服务器的连线状态,一般包含开始连线,连线中,断线等状态,而在不同的状态下,会有不同的封包信息处理方式,需要分别实现。

关卡进行状态: 如果是通关型游戏,进入关卡时通常会分成不同的阶段,包含加载数据,显示关卡信息,倒数通知开始,关卡进行,关卡结束和分数计算,这些不同的阶段可以使用不同的状态类来负责实现。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值