游戏设计模式杂谈(一)

设计模式简介

设计模式的主要目的就在于解耦,也就是今后处理改动的时候最小化在编写代码前需要了解的信息

任何设计模式都需要遵循基本原则,让类与类之间减少耦合

设计模式的类型

创建型模式: 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

结构型模式: 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。

行为型模式:这些设计模式特别关注对象之间的通信。

命令模式

准确定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。

这太难理解了,简化后就是: 命令是具现化的方法调用,就是函数调用被存储在对象之中

什么时候使用?

游戏中比较常见一种场景就是实现用户自定义配置按键,要实现这样的话我们就不能在代码中将用户输入和将执行的方法一一对应。

我们需要将游戏行为作为一个对象来传递, 将每个行为都继承同一个基类,这样的话就能通过基类调用函数。我们还能在基类中要求实现行为执行的虚函数的同时实现他对应的撤销方法虚函数,这就是如何支持他可撤销的操作

我们可以给游戏行为指定游戏对象,比如 execute(GameObject go)这样的话不仅能够复用代码在玩家或者是NPC上,还能实现本地双人游戏的按键映射

这样还有个好处,我们能够在选择命令的AI和执行命令的角色之间解耦,这样我们能够为了不同的行为混合AI或者是对同一角色执行不同AI,这样我们就能分模块编写巡逻AI,不同难度的战斗AI,控制AI等

享元模式

享元模式,利用先前创建的已有对象,判断当前所需对象是否能够通过原有对象做一定修改后获得

比如在游戏中出现了一片森林,需要大量的时间渲染,但是每棵树之间只是位置、颜色、大小等可调节差异。那么我们就可以将共有的数据分离到另一个类中

class TreeModel
{
  Mesh mesh_;
  Texture bark_;
  Texture leaves_;
}

其余的修改数据放到另一个类中,保留对这个类的引用就行了

在这里插入图片描述

同时,我们只用发送后每个模型的独特数据后在告诉GPU,用同一模型渲染每一个实例,这样也能减少Drawcall

更近一步,我们对于游戏中的地形也能采用这样的模式。所有地形都是一个类的不同实例,只是属性不同而已,比如纹理,能够在上面生成的生物类型等。水域,草地,沙漠一系列地形,我们提前创建好他们的范式实例。

在这里插入图片描述

我们需要获取某一区块属性的时候就可以直接从那个区块获得

int id = world.getTile(2, 3).getID();

这样相较于在每个函数中判断地形的枚举值,做许多的分支跳转好得多

观察者模式

大概思路就是:让代码宣称某个事件触发了,而不必关心谁接受了这个通知。C#甚至专门设定了event关键词来方便我们开发观察者模式

比如游戏中成就系统,各种全局的管理系统都和各种各样的模块有交互。当我们不想这些模块了解彼此,那样会写出非常不明所以的代码

比如MC中有一个成就就是把猪摔死,我们总不能在地面每次检测到有东西落地时判断下落物体是不是猪而且有没有摔死,这样的判断放在物理碰撞的代码之后太诡异了。

我们可以在落地的时候加一个通知,成就系统注册他自己为观察者就行了,具体的判断交给成就系统自己去做。这样的话我们之后修改了成就的具体要求或者是删除了整个成就系统也不会对物理引擎造成丝毫的影响。此外物体落地时的订阅者还可能包含血量减少,音效播放等,这些代码都不应该出现在物理模块中,而是应该传递给对应的模块去处理

值得注意的地方:

我们移除了被观察者的时候如果没有取消观察者的注册,有可能这时候他没有被垃圾回收,这个时候事件仍然会被传递,这个问题在通知系统中被称为:失效监听者问题

原型模式

和之前的享元模式不同,原型模式是一种创建型模式,它是根据某种规则去根据原型创建实例

所有怪物子类都继承基类,并实现他的clone方法来依据模板生成更多一样的怪物。

这不仅能复制原型的类还能复制他的状态,不需要硬编码修改独特值,只需要有对应的原型就行了

单例模式

单例模式算是日常开发中解决问题最容易想到的设计模式,他保证一个类只有一个实例,并且提供了访问该实例的全局访问点,这带来了极大的便利,但是通常他弊大于利

  • 他是一个全局变量

这意味着不同模块的代码会相互耦合,比如我们在击打怪物的时候需要播放音乐,而恰好有个全局可见的音乐模板,我们很难抵制诱惑最终在战斗模块中加入有关音频的代码

同时他对并行操作很难兼容,我们并不知道其他线程是否正在使用单例,很容易造成死锁以及数据同步问题

  • 我们不能控制初始化

单例通常都是需要调用时在初始化他,因为这样就不用考虑生命周期的问题。但是如果初始化需要消耗比较多的时间,这就会影响游戏体验。

如何不使用单例模式?

很多游戏中的单例类都是作为管理器这个角色,也就是“XXXManager”。很多时候我们都可以删去这些manager,把他们移到单例管理的类当中,让对象管理好自己

比如有可能我们拥有 BulletBulletManagerBulletManager中拥有生成子弹,移动子弹等方法,但是呢这些都可以让 Bullet自己实现。设计糟糕的单例会让另一个类反而增加代码

此外,很多时候我们使用单例就是为了方便地获取所需的对象。但是代价就是我们不想要对象的地方也能轻易的使用。我们需要在完成功能的同时,将对象影响的范围改变的尽量小,于是解决方案就有以下几种:

  • 我们不让方法自己去获取对象,而是将对象传入方法,这是最简单的方法,但是呢,我们在调用攻击怪物的函数时还要传入一个音频的参数,这并没有改善代码的耦合程度
  • 从基类中获得,我们可以在武器基类中加入protected音频对象,使得不同的武器子类拥有不同的打击音效,每个派生的实体都能调用自己的播放音频方法
  • 从已经是全局的东西中获取,我们可以将整个游戏世界作为一个单例,将音频,log类作为这个单例的属性,能访问Game这个单例的类才能访问音频,从而限定了范围

状态模式

状态模式简单来说可以认为是FSM——有限状态机

有限状态机在编写玩家控制逻辑的时候经常使用,根据当前角色的状态:下落过程,在地上等过程对同一操作有不同的反应。

展开来讲状态机就是:拥有状态机所有可能状态的集合状态机同时只能在一个状态每个状态都有一系列的转移,每个转移与输入和另一状态相关

更近一步,如果我们允许玩家能拿起武器,这样的话状态就会翻倍:站立,持械站立等等。这可不符合我们的预期,为了改进我们不能使用大量的switch,而是将每个状态都封装为一个类。这样的话每个状态独有的字段都只出现在对应的状态当中,每次输入或者游戏循环都交给当前状态去执行。

这样有了个新的问题,我们切换状态的时候需要删除之前的状态,而且不同状态切换到同一状态的效果几乎是差不多的。因此我们可以给所有状态添加一个入口方法,这样避免了重复代码而且不用关心从哪个状态转换而来。

当然如果遇到更复杂的状态编写,我们可以实现分层状态机,使状态之间可以继承来实现层级。或者是遇到需要记录上一个状态的场景,我们可以使用栈来记录,这样就不用为了追踪之前的状态定义很多相似的类:比如站立开火,跑步开火等

在这里插入图片描述

当然由于状态机各种各样的限制,如今的AI已经发展成了行为树,规划系统等领域。

但是他还有有用的,我们可以在下列场景愉快的使用FSM

  • 你有个实体,它的行为基于一些内在状态。
  • 状态可以被严格地分割为相对较少的不相干项目。
  • 实体响应一系列输入或事件。

猜你喜欢

转载自blog.csdn.net/jkkk_/article/details/127734756