游戏开发中常用设计模式梳理和总结

本文讲述内容为游戏开发中涉及的常用设计模式,以及个人对这些模式的思考和总结,这些模式来自于前辈们众多项目经验实战总结,因此读者应具备一定的游戏开发经验,方能更好的理解和运用到实际工作中。

状态模式

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

优点:使用状态模式代替if else和switch,方便代码维护,也增加灵活性,同时由于各自的上下文关系都在各自的状态类内部,因此可以充分解耦,方便修改和维护

缺点:状态过多时,会创建很多不同状态的类(类爆炸),状态之间的跳转规则要制定的非常清楚,否则状态一多,自己都会搞不清楚了

注意与策略模式的区别

外观模式

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

外观模式的通俗解释就是封装,把复杂的逻辑及调用顺序封装为一个简单的接口,方便使用者调用,而不需要使用者去关心调用的逻辑和顺序,同时也可以让各系统之间进行解耦,让独立的模块可以在别的项目中复用。

优点:让各模块单一化,增加各模块复用的可能性

缺点:如果将所有子系统统一合并到一个外观模式接口下,会导致外观模式接口过于庞大而难以维护,如果出现这种情况,则需要对外观模式的接口进行重构,将功能相近的子系统进行整合,减少内部的依赖性,另外外观模式接口内部子系统之间如何减少耦合度也是一个需要注意的问题

注意与模板方法模式的区别

单例模式

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

优点:可以限制对象的产生数量,提供方便获取唯一对象的方法

缺点:容易造成设计思考不周和过度使用的问题。

如何避免使用单例模式:

  • 在类的构造函数中使用引用计数,在构造时进行判断,一旦超过1个立即报错
  • 设置为类的引用,让对象可以被取用
  • 使用类的静态方法

中介者模式

GOF定义:定义一个接口用来封装一群对象的互动行为。中介者通过移除对象间的引用,来减少它们之间的耦合度,并且能改变它们之间的互动独立性

主体思路是创建一个中介者类处于中心,所有系统,UI的通信都从中介者这里过一次,让这些系统只和中介者发生联系,而不必相互之间互相联系,增加复杂度

优点:能让系统之间的耦合度降低,提升系统的可维护性。

注意事项:在使用中介者模式时,很容易出现中介者类接口爆炸的情况,必须配合其它模式来进行优化

桥接模式

GOF定义:将抽象与实现分离,使二者可以独立的变化,定义一个接口类,然后将实现的部分在子类中完成。

例如:在角色身上放置一个武器类,角色类和武器类都各自独立实现,两者通过桥接的方式发生联系,这样无论是修改角色类或者武器类都不会牵涉其它的类,充分的解耦

缺点:实例化种类太多时也会需要用switch分支来进行判断,并无法完全的自由增减

策略模式

GOF定义:定义一组算法,并封装每个算法,让它们可以彼此交换使用。策略模式让这些算法在客户端使用它们时能更加独立

把不同的策略独立的实现,载体根据需要随意调用对应的策略,同时让相关逻辑集中在同一个类下管理,有助于后续项目的维护,降低复杂度。同时减少了if else的使用频率。

与状态模式的区别:

  • 状态模式是在一群状态中进行切换,状态之间有对应和连接的关系。策略模式则是由一群没有任何关系的类所组成,不知彼此存在
  • 状态模式受限于状态机的切换规则,在设计初期就会定义所有可能的状态,就算后期追加也需要和现有的状态有所关联,而不是想加入就加入,策略模式是由封装计算算法而形成的一种设计模式,算法之间不存在任何依赖关系,有新增的算法就可以马上加入或替换。

模板方法模式

GOF定义:在一个操作方法中定义算法的流程,其中某些步骤由子类完成。模板方法模式让子类在不变更原有算法流程的情况下,还能够重新定义其中的步骤。

把逻辑流程固化到父类中,分类开放接口给子类实现(强制)

优点:如果要改动流程,只需要修改父类就可以了

缺点:如果开放的接口过多,反而会增加维护难度

难度:抽象出所有子类共有的流程不容易,如果子类流程各有不同,那如何去抽象和归纳非常考验设计经验,没设计好反而影响维护效率和难度。

模板方法模式和外观模式的异同:

  • 相同点:都是封装了一组接口的顺序调用
  • 不同点:模板方法把具体实现下放到子类来实现,而外观模式只能调用封装好的接口,不能再做灵活的改变

注意与建造者模式的区别

工厂模式

GOF定义:定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化程序延迟到子类中实施

优点:降低了客户端与产生过程的耦合度,客户端不再需要了解产生过程的内部细节,让对象产生流程更加独立

缺点:除非使用泛型方法,不然一定会存在大量的if else或者switch分支判断,但是使用泛型方法一是需要语言本身支持,二是实现过程有一定的难度,不如分支判断简单易懂,不是很方便维护

建造者模式

GOF定义:将一个复杂对象的构建流程与它的对象表现分离出来,让相同的构建流程可以产生不同的对象行为表现。

优点:固化执行流程,director只负责导演整个流程,具体功能,由里面的演员来负责,将系统间的耦合度降低,也有助于代码的阅读和维护,将复杂的‘产生流程’与‘功能实现拆分后’让系统调整和维护变得更容易。并且如果流程要调整,完全不用修改具体实现者。

建造者模式可以分成两个步骤来实施:

  • 将复杂的构建流程独立出来,并将整个流程分成几个步骤,其中的每一个步骤可以是一个功能组件的设置,也可以是参数的制定,并且在一个构建方法中,将这些步骤串接起来。
  • 定义一个专门实现这些步骤(提供这些功能)的实现者,这些实现者知道每一部分该如何完成,并且能接收参数来决定要产出的功能,但不知道整个组装流程是什么

基本上,实现时只要把握这两个原则:“流程分析安排”和“功能分开实现”,就能将建造者模式应用于复杂的对象构建流程上。

建造者模式在实际开发中经常与工厂模式搭配使用。

建造者模式与模板方法模式的异同:

  • 模板方法模式主要针对与继承关系的流程固化,而建造者模式不受控于继承关系,而是组合关系的控制,有根本的不同。

享元模式

GOF定义:使用共享的方式,让一大群小规模对象能更有效地运行

在游戏中主要通过dictionary的key,value来实现属性数据共享的,这样拥有同样基础属性的对象就不用创建多余的完全相同的属性了,可以共享一份属性来源,节约内存

注意:不同的个体属性皆不相同,要把相同的属性归纳独立出来有一定难度,另一方面,如果是采用lua表形式的属性,可以直接读取,速度也很高效,是否还需要采用享元模式来节约内存和加强小数据的管理还值得商榷

组合模式

GOF定义:将对象以树状结构组合,用以表现部分-全体的层次关系。组合模式让客户端在操作各个对象或组合对象时是一致的

主要用于在unity中排列好各组件的组合关系,然后在代码中有序的把对应组件找出来使用,以便做到界面和代码完全分离。而组合模式的作用在于由于是有序的组合,在查找上就能节约不少效率。

命令模式

GOF定义:将请求封装成为对象,让你可以将客户端的不同请求参数化,并配合队列,记录,复原等方法来执行请求的操作

命令模式比较简单,就是将命令发起者与接收者解耦,同时接收者在接收到命令后也可灵活处理。

缺点:

  • 容易产生过多的命令类,同观察者模式相同,可以用回调函数来替代命令类
  • 命令模式在解耦的同时也让逻辑出现断层,增加了调试的复杂度

注意与观察者模式的区别

责任链模式

GOF定义:让一群对象都有机会来处理一项请求,以减少请求发送者与接收者之间的耦合度,将所有的接收者对象串联起来,让请求沿着串接传递,直到有一个对象可以处理为止

让一群信息接收者能够一起被串联起来管理,让信息判断上能有一致的操作接口,不必因为不同的接收者而必须执行类转换操作,并且让所有的信息接收者都能有机会可以判断是否提供服务或将需求移往下一个信息接收者,在后续的系统维护上,也可以轻易地增加接收者类

优点:将函数中的代码提取到类中,通过类对象来避免出现冗长的代码写法,同时通过拆分成类后的多种组合也可以让设计人员更灵活的进行调整

注意:责任链模式并不是每次都需要重头开始判断,这个需要根据具体的业务逻辑进行灵活的调整

观察者模式

GOF定义:在对象之间定义一个一对多的连接方法,当一个对象变换状态时,其他关联的对象都会自己收到通知。

主要运用于成就系统以及通知-执行系统中。

优点:

  • 把'主题发生‘与'功能执行’解除绑定,主题发生只负责通知,执行者具体怎么执行与主题发生者没有关系
  • 观察者模式可以很容易的做到单一触发,多订阅的模式,很实用

缺点:由于观察者与具体主题完全解绑,有可能会产生过多的类,但是可以用回调函数代替独立的类来解决此问题

观察者模式与命令模式的对比:

  • 当观察者模式的主题只存在一个观察者时,就是命令模式了
  • 命令模式:重点是命令的管理,应用的系统对于发出的命令有新增,删除,记录,排序,撤销等等需求
  • 观察者模式:对于‘观察者/订阅者’可进行管理,观察者可以在系统运行期间决定订阅或者退订的操作,让执行者(观察者/订阅者)可以被管理

备忘录模式

GOF定义:在不违反封装的原则下,获取一个对象的内部状态并保留在外部,让该对象可以在日后恢复到原先保留时的状态

把存盘读盘的功能封装到一个类里面统一处理,而不是散落在各个子系统中单独实现,方便统一管理,外部调用者也不必知道存盘功能的具体实现,方便调用。

优点:提供了一个不破坏原有类封装性的‘对象状态保存’方案,并让对象状态保存可以存在多个版本,并且还可以选择要恢复到哪个版本

注意:当每个游戏系统都有存盘需求时,负责保存记录的类就会过于庞大,此时可以让各个系统的存盘信息以结构化方式编排,或者是内部再以子类的方式加以规划。

访问者模式

GOF定义:定义一个能够在一个对象结构中对于所有元素执行的操作。访问者让你可以定义一个新的操作,而不必更改到被操作元素的类接口。

主要设计原理是让被操作的类中置入一个访问者对象,该访问者对象可通过被访问者提供的共享接口来实现独有的功能。

优点:在新增功能时只需要新增对应的类,而不必改动原有的类接口,方便维护和扩展,增加系统的稳定性

缺点:需要被操作的类开放足够多的接口给访问者来调用,这样降低耦合性,同时,如果有新增的被操作类或者被操作类的接口有调整会导致很多相关代码的修改,是把双刃剑

装饰模式

GOF定义:动态地附加额外的责任给一个对象。装饰模式提供了一个灵活的选择,让子类可以用来扩展功能

主要原则就是在不改动已有类已有对象的基础上,通过一层一层的在外围包装装饰来达到更改原本类或对象属性或逻辑的方法。这种方式比较灵活,可随意组合出想要的加成效果。

注意:装饰模式应该运用于‘目标早已存在,而装饰需求之后才出现’的场合中,不该被滥用。对于项目进入后期,或者已上市xia项目的维护周期来说,使用装饰模式来增加现有系统的附加功能是比较稳定的方式,而不应该在项目初期就进行这样的规划,否则过度套叠附加功能,会造成调试上的困难,也会让后续维护者不容易看懂原始设计者最初的组装顺序和设计意图重点内容

注意与代理模式的区别

适配器模式

GOF定义:将一个类的接口转换成为客户端期待的类接口。适配器模式让原本接口不兼容的类能一起合作。

总的来说就是通过二次封装,把原本接口不统一的类通过判断语句(或者宏开关)进行管理,针对不同的状况进行调用

优点:不必使用复杂的方法,将两个不同接口的类对象交换使用。

注意与代理模式的区别

代理模式

GOF定义:提供一个代理者位置给一个对象,好让代理者可以控制存取这个对象

优点:可判断是否要将原始类的工作交由代理者类来执行,如此可以免去修改原始类的接口及实现。同时使用新增类的方式来强化原有功能,对原本的实现不进行更改。

代理模式与装饰模式的差别:

  • 代理模式会知道代理的对象是哪个子类,并拥有该子类的对象,而装饰模式则是拥有父类对象(被装饰对象)的引用。代理模式会按“职权”来决定是不是需要将需求转给原始类,所以代理有“选择”要不要执行原有功能的权利,但装饰模式是一个“增加”的操作,必须在原始类被调用之前或之后,再按照自己的职权“增加”原始类没有的功能。

代理模式与适配器模式的差别:

  • 代理类与原始类同属一个父类,所以客户端不需要做任何变动,只需决定是否要采用代理者,而适配器模式中的适配类和原始类则分属于不同的类群组,着重于“不同实现的转换”。

其它模式

迭代器模式

GOF定义:在不知道集合内部细节的情况下,提供一个按序方法存取一个对象集合体的每一个单元。

在不知道集合内部细节的情况下,提供一个按序方法存取一个对象集合体的每一个单元
一句话:for 循环也

原型模式

GOF定义:使用原型对象来产生制定类的对象,所以产生对象时,时使用复制原型对象来完成。

解释器模式

GOF定义:定义一个程序设计语言所需要的语句,并提供解释来解析(执行)该语言(js,lua)

抽象工厂模式

GOF定义:定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化程序延迟到子类中实行。

提供一个能够建立整个类群组或有关联的对象,而不必指明它们的具体类

优点:能将产生的对象”整组”转换到不同的类群组上

参考书目:
[1]: 设计模式:可复用面向对象软件的基础 [美] Erich Gamma,[美] Richard Helm,[美] Ralph Johnson 等 著,刘建中 等 译
[2]: 设计模式与游戏完美开发 蔡升达 著
[3]: 大话设计模式 程杰 著

本文转自:博客园 - CraneInForest,转载此文目的在于传递更多信息,版权归原作者所有。

推荐阅读