思考并回答以下问题:
- 外观模式的官方定义是什么?
- 为什么要使用外观模式?
本章涵盖:
- 游戏子功能的整合
- 外观模式
- 外观模式的定义
- 外观模式的说明
- 外观模式的实现说明
- 使用外观模式实现游戏主程序
- 游戏主程序架构设计
- 实现说明
- 使用外观模式的优点
- 实现外观模式时的注意事项
- 外观模式面对变化时
- 结论
游戏子功能的整合
一款游戏要能顺利运行,必须同时由内部数个不同的子系统一起合作完成。在这些子系统中,有些是在早期游戏分析时规划出来的,有些则是实现过程中,将相同功能重构整合之后才完成的。以《P级阵地》为例,它是由下列游戏系统所组成:
- 游戏事件系统(GameEventSystem);
- 兵营系统(CampSystem);
- 关卡系统(StageSystem);
- 角色管理系统(CharacterSystem);
- 行动力系统(APSystem);
- 成就系统(AchievementSystem);
这些系统在游戏运行时会彼此使用对方的功能,并且通知相关信息或传送玩家的指令。另外,有些子系统必须在游戏开始运行前,按照一定的步骤将它们初始化并设置参数,或者游戏在完成一个关卡时,也要按照一定的流程替它们释放资源。
可以理解的是,上面这些子系统的沟通及初始化过程都发生在“内部”会比较恰当,因为对于外界或客户端来说,大可不必去了解它们之间的相关运行过程。如果客户端了解太多系统内部的沟通方式及流程,那么对于客户端来说,就必须与每一个游戏系统绑定,并且调用每一个游戏系统的功能。这样的做法对客户端来说并不是一件好事,因为客户端可能只是单纯地想使用某一项游戏功能而已,但它却必须经过一连串的子系统调用之后才能使用,对于客户端来说,压力太大,并且让客户端与每个子系统都产生了依赖性,增加了游戏系统与客户端的耦合度。
如果要在我们的游戏范例中举一个例子,那么上一章所提到的“战斗状态类(BattleState)”就是一个必须使用到游戏系统功能的客户端。
根据上一章的说明,战斗状态类(BattleState)主要负责游戏战斗的运行,而《P级阵地》在进行一场战斗时,需要大部分的子系统一起合作完成。在实现时,可以先把这些子系统及相关的执行流程全都放在BattleState类之中一起完成:
Listing 1 在战斗状态类中实现所有子系统相关的操作
1 | public class BattleState : ISceneState |
虽然这样的实现方式很简单,但就如本章一开始所说明的,让战斗状态类(BatleState)这个客户端去负责调用所有与游戏玩法相关的系统功能,是不好的实现方式,原因是:
- 从让事情单一化(单一职责原则)这一点来看,BattleState类负责的是游戏在“战斗状态”下的功能执行及状态切换,所以不应该负责游戏子系统的初始化、执行操作及相关的整合工作。
- 以“可重用性”来看,这种设计方式会使得BattleState类不容易转换给其他项目使用,因为BattleState类与太多特定的子系统类产生关联,必须将它们删除才能转换给其他项目,因此丧失可重用性。
综合上述两个原因,将这些子系统从BattleState类中移出,整合在单一类之下,会是比较好的做法。所以,在《P级阵地》中应用了外观模式来整合这些子系统,使它们成为单一接口并提供外界使用。
外观模式
其实,外观模式是在生活中最容易碰到的模式。当我们能够利用简单的行为来操作一个复杂的系统时,当下所使用的接口,就是以外观模式来定义的高级接口。
外观模式的定义
外观模式(Facade)在GoF的解释是:1
为子系统定义一组统一的接口,这个高级的接口会让子系统更容易被使用。
以驾驶汽车为例,当驾驶者能够开着一辆汽车在路上行走,汽车内部还必须由许多的子系统一起配合才能完成汽车行走这项功能,这些子系统包含引擎系统、传动系统、悬吊系统、车身骨架系统、电装系统等,如图1所示。但对客户端(驾驶者)而言,并不需要了解这些子系统是如何协调工作的,驾驶者只需要通过高级接口(方向盘、踏板、仪表盘)就可以轻易操控汽车。
图1 汽车与5大系统的透视图
再以生活中常用的微波炉为例,微波炉内部包含了电源供应系统、微波加热系统、冷却系统、外装防护等,如图2所示。当我们想要使用微波炉加热食物时,只需要使用微波炉上的面板调整火力和时间,按下启动键后,微波炉的子系统就会立即交互合作将食物加热。
图2 微波炉的透视图
所以,外观模式的重点在于,它能将系统内部的互动细节隐藏起来,并提供一个简单方便的接口。之后客户端只需要通过这个接口,就可以操作一个复杂系统并让它们顺利运行。
外观模式的说明
整合子系统并提供一个高级的接口让客户端使用,可以由图3表示。
图3 外观模式的整合作用示意图
参与者的说明如下:
- client(客户端、用户)
从原本需要操作多个子系统的情况,改为只需要面对一个整合后的接口。 - subsystem(子系统)
原本会由不同的客户端(非同一系统相关)来操作,改为只会由内部系统之间交互使用。 - Facade(统一对外的接口)
- 整合所有子系统的接口及功能,并提供高级接口供客户端使用。
- 接收客户端的信息后,将信息传送给负责的子系统。
外观模式的实现说明
从之前提到的一些实例来看,驾驶座位前的方向盘、仪表板,以及微波炉上的面板,都是制造商提供给用户使用的Facade接口,如图4和图5所示。
图4 驾驶座上的方向盘、踏板、仪表板
图5 微波炉与用户
外观模式可以让客户端使用简单的接口来操作一个复杂的系统,并且减少客户端要与之互动的系统数量,让客户端能够专心处理与本身有关的事情。所以,驾驶员不需要了解汽车引擎系统是否已完成调校,只需要注意行车速度及仪表板上是否有红灯亮起;用户在使用微波炉时,也不用了解此时的微波功率是多少瓦,只需要知道放入的容器是否正确、食物是否过熟即可。
使用外观模式实现游戏主程序
游戏开始实现时,就如同本章第一节中的范例一样,先将几个游戏系统写在一个直接使用它们的类中,但随着游戏系统越加越多,会发现这些游戏系统的程序代码占据了整个类。这些游戏系统的初始化设置和流程串接,与使用它们的类完全没有关系,此时就需要将它们移出,并以一个类重新组织。
游戏主程序架构设计
在《P级阵地》中, PBaseDefenseGame就是“整合所有子系统,并提供高级接口的外观模式类”。重新规划后的类结构图如图6所示。
图6 外观模式类的结构图
参与者的说明如下:
- GameEventSystem、CampSystem、……:分别为游戏的子系统,每个系统负责各自应该实现的功能并提供接口。
- PBaseDefenseGame:包含了和游戏相关的子系统对象,并提供了接口让客户端使用。
- BattleState:战斗状态类,即是《P级阵地》中与PBaseDefenseGame互动的客户端之一。
实现说明
在PBaseDefenseGame类中,将子系统定义为类的私有成员,如下:
Listing 2 游戏主要类的实现,将子系统定义为类成员(PBaseDefenseGame.cs)
1 | public class PBaseDefenseGame |
并提供初始化方法,供游戏开始时调用。初始化方法被调用时,各个子系统的对象才会被产生出来:
Listing 3 初始化P-BaseDefense游戏相关设置(PBaseDefenseGame.cs)
1 | public void Initinal() |
再定义出相关的高级接口供客户端使用。而这些PBaseDefenseGame类方法,多数会把接收到的信息或请求转发给相对应的子系统负责。
Listing 4 P-BaseDefense更新(PBaseDefenseGame.cs)
1 | public void Update() |
在战斗状态类(BattleState)中,通过PBaseDefenseGame类提供的接口来操作《P级阵地》的系统运行:
Listing 5 使用PBaseDefenseGame Facade接口沟通的战斗状态类(BattleState.cs)
1 | public class BattleState : ISceneState |
使用外观模式的优点
将游戏相关的系统整合在一个类下,并提供单一操作接口供客户端使用,与当初将所有功能都直接实现在BattleState类中的方式相比,具有以下几项优点:
- 使用外观模式可将战斗状态类BattleState单一化,让该类只负责游戏在“战斗状态”下的功能执行及状态切换,不用负责串接各个游戏系统的初始化和功能调用。
- 使用外观模式使得战斗状态类BattleState减少了不必要的类引用及功能整合,因此增加了BattleState类被重复使用的机会。
除了上述优点之外,外观模式如果应用得当,还具有下列优点:
节省时间
对某些程序设计语言而言,减少系统之间的耦合度,有助于减少系统构建的时间。以C/C++为例,头文件(.h)代表了某一个类所提供的接口,当接口中的方法有所改变时,任何引用到的该头文件(.h)的单元都必须重新编译。以笔者过去的开发经验来说,即便现代的计算机设备越来越进步,仍须花费许多时间在等待编译程序进行编译。
虽然使用C#在Unity3D开发上,不至于发生修改一个文件就让系统重建时间变长的情况,但良好的设计习惯还是有助于其他程序设计语言的使用。
事实上,Unity3D本身提供了不少系统的Facade接口,例如物理引擎、渲染系统、动作系统、粒子系统等。当在Unity3D中使用物理引擎时,只需要在GameObject挂上碰撞组件(Collider)或刚体组件(Rigidbody),并在面板上设置好相关参数之后,GameObject即可与其他物理组件产生反应。另外,通过面板上的材质设置及相关参数调整,也可以轻易得到Unity3D渲染系统反馈的效果。所以,开发者只需要专心在游戏效果和可玩性上,不必再自行开发对象引擎及渲染功能。
易于分工开发
对于一个既庞大又复杂的子系统而言,若应用外观模式,即可成为另一个Facade接口。所以,在工作的分工配合上,开发者只需要了解对方负责系统的Facade接口类,不必深入了解其中的运行方式。例如,今天有一位程序员A告诉你,要使用他写的“关卡系统”时,必须:
①先初始化一个关卡数据List;
②将关卡信息加入;
③设置排序规则;
④最后才能获得关卡信息……。
但另一位程序员B也告诉你,使用他写的“关卡系统”时,只要初始化关卡系统后,就可以马上获得关卡信息 。自然,与程序员B合作时是比较愉快的,因为在使用程序员B的关卡系统时,不必了解每一步的流程是什么,而且也不必编写太多的程序代码与对方的系统连接,进而也会让自已编写的功能更容易维护。所以,为了让系统能够顺利分工开发,将单一系统功能内部所需要的操作流程全部隐藏,不让客户端去操作,可协助开发团队在分工上的任务划分。
增加系统的安全性
隔离客户端对子系统的接触,除了能减少耦合度之外,安全性也是重点之一。这里所说的安全性,指的是系统执行时“意外宕机或出错”的情况。因为有时候子系统之间的沟通和构建程序上会有一定的步骤,例如:某一个功能一定要先通知子系统A将内部功能设置完成后,才能通知子系统B接手完成后续的设置,顺序的错误会让系统初始化失败或导致宕机,所以像这样的程序构建顺序,应该由Facade接口类来完成,而不应该由客户端去实现。
实现外观模式时的注意事项
由于将所有子系统集中在Facade接口类中,最终会导致Facade接口类过于庞大且难以维护。当发生这种情况时,可以重构Facade接口类,将功能相近的子系统进行整合,以减少内部系统的依赖性,或是整合其他设计模式来减少Facade接口类过度膨胀。
例如在本章的实现上,PBaseDefenseGame类虽然隔离了战斗状态类(BattleState)和各游戏子系统之间的操作,但还需要注意的是,PBaseDefenseGame内部子系统之间要如何减少耦合度的问题,在获取游戏服务的唯一对象--单例模式中,将说明如何减少子系统之间的耦合度。
外观模式面对变化时
随着开发需求的变更,任何游戏子系统的修改及更换,都被限制在PBaseDefenseGame这个Facade接口类内。所以,当有新的系统需要增加时,也只会影响PBaseDefenseGame类的定义及增加对外开放的方法,这样就能使项目的变动范围减到最小。
结论
将复杂的子系统沟通交给单一的一个类负责,并提供单一界面给客户端使用,使客户减少对系统的耦合度是外观模式的优点。在本章中,我们利用外观模式实现了PBaseDefenseGame类,所以战斗状态类(BattleState)与各游戏子系统被隔离开了,这样做的好处是显而易见的。除此之外,本章所实现的设计还有哪些应该注意的地方,以及还可以将外观模式应用在游戏设计的哪些地方,分述如下。
与其他模式的合作
在《P级阵地》中,PBaseDefenseGame类使用单例模式(Singleton)来产生唯一的类对象,内部子系统之间则使用中介者模式(Mediator)作为互相沟通的方式,而游戏事件系统(GameEventSystem)是观察者模式(Observer)的实现,主要目的就是要减少PBaseDefenseGame类接口过于庞大而加入的设计。
其他应用方式
- 网络引擎:网络通信是一项复杂的工作,通常包含连线管理系统、信息事件系统、网络数据封包管理系统等,所以一般会用外观模式将上述子系统整合为一个系统。
- 数据库引擎:在游戏服务器的实现中,可以将与“关系数据库”(MySQL、MSSQL等)相关的操作,以一种较为高级的接口隔离,这个接口可以将数据库系统中所需的连线、数据表修改、新增、删除、更新、查询等的操作加以封装,让不是很了解关系数据库原理的设计人员也能使用。