设计模式与游戏完美开发 (蔡升达 著)

https://github.com/sttsai/PBaseDefense_Unity3D

第1篇 设计模式与游戏设计

第1章 游戏实现中的设计模式

第2章 游戏范例说明

第2篇 基础系统

第3章 游戏场景的转换----状态模式(State) (已看)

第4章 游戏主要类----外观模式(Facade) (已看)

第5章 获取游戏服务的唯一对象----单例模式(Singleton) (已看)

第6章 游戏内各系统的整合----中介者模式(Mediator) (已看)

第7章 游戏的主循环----Game Loop (已看)

第3篇 角色的设计

第8章 角色系统的设计分析 (已看)

第9章 角色与武器的实现----桥接模式(Bridge) (已看)

第10章 角色的属性----策略模式(Strategy)  (已看)

第11章 攻击特效与击中反应----模板方法模式(Template Method)  (已看)

第12章 角色AI----状态模式(State)

第13章 角色系统 (已看)

第4篇 角色的产生

第14章 游戏角色的产生----工厂方法模式(Factory Method) (已看)

第15章 角色的组装----建造者模式(Builder) (已看)

第16章 游戏属性管理功能----享元模式(Flyweight) (已看)

第5篇 战争开始

第17章 Unity3D的界面设计----组合模式(Composite)

第18章 兵营系统及兵营信息显示 

第19章 兵营训练单位----命令模式(Command) (已看)

第20章 关卡设计----责任链模式(Chain of Responsibility) (已看)

第6篇 辅助系统

第21章 成就系统----观察者模式(Observer) (已看)

第22章 存盘功能----备忘录模式(Memento)

第23章 角色信息查询----访问者模式(Visitor)

第7篇 调整与优化

第24章 前缀字尾----装饰模式(Decorator)

第25章 俘兵----适配器模式(Adapter)

第26章 加载速度的优化----代理模式(Proxy)

第8篇 未明确使用的模式

第27章 迭代器模式(Iterator), 原型模式(Prototype) 和解释器模式(Interpreter)

第28章 抽象工厂模式(Abstract Factory)

参考文献

第1章 游戏实现中的设计模式

  1.1 设计模式的起源

  1.2 软件的设计模式是什么?

  1.3 面向对象设计中常见的设计原则

  1.4 为什么要学习设计模式

  1.5 游戏程序设计与设计模式

  1.6 模式的应用与学习方式

  1.7 结论

第2章 游戏范例说明

  2.1 游戏范例

  2.2 GoF的设计模式范例

第3章 游戏场景的转换----状态模式(State)

  3.1 游戏场景

Unity3D是使用场景(Scene)作为游戏运行时的环境.开始制作游戏时,开发者会将游戏需要的素材(3D模型,游戏对象)放到一个场景中,然后编写对应的程序代码,之后只要单击Play按钮,就可以开始运行游戏

笔者过去开发游戏时使用的游戏引擎(Game Engine)或开发框架(SDK, Framework),多数也都存在"场景"的概念,例如

  早期Java Phone的J2ME开发SDK中使用的Canvas类

  Android的Java开发SDK中使用的Activity类

  iOS上2D游戏开发工具Cocos2D中使用的CCScene类

虽然各种工具不见得都使用场景(Scene)这个名词,但在实际上,一样可使用相同的方式来呈现.而上面所列的各个类,都可以被拿来作为游戏实现中"场景"转换的目标

    3.1.1 场景的转换

切分场景的好处

将游戏中不同的功能分类在不同的场景中来执行,除了可以将游戏功能执行时需要的环境明确分类之外,"重复使用"也是使用场景转换的好处之一

本书范例场景的规划

    3.1.2 游戏场景可能的实现方式

Easy Way

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;
        }
    }
}
View Code

缺点

  只要增加一个状态,则所有switch(m_state)的程序代码都需要增加对应的程序代码

  与每一个状态有关的对象, 都必须在SceneManager类中被保留,当这些对象被多个状态共享时,可能会产生混淆,不太容易识别是由哪个状态设置的,造成游戏程序调试上的困难

  每一个状态可能使用不同的类对象,容易造成StageManager类过度依赖其他类,让SceneManager类不容易移植到其他项目中

为了避免出现上述缺点,修正的目标会希望使用一个"场景类"来负责维护一个场景,让与此场景相关的程序代码和对象能整合在一起.这个负责维护的"场景类",其主要工作如下:

  场景初始化

  场景结束后,负责清除资源

  定时更新游戏逻辑单元

  转换到其他场景

  其他与该场景有关的游戏实现

由于在范例程序中我们规划了3个场景,所以会产生对应的3个"场景类",但如何让这3个"场景类"相互合作,彼此转换呢?我们可以使用GoF的状态模式(State)来解决这些问题

  3.2 状态模式(State)

    3.2.1 状态模式(State)的定义

状态模式(State), 在GoF中的解释是:

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

    3.2.2 状态模式(State)的说明

参与者的说明如下:

  Context(状态拥有者)

    是一个具有"状态"属性的类, 可以制定相关的接口, 让外界能够得知状态的改变或通过操作让状态改变

    有状态属性的类, 例如: 游戏角色有潜行, 攻击, 施法等状态; 好友上线, 脱机, 忙碌等状态; GoF使用TCP联网为例, 有已连接, 等待连接, 断线等状态. 这些类中会有一个ConcreteState[X]子类的对象为其成员, 用来代表当前的状态

  State(状态接口类) 

     制定状态的接口, 负责规范Context(状态拥有者)在特定状态下要表现的行为

  ConcreteState(具体状态的类)

    继承自State(状态接口类)

    实现Context(状态拥有者)在特定状态下该有行为.例如, 实现角色在潜行状态时该有的行动变缓, 3D模型变半透明, 不能被敌方角色察觉等行为

    3.2.3 状态模式(State)的实现范例

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;
    }
}

public abstract class State {
    protected Context m_Context = null;
    
    public State(Context theContext) {
        m_Context = theContext;
    }

    public abstract void Handle(int Value);
}

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));
        }
    }
}

void UnitTest() {
    Context theContext = new Context();
    theContext.SetState(new ConcreteStateA(theContext));
    theContext.Request(5);
    theContext.Request(15);
    theContext.Request(25);
    theContext.Request(35);
}

Context.SetState:DesignPattern_State.ConcreteStateA
ConcreteStateA.Handle
ConcreteStateA.Handle
Context.SetState:DesignPattern_State.ConcreteStateB
Context.SetState:DesignPattern_State.ConcreteStateC
Context.SetState:DesignPattern_State.ConcreteStateA
View Code

Context类中提供了一个SetState方法,让外界能够设置Context对象当前的状态,而所谓的"外界", 也可以是由另一个State状态来调用.所以实现上,状态的转换可以有下列两种方式:

  交由Context类本身,按条件在各状态之间转换

  产生Context类对象时, 马上指定初始状态给Context对象,而在后续执行过程中的状态转换则交由State对象负责,Context对象不再介入

笔者在实现时,大部分情况下会选择第2种方式,原因在于:

  状态对象本身比较清楚"在什么条件下, 可以让Context对象转移到下一个State状态".所以在每个ConcreteState类的程序代码中,可以看到"状态转换条件"的判断, 以及设置哪一个ConcreteState对象成为新的状态

  每个ConcreteState状态都可以保持自己的属性值,作为状态转换或展现状态行为的依据,不会与其他的ConcreteState状态混用,在维护时比较容易理解

  因为判断条件及状态属性都被转换到ConcreteState类中,故而可缩减Context类的大小

  3.3 使用状态模式(State)实现游戏场景的转换

    3.3.1 SceneState的实现

参与者如下:

  ISceneState: 场景类的接口, 定义《P级阵地》种场景转换和执行时需要调用的方法

  StartState, MainMenuState, BattleState: 分别对应范例中的开始场景(StartScene), 主画面场景(MainMenuScene)及战斗场景(BattleScene),作为这些场景执行时的操作类

  SceneStateController: 场景状态的拥有者(Context), 保持当前游戏场景状态, 并作为与GameLoop类互动的接口. 除此之外, 也是执行"Unity3D场景转换"的地方

  GameLoop:: 游戏主循环类作为Unity3D与《P级阵地》的互动接口,包含了初始化游戏和定期调用更新操作

    3.3.2 实现说明

public class ISceneState {
    private string m_StateName = "ISceneState";
    public string StateName {
        get { return m_StateName; }
        set { m_StateName = value; }
    }

    protected SceneStateController m_Controller = null;

    public ISceneState(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);
    }
}

public class StartScene: ISceneState {
    public StartScene(SceneStateController Controller): base(Controller) {
        this.StateName = "StartState";
    }

    public override void StateBegin() {

    }

    public override void StateUpdate() {
        m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene");    
    }
}

public class MainMenuState: ISceneState {
    public MainMenuState(SceneStateController Controller): base(Controller) {
        this.StateName = "MainMenuState";
    }

    public override void StateBegin() {
        Button tmpBtn = UITool.GetUIComponent<Button>("StartGameBtn");
        if (tempBtn != null) {
            tmpBtn.onClick.AddListener(()=>OnStartGameBtnClick(tmpBtn));
     }

    public void OnStartGameClick(Button theButton) {
        m_Controller.SetState(new BattleState(m_Controller), "BattleScene");
    }
}

public class BattleScene: ISceneState {
    public BattleScene(SceneStateController Controller): base(Controller) {
        this.StateName = "BattleState";
    }

    public overrride void StateBegin() {
        PBaseDefenseGame.Instance.Initial();
    }

    public ovrride void StateEnd() {
        PBaseDefenseGame.Instance.Update();
    }

    public ovrride void StateUpdate() {
        InputProcess();
        PBaseDefenseGame.Instance.Update();
        if (PBaseDefenseGame.Instance.ThisGameIsOver()) {
            m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene");
        }
    }

    private void InputProcess() {
        //...
    }
}

public class SceneStateController {
    private ISceneState m_State;
    private bool m_bRunBegin = false;
    
    public SceneStateController() { }
    
    public void SetState(ISceneState State, string LoadSceneName) {
         m_bRunBegin = false;

        LoadScene(LoadSceneName);

        if (m_State != null) {
            m_State.StateEnd();
        }

        m_State = State;
    }

    private void LoadScene(string LoadSceneName) {
        if (LoadSceneName == null || LoadSceneName.Length == 0) {
            return;
        }

        Application.LoadLevel(LoadSceneName);
    }

    public void StateUpdate() {
        if (Application.isLoadingLevel) {
            return;
        }

        if (m_state != null && m_bRunBegin == false) {
            m_State.StateBegin();
            m_bRunBegin = true;
        }

        if (m_State != null) {
            m_State.StateUpdate();
        }
    }
}

public class GameLoop: MonoBehavior {
    SceneStateController m_SceneStateController = new SceneStateController();

    void Awake() {
        GameObject.DontDestroyOnLoad(this.gameObject);
        UnityEngine.Random.seed = (int)DateTime.Now.Ticks;
    }

    void Start() {
        m_SceneStateController.SetState(new StartState(m_SceneStateController), "");
    }

    void Update() {
        m_SceneStateController.StateUpdate();
    }
}
View Code

    3.3.3 使用状态模式(State)的优点

使用状态模式(State)来实现游戏场景转换,有下列优点:

  减少错误的发生并降低维护难度

不再使用switch(m_state)来判断当前的状态,这样可以减少新增游戏状态时,因未能检查到所有switch(m_state)程序代码而造成的错误

  状态执行环境单一化

与每一个状态有关的对象及操作都被实现在一个场景状态类下,对程序设计师来说,这样可以清楚地了解每一个状态执行时所需要的对象及配合的类

  项目之间可以共享场景

    3.3.4 游戏执行流程及场景转换说明

  3.4 状态模式(State)面对变化时

随着项目开发进度进入中后期,游戏企划可能会提出新的系统功能来增加游戏内容.这些提案可能是增加小游戏关卡,提供查看角色信息图鉴,玩家排行等功能.当程序人员在分析这些新增的系统需求后,如果觉得无法在现有的场景(Scene)下实现,就必须使用新的场景来完成.而在现有的架构下,程序人员只需要完成下列几项工作:

  在Unity3D编辑模式下新增场景

  加入一个新的场景状态类对应到新的场景,并在其中实现相关功能

  决定要从哪个现有场景转换到新的场景

  决定新的场景结束后要转换到哪一个场景

上述流程,就程序代码的修改而言,只会新增一个程序文件(.cs)用来实现新的场景状态类,并修改一个现有的游戏状态,让游戏能按照需求转换到新的场景状态.除此之外,不需要修改其他任何的程序代码

  3.5 结论

在本章中,我们利用状态模式(State)实现了游戏场景的切换,这种做法并非全然都是优点,但与传统的switch(state_code)相比,已经算是更好的设计.

状态模式(State)的优缺点

使用状态模式(State)可以清楚地了解某个场景状态执行时所需要配合使用的类对象,并且减少因新增状态而需要大量修改现有程序代码的维护成本

《P级阵地》只规划了3个场景来完成整个游戏,算是"产出较少状态类"的应用.但如果状态模式(State)是应用在大量状态的系统时,就会遇到"产生过多状态类"的情况,此时会伴随着类爆炸的问题,这算是一个缺点.不过与传统使用switch(state_code)的实现方式相比,使用状态模式(State)对于项目后续的长期维护效益上,仍然具有优势

与其他模式(Pattern)的合作

在《P级阵地》的BattleScene类实现中,分别调用了PBaseDefenseGame类的不同方法,此时的PBaseDefenseGame使用的是"单例模式(Singleton)",这是一种让BattleScene类方法中的程序代码,可以取得唯一对象的方式.而PBaseDefenseGame也使用了"外观模式(Facade)"来整合PBaseDefenseGame内部的复杂系统,因此BattleScene类不必了解太多关于PBaseDefenseGame内部的实现方式.

状态模式(State)的其他应用方式:

  角色AI: 使用状态模式(State)来控制角色在不同状态下的AI行为

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

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

第4章 游戏主要类----外观模式(Facade)

  4.1 游戏子功能的整合

一款游戏要能顺利运行,必须同时由内部数个不同的子系统一起合作完成.在这些子系统中,有些是在早期游戏分析时规划出来的,有些则是实现过程中,将相同功能重构整合之后才完成的.以《P级阵地》为例,它是由下列游戏系统所组成:

  游戏事件系统(GameEventSystem)

  兵营系统(CampSystem)

  关卡系统(StageSystem)

  角色管理系统(CharacterSystem)

  行动力系统(APSystem)

  成就系统(AchivementSystem)

这些系统在游戏运行时会彼此使用对方的功能,并且通知相关信息或传送玩家的指令.另外,有些子系统必须在游戏开始运行前,按照一定的步骤将它们初始化并设置参数,或者游戏在完成一个关卡时,也要按照一定的流程替它们释放资源

可以理解的是, 上面这些子系统的沟通及初始化过程都发生在"内部"会比较恰当,因为对于外界或客户端来说,大可不必去了解它们之间的相关运行过程.如果客户端了解太多系统内部的沟通方式及流程,那么对于客户端来说,就必须与每一个游戏系统绑定,并且调用每一个游戏系统的功能.这样的做法对于客户端来说并不是一件好事,因为客户端可能只是单纯地想使用某一项游戏功能而已,但它却必须经过一连串的子系统调用之后才能使用,对于客户端来说,压力太大,并且让客户端与每个子系统都产生了依赖性,增加了游戏系统与客户端的耦合度

如果要在我们的游戏范例中举一个例子,那么上一章所提到的"战斗状态类(BattleScene)"就是一个必须使用到的游戏系统功能的客户端

根据上一章的说明,战斗状态类(BattleState)主要负责游戏战斗的运行,而《P级阵地》在进行一场战斗时,需要大部分的子系统一起合作完成.在实现时,可以先把这些子系统及相关的执行流程全都放在BattleState类之中一起完成

public class BattleState: ISceneState {
    private GameEventSystem m_GameEventSystem = null;
    private CampSystem m_CampSystem = null;
    private StageSystem m_StageSystem = null;
    private CharacterSystem m_CharacterSystem = null;
    private APSystem m_ApSystem = null;
    private AchivementSystem m_AchievementSystem =null;

    public GameState(SceneStateController Controller): base(Controller) {
        this.StateName = "GameState";
        InitGameSystem();
    }

    private void InitGameSystem() {
        m_GameEventySystem = new GameEventSystem();
        ...
    }

    private void UpdateGameSystem() {
        m_GameEventSystem.Update();
        ...
    }
}
View Code

虽然这样的实现方式很简单,但就如本章一开始所说明的,让战斗状态类(BattleState)整个客户端去负责调用所有与游戏玩法相关的系统功能,是不好的实现方式,原因是:

  从让事情单一化(单一职责原则)这一点来看,BattleScene类负责的是游戏在"战斗状态"下的功能执行及状态切换,所以不应该负责游戏子系统的初始化,执行操作及相关的整合工作

  以"可重用性"来看,这种设计方式会使得BattleState类不容易转换给其他项目使用,因为BattleState类与太多特定的子系统类产生关联,必须将它们删除才能转换给其他项目,因此丧失可重用性

综合上述两个原因,将这些子系统从BattleState类中移出,整合在单一类之下,会是比较好的做法.所以,在《P级阵地》中应用了外观模式(Facade)来整合这些子系统,使它们成为单一界面并提供外界使用

  4.2 外观模式(Facade)

其实,外观模式(Facade)是在生活中最容易碰到的模式.当我们能够利用简单的行为来操作一个复杂的系统时,当下所使用的接口,就是以外观模式(Facade)来定义的高级接口

    4.2.1 外观模式(Facade)的定义

外观模式(Facade)在GoF的解释是:

"为子系统定义一组统一的接口,这个高级的接口会让子系统更容易被使用"

以驾驶汽车为例,当驾驶者能够开着一辆汽车在路上行走,汽车内部还必须由许多的子系统一起配合才能完成汽车行走这项功能,这些子系统包含引擎系统,传动系统,悬吊系统,车身骨架系统,电装系统等.但对于客户端(驾驶者)而言,并不需要了解这些子系统是如何协调工作的,驾驶者只需要通过高级接口(方向盘,踏盘,仪表盘)就可以轻易操控汽车

以微波炉为例,微波炉内部包含了电源供应系统,微博加热系统,冷却系统,外装防护等.当我们想要使用微波炉加热食物时,只需要使用微波炉上面的面板调整火力和时间,按下启动键后,微波炉的子系统就会立即交互合作将食物加热

所以,外观模式(Facade)的重点在于,它能将系统内部的互动细节隐藏起来,并提供一个简单方便的接口.之后客户端只需要通过这个接口,就可以操作一个复杂系统并让它们顺利运行

    4.2.2 外观模式(Facade)的说明

参与者的说明如下:
  client(客户端,用户)

从原本需要操作多个子系统的情况,改为只需要面对一个整合后的界面

  subSystem(子系统)

原本会由不同的客户端(非同一系统相关)来操作,改为只会由内部系统之间交互使用

  Facade(统一对外的界面)

整合所有子系统的接口及功能,并提供高级界面(或接口)供客户端使用

接收客户端的信息后,将信息传送给负责的子系统

    4.2.3 外观模式(Facade)的实现说明

从之前提到的一些实例来看,驾驶座位前的方向盘,仪表板,以及微波炉上的面板,都是制造商提供给用户使用的Facade界面

  4.3 使用外观模式(Facade)实现游戏主程序

    4.3.1 游戏主程序架构设计

PBaseGameDefenseGame就是"整合所有子系统,并提供高级界面的外观模式类"

参与者说明如下:

  GameEventSystem, CampSystem......: 分别为游戏的子系统,每个系统负责各自应该实现的功能并提供接口

  PBaseDefenseGame: 包含了和游戏相关的子系统对象, 并提供了界面让客户端使用

  BattleScene: 战斗状态类, 即是《P级阵地》中与PBaseDefenseGame互动的客户端之一

    4.3.2 实现说明

PBaseDefenseGame.cs

public class PBaeDefenseGame {
    ...
    private GameEventSystem m_GameEventSystem =null;
    ...
}

public void Initinal() {
    ...
    m_GameEventSystem = new GameEventSystem(this);
    ...
}

public void Update() {
    ...
    m_GameEventSystem.Update();
    ...
}

BattleState.cs

public class BattleState: ISceneState {
    public override void StateBegin() {
        PBaseDefenseGame.Instance.Initinal();
    }

    public override void StateEnd() {
        PBaseDefenseGame.Instance.Release();
    }

    public override void StateUpdate() {
        ...
        PBaseDefenseGame.Instance.Update();
        ...
        if (PBaseDefenseGame.Instance.ThisGameIsOver()) {
            m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuState");
        }
    }
}
View Code

    4.3.3 使用外观模式(Facade)的优点

将游戏相关的系统整合在一个类下,并提供单一操作界面供客户端使用,与当初所有功能都直接实现在BattleScene类中的方式相比,具有以下几项优点

  使用外观模式(Facade)可将战斗状态类BattleState单一化,让该类只负责游戏在"战斗状态"下的功能执行及状态切换,不用负责串接各个游戏系统的初始化和功能调用

  使用外观模式(Facade)使得战斗状态类BattleScene减少了不必要的类引用及功能整合,因此增加了BattleState类被重复使用的机会

除了上述优点之外,外观模式(Facade)如果应用得当,还具有下列优点:

节省时间  

  Unity3D本身提供了不少系统的Facade接口,例如物理引擎,渲染系统,动作系统,粒子系统等.

易于分工开发  

  对于一个既庞大又复杂的子系统而言,若应用外观模式(Facade), 即可成为另一个Facade接口.所以,在工作的分工配合上,开发者只需要了解对方负责系统的Facade接口类,不必深入了解其中的运行方式

增加系统的安全性

  隔离客户端对子系统的接触,除了能减少耦合度之外,安全性也是重点之一

    4.3.4 实现外观模式(Facade)时的注意事项

由于将所有子系统集中在Facade接口类中,最终会导致Facade接口类过于庞大且难以维护,当发生这种情况时,可以重构Facade接口类,将功能相近的子系统进行整合,以减少内部系统的依赖性,或是整合其他设计模式来减少Facade接口类过度膨胀

  4.4 外观模式(Facade)面对变化时

随着开发需求的变更,任何游戏子系统的修改及更换,都被限制在PBaseDefenseGame这个Facade接口类内.所以, 当有新的系统需要增加时,也只会影响PBaseDefenseGame类的定义及增加对外开放的方法,这样就能使项目的变动范围减到最小

  4.5 结论

将复杂的子系统沟通交给单一的一个类负责,并提供单一界面给客户端使用,使客户减少对系统的耦合度是外观模式(Facade)的优点.

与其他模式(Pattern)的合作

  在《P级阵地》中,PBaseDefenseGame类使用单例模式(Singleton)来产生唯一的类对象,内部子系统之间则使用中介者模式(Mediator)作为互相沟通的方式,而游戏事件系统(GameEventSystem)是观察者(Observer)的实现,主要的目的就是减少PBaseDefenseGame类接口过于庞大而加入的设计

其他应用方式

  网络引擎: 网络通信是一项复杂的工作,通常包含连线管理系统,信息事件系统,网络数据封包管理系统等, 所以一般会用外观模式(Facade)将上述子系统整合为一个系统

  数据库引擎: 在游戏服务器的实现中,可以将与"关系数据库"(MySQL, MSSQL等)相关的操作, 以一种较为高级的接口隔离, 这个接口可以将数据库系统中所需的连线,数据表修改,新增,删除,更新,查询等的操作加以封装,让不是很了解关系数据库原理的设计人员也能使用

第5章 获取游戏服务的唯一对象----单例模式(Singleton)

  5.1 游戏实现中的唯一对象

生活中的许多物品都是唯一的,地球是唯一的,太阳是唯一的等.软件设计上也会有唯一的对象的的需求,例如:服务器端的程序只能连接到一个数据库,只能有一个日志产生器,有些世界也是一样的,同时间只能有一个关卡正在进行,只能连线到一台游戏服务器,只能同时操作一个橘色等

  5.2 单例模式(Singleton)

    5.2.1 单例模式(Singleton)的定义

单例模式(Singleton)在GoF中的定义是:

"确认类只有一个对象,并提供一个全局的方法来获取这个对象"

单例模式(Singleton)在实现时,需要程序设计语言的支持.只要具有静态类属性,静态类方法和重新定义类建造者存取层级.3项语句功能的程序设计语言,就可以实现出单例模式(Singleton)

    5.2.2 单例模式(Singleton)的说明

参与者如下:

  能产生唯一对象的类,并且提供"全局方法"让外界可以方便获取唯一的对象

  通常会把唯一的类对象设置为"静态类属性"

  习惯上会使用Instance作为全局静态方法的名称,通过这个静态函数可能获取"静态类属性"

    5.2.3 单例模式(Singleton)的实现范例

public class Singleton {
    public string Name { get; set; }

    private static Singleton _instance;
    
    public static Singleton Instance {
        get {
            if (_instance == null) {
                Debug.Log("产生Singleton");
                _instance = new Singleton();
            }
            return _instance;
        }
    }

    private Single() { }
}

void UnitTest() {
    Singleton.Instance.Name = "Hello";
    Singleton.Instance.Name = "World";
    Debug.Log(Singleton.Instance.Name);
}
View Code

  5.3 使用单例模式(Singleton)获取唯一的游戏服务对象

    5.3.1 游戏服务类的单例模式实现

在《P级阵地》中,因为PBaseDefenseGame类包含了游戏大部分的功能和操作,因此希望只产生一个对象,并提供方便的方法来取用PBaseDefenseGame功能,所以将该类运用单例模式

参与者说明:

  PBaseDefenseGame

    游戏主程序,内部包含了类型为PBaseDefenseGame的静态成员属性_instance,作为该类唯一的对象

    提供使用C# getter实现的静态成员方法Instance,用它来获取唯一的静态成员属性_instance

  BattleScene

    PBaseDefenseGame类的客户端,使用PBaseDefenseGame.Instance来获取唯一的对象

    5.3.2 实现说明

PBaseDefenseGame.cs

public class PBaseDefenseGame {
    private static PBaseDefenseGame _instance;

    public static PBaseDefenseGame Instance {
        get {
            if (_instance == null) {
                _instance = new PBaseDefenseGame();
            }
            return _instance;
        }
    }
    ...
    private PBaseDefenseGame() { }
}

BattleState.cs

public class BattleScene: ISceneState {
    ...
    pubic override void StateBegin() {
        PBaseDefenseGame.Instance.Initinal();
    }
    ...
}

SoldierClickScript.cs

public class SoldierOnClick: MonoBehavior {
    ...
    public void OnClick() {
        PBaseDefenseGame.Instance.ShowSoldierInfo(Solder);
    }
}
View Code

    5.3.3 使用单例模式(Singleton)后的比较

    5.3.4 反对使用单例模式(Singleton)的原因

  5.4 少用单例模式(Singleton)时如何方便地引用到单一对象

让类具有技术功能来限制对象数量

ClassWithCounter.cs

public class ClassWithCounter {
    protected static int m_ObjCounter = 0;
    protected bool m_bEnable = false;

    public ClassWithCounter() {
        m_ObjCounter++;
        m_bEnable = (m_ObjCounter == 1) ? true : false;

        if (m_bEnalbe == false) {
            Debug.LogError("当前对象数[" + m_ObjCounter + "]超过1个!!");
        }

    public void Operator() {
        if (m_bEnable == false) {
            return;
        }
        Debug.Log("可以执行");
    }
}

SingletonTest.cs

void UnitTest_ClassWithCounter() {
    ClassWithCounter pObj = new ClassWithCounter();
    pObj1.Operator();

    ClassWithCounter pObj2 = new ClassWithCounter();
    pObj2.Operator();

    pObj1.Operator();
}
View Code

设置成为类的引用,让对象可以被取用

某个类的功能被大量使用时,可以将这个类对象设置为其他类中的成员,方便直接引用这些类.而这种实现方法是"依赖性注入"的方式之一,可以让被引用的对象不必通过参数传递的方式,就能被类的其他方法引用.按照设置的方式又可以分为"分别设置"和"指定类静态成员"两种

  分别设置

在《P级阵地》中,PBaseDefenseGame是最常被引用的.虽然已经运用了单例模式(Singleton),但笔者还是以此来示范如何通过设置它成为其他类引用的方式,来减少对单例模式的使用

public class PBaseDefenseGame {
    public void Initinal() {
        m_GameEventSystem = new GameEventSystem(this);
    }
}

public abstract class IGameSystem {
    protected PBaseDefenseGame m_PBDGame = null;
    public IGameSystem(PBaseDefenseGame PBDGame) {
        m_PBDGame = PBDGame;
    }
}

public class CampSystem: IGameSystem {

    public CampSystem(PBaseDefenseGame PBDGame): base(PBDGame) {
        Initialize();
    }

    public void ShowCaptiveCamp() {
        m_PBDGame.ShowGameMsg("获得俘兵营");
    }
}
View Code

  指定类的静态成员

A类的功能若需要使用到B类的方法,并且A类在产生其对象时具有下列几种情况:

  1. 产生对象的位置不确定

  2. 有多个地方可以产生对象

  3. 生成的位置无法引用到

  4. 有众多子类

当满足上述情况之一时,可以直接将B类对象设置为A类中的"静态成员属性", 让该类的对象都可以直接使用

// PBaseDefenseGame.cs

public class PBaseDefenseGame {
    public void Initinal() {
        m_StageSystem = new StageSystem(this);
        // 注入其他系统
        EnemyAI.SetStageSystem(m_StageSystem);
    }
}
View Code

举例来说,敌方单位AI类(EnemyAI), 在运行时需要使用关卡系统(StageSystem)的信息,但EnemyAI对象产生的位置是在敌方单位建造者(EnemyBuilder)之下:

EnemyBuilder.cs

public class EnemyBuilder: ICharacterBuilder {
    public override void AddAI() {
        EnemyAI theAI = new EnemyAI(m_BuildParam.NewCharacter, m_BuildParam.AttackPosition);
        m_BuildParam.NewCharacter.SetAI(theAI);
    }
}
View Code

按照"最少知识原则(LKP)",会希望敌方单位的建造者(EnemyBuilder)减少对其他无关类的引用.因此,在产生敌方单位AI(EnemyAI)对象时,敌方单位建造者(EnemyBuilder)无法将关卡系统(StageSystem)对象设置给敌方单位AI,这是属于上述"生成的位置无法引用到"的情况.所以,可以在敌方单位AI(EnemyAI)类中,提供一个静态成员属性和静态方法,让关卡系统(StageSystem)对象产生的当下,就设置给敌方单位AI(EnemyAI)类:

public class EnemyAI: ICharacterAI {
    private static StageSystem m_StageSystem = null;

    public static void SetStageSystem(StageSystem StageSystem) {
        m_StageSystem = StageSystem;
    }

    public ovrride bool CanAttackHeart() {
        m_StageSystem.LoseHeart();
        return true;
    }
}
View Code

  使用类的静态方法

每当增加一个类名称就等同于又少了一个可以使用的全局名称,但如果是在类下增加"静态方法"就不会减少可使用的全局名称数量,而且还能马上增加这个静态类方法的"可视性"----就是全局都可以引用这个静态类方法.如果在项目开发时,不存在限制全局引用的规则,或者已经没有更好的设计方法时,使用"类静态方法"来获取某一系统功能的接口,应该就是最佳的方式了.它有着单例模式(Singleton)的第二个特性:方便获取对象

举例来说,在《P级阵地》中,有一个静态类PBDFactory就是按照这个概念去设计的.由于它在《P级阵地》中负责的是所有资源的产生,所以将其定义为"全局引用的类"并不违反这个游戏项目的设计原则.它的每一个静态方法都负责返回一个"资源生成工厂接口",注意,是"接口",所以在以后的系统维护更新中,是可以按照需求的改变来替换子类而不影响其他客户端:

public static class PBDFactory {
    private static IAssetFactory m_AssetFactory = null;
    
    public static IAssetFactory GetAssetFactory() {
        if (m_AssetFactory == null) {
            if (m_bLoadFromResource) {
                m_AssetFactory = new ResourceAssetFactory();
            } else {
                m_AssetFactory = new RemoteAssetFactory();
            }
        }
        return m_AssetFactory;
    }
}
View Code

  5.5 结论

单例模式(Singleton)的优点是: 可以限制对象的产生数量;提供方便获取唯一对象的方法.单例模式(Singleton)的缺点是容易造成设计思考不周和过度使用的问题,但并不是要求设计者完全不使用这个模式,而是应该在仔细设计和特定的前提之下,适当地采用单例模式(Singleton)

在《P级阵地》中,只有少数地方引用到单例类PBaseDefenseGame,而引用点可以视为单例模式(Singleton)优点的呈现

其他应用方式

  网络在线游戏的客户端,可以使用单例模式(Singleton)来限制连接数,以预防误用而产生过多连接,避免服务器端因此失败

  日志工具是比较不受项目类型影响的功能之一,所以可以设计为跨项目共享使用,此外,日志工具大多使用在调试或重要信息的输出上,而单例模式(Singleton)能让程序设计师方便快速地获取日志工具,所以是个不错的设计方式

第6章 游戏内各系统的整合----中介者模式(Mediator)

  6.1 游戏系统之间的沟通

回顾单一职责原则(SRP)强调的是,将系统功能细分,封装,让每一个类都能各司其职,负责系统中的某一功能.因此,一个分析设计良好的软件或游戏,都是由一群子功能或子系统一起组合起来运行的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值