前言

本文为游戏设计模式的读书笔记,

img

架构,性能和游戏

重访设计模式

命令模式

定义:

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

理解

1
将命令封装,与目标行为解耦,使命令由流程概念变为对象数据

UML

img

要点

  • 将一组行为抽象为对象,这个对象和其他对象一样可以被存储和传递,从而实现行为请求者与行为实现者之间的松耦合,这就是命令模式。

一个手柄,每个按键都与一个特定的'button_'变量相关联,这个变量再与函数关联。

  • 命令模式是回调机制的面向对象版本。
  • 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
  • 一条连接AI到角色的管道。
  • 命令模式的优点有:对类间解耦、可扩展性强、易于命令的组合维护、易于与其他模式结合,而缺点是会导致类的膨胀。
  • 命令模式有不少的细分种类,实际使用时应根据当前所需来找到合适的设计方式。

使用场合

  • 命令模式很适合实现诸如撤消,重做,回放,时间倒流之类的功能。

从旧到新排列的命令栈。 一个当前箭头指向一条命令,一个“撤销”箭头指向之前的命令,一个“重做”指向之后的命令

  • 基于命令模式实现录像与回放等功能,也就是执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合。

引申与参考

  • 最终我们可能会得到很多不同的命令类。为了更容易实现这些类,定义一个具体的基类,包含一些能定义行为的高层方法,往往会有帮助。可以将命令的主体execute()转到子类沙箱中。
  • 对象可以响应命令,或者将命令交给它的从属对象。如果我们这样实现了,就完成了一个职责链模式。
  • 对于等价的实例,可能会导致大量的实例化, 可以用享元模式提高内存利用率。

享元模式

享元模式,以共享的方式高效地支持大量的细粒度的对象。通过复用内存中已存在的对象,降低系统创建对象实例的性能消耗。

理解

1
不同的实例共享相同的特性(共性),同时保留自己的特性部分

UML

img

左半部分为享元模式下,只有一个CubeBase,通过ObjInstancing(int num)将共享的网格、材质及一个Transform信息表传递给GPU,只有一个Draw Call,所以效率极高

右半部分为关闭享元模式后的做法,每生成一个Cube都会重新实例化一个立方体,并向GPU发送一次网格、材质和位置信息,所以1000个立方体就需要1000个Draw Call,效率极低

要点

  • 享元模式中有两种状态。内蕴状态(Internal State)和外蕴状态(External State)。
    • 内蕴状态,是不会随环境改变而改变的,是存储在享元对象内部的状态信息,因此内蕴状态是可以共享的。对任何一个享元对象而言,内蕴状态的值是完全相同的。
    • 外蕴状态,是会随着环境的改变而改变的。因此是不可共享的状态,对于不同的享元对象而言,它的值可能是不同的。
  • 一行树,每个都有自己的参数和位置朝向,指向另一个有网格、纹理、树叶的树模型。
  • 享元模式通过共享内蕴状态,区分外蕴状态,有效隔离系统中的变化部分和不变部分。

使用场合

在以下情况都成立时,适合使用享元模式:

  • 当系统中某个对象类型的实例较多的时候。
  • 由于使用了大量的对象,造成了很大的存储开销。
  • 对象的大多数状态都可变为外蕴状态。
  • 在系统设计中,对象实例进行分类后,发现真正有区别的分类很少的时候。

一行区块,每个区块指向共享的草、河、山丘对象。

1
世界的表面被划分为由微小区块组成的巨大网格。 每个区块都由一种地形覆盖。我的世界中山川地形有河流有树有草皮,以一定的地形系统来绘制地形,我们只需要调用一个地形类就可以对于地图的绘制

引申与参考

  • 为了返回一个已经创建的享元,需要和那些已经实例化的对象建立联系,我们可以配合对象池来进行操作。
  • 当使用状态模式时,很多时候可以配合使用享元模式,在不同的状态机上使用相同的对象实例。

观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

成就:一无所成

理解

1
解耦,物价局改了粮价不需要挨家挨户通知公民,只需要让电视台播个新闻就好

UML

img

要点

  • 观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 我们知道,将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。
  • 目前广泛使用的MVC模式,究其根本,是基于观察者模式的。
  • 观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法(event关键字)中。

使用场合

  • 当一个抽象模式有两个方面,其中一个方面依赖于另一个方面,需要将这两个方面分别封装到独立的对象中,彼此独立地改变和复用的时候。
  • 当一个系统中一个对象的改变需要同时改变其他对象内容,但是又不知道待改变的对象到底有多少个的时候。
  • 当一个对象的改变必须通知其他对象作出相应的变化,但是不能确定通知的对象是谁的时候。

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

UML

img

  • Unity中Prefab本质就是此模式里的原型,而Spawner要做的只是调用Instantiate方法
  • 新的Prefab被生成以后,通过读取Dragons.txt里配置的信息来设置克隆体的名称和尺寸

要点

  • 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

一个生产者包含一个对怪物应用的原型字段。 他调用原型的clone()方法来产生新的怪物。

  • 原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。
  • 使用原型模式拷贝对象时,需注意浅拷贝与深拷贝的区别。
  • 原型模式可以结合JSON等数据交换格式,为数据模型构建原型。

使用场合

  • 产生对象过程比较复杂,初始化需要许多资源时。
  • 希望框架原型和产生对象分开时。
  • 同一个对象可能会供其他调用者同时调用访问时。

单例模式

保证一个类只有一个实例,并且提供了访问该实例的全局访问点。

UML

img

1
游戏中看到的很多单例类都是“管理器”——那些类存在的意义就是照顾其他对象。 我曾看到一些代码库中,几乎所有类都有管理器: 怪物,怪物管理器,粒子,粒子管理器,声音,声音管理器,管理管理器的管理器。 有时候,它们被叫做“系统”或“引擎”,但是思路还是一样的。

要点

  • 单例模式因其方便的特性,在开发过程中的运用很多。
  • 单例模式有两个要点,保证一个类只有一个实例,并提供访问该实例的全局访问点。
  • 尽量少用单例模式。单例模式作为一个全局的变量,有很多全局的变量的弊病。它会使代码更难理解,更加耦合,并且对并行不太友好。

使用场合

  • 当在系统中某个特定的类对象实例只需要有唯一一个的时候。
  • 单例模式要尽量少用,无节制的使用会带来各种弊病。
  • 为了保证实例是单一的,可以简单的使用静态类。 还可以使用静态标识位,在运行时检测是不是只有一个实例被创建了。

参考与引申

  • 下文中介绍的子类沙箱模式通过对状态的分享,给实例以类的访问权限而无需让其全局可用。
  • 下文中介绍的服务定位器模式不但让一个对象全局可用,还可以带来设置对象的一些灵活性。

状态模式

允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。

UML

img

img

一张画有盒子的图表,盒子代表了站立,跳跃,俯卧和跳斩。标记了按键的按下和释放的箭头连接了这些盒子。

1
2
3
4
5
6
7
8
9
10
11
12
13
祝贺,你刚刚建好了一个有限状态机。 它来自计算机科学的分支自动理论,那里有很多著名的数据结构,包括著名的图灵机。 FSMs是其中最简单的成员。

要点是:

你拥有状态机所有可能状态的集合。 在我们的例子中,是站立,跳跃,俯卧和速降。

状态机同时只能在一个状态。 英雄不可能同时处于跳跃和站立状态。事实上,防止这点是使用FSM的理由之一。

一连串的输入或事件被发送给状态机。 在我们的例子中,就是按键按下和松开。

每个状态都有一系列的转移,每个转移与输入和另一状态相关。 当输入进来,如果它与当前状态的某个转移相匹配,机器转换为所指的状态。

举个例子,在站立状态时,按下下方向键转换为俯卧状态。 在跳跃时按下下方向键转换为速降。 如果输入在当前状态没有定义转移,输入就被忽视。

要点

  • 状态模式用来解决当控制一个对象状态转换的条件表达式过于复杂的情况,它把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。

  • 状态模式的实现分为三个要点:

    • 为状态定义一个接口。
    • 为每个状态定义一个类。
    • 恰当地进行状态委托。
  • 通常来说,状态模式中状态对象的存放有两种实现存放的思路:

    • 静态状态。初始化时把所有可能的状态都new好,状态切换时通过赋值改变当前的状态。
    • 实例化状态。每次切换状态时动态new出新的状态。

使用场合

  • 在游戏开发过程中,涉及到复杂的状态切换时,可以运用状态模式以及状态机来高效地完成任务。

  • 有限状态机的实现方式,有两种可以选择:

    • 用枚举配合switch case语句。
    • 用多态与虚函数(即状态模式)。
  • 有限状态机在以下情况成立时可以使用:

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

序列模式

双缓冲模式

双缓冲模式,使用序列操作来模拟瞬间或者同时发生的事情。

理解

1
我们称这些舞台为舞台A和舞台B。 场景一在舞台A上。同时场务在处于黑暗之中的舞台B布置场景二。 当场景一完成后,将切断场景A的灯光,打开场景B的灯光。观众看向新舞台,场景二立即开始。此时场景切换只需要开关灯光(指针的指向改变),后swap就可以得到改变缓冲区的效果
1
对于跑酷游戏,生成地图,碰到某个点而去生成下一个地图,生成过程中可以使用缓冲区

要点

  • 一个双缓冲类封装了一个缓冲:一段可改变的状态。这个缓冲被增量的修改,但我们想要外部的代码将其视为单一的元素修改。 为了实现这点,双缓冲类需保存两个缓冲的实例:下一缓存和当前缓存。
  • 当信息从缓冲区中读取,我们总是去读取当前的缓冲区。当信息需要写到缓存,我们总是在下一缓冲区上操作。 当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换, 这样新缓冲区就是公共可见的了。旧的缓冲区则成为了下一个重用的缓冲区。
  • 双缓冲模式常用来做帧缓冲区交换。

使用场合

双缓冲模式是那种你需要它时自然会想起来的模式。以下情况都满足时,使用这个模式很合适:

  • 我们需要维护一些被增量修改的状态
  • 在修改过程中,状态可能会被外部请求。
  • 我们想要防止请求状态的外部代码知道内部是如何工作的。
  • 我们想要读取状态,而且不想在修改的时候等待。

游戏循环

游戏循环模式,实现游戏运行过程中对用户输入处理和时间处理的解耦。

可参考[脚本生命周期](Unity - Manual: Order of execution for event functions (unity3d.com)),主要游戏引擎给你处理好了,可以相当于每一帧对于游戏的处理如同。

要点

  • 游戏循环模式:游戏循环在游戏过程中持续运转。每循环一次,它非阻塞地处理用户的输入,更新游戏状态,并渲染游戏。它跟踪流逝的时间并控制游戏的速率。
  • 游戏循环将游戏的处理过程和玩家输入解耦,和处理器速度解耦,实现用户输入和处理器速度在游戏行进时间上的分离。
  • 游戏循环也许需要与平台的事件循环相协调。如果在操作系统的高层或有图形UI和内建事件循环的平台上构建游戏, 那就有了两个应用循环在同时运作,需要对他们进行相应的协调。

使用场合

任何游戏或游戏引擎都拥有自己的游戏循环,因为游戏循环是游戏运行的主心骨。

游戏循环发展史

  • 最古老的交互式程序

文本处理器通常呆在那里什么也不做,直到你按了个键或者点了什么东西。

类似于对于普通的galgame,你只需要点击后才会触发事件:

1
2
3
4
5
while (true)
{
Event* event = waitForEvent();
dispatchEvent(event);
}
  • 不堵塞游戏内时间的交互式系统

这是真实游戏循环的第一个关键部分:它处理用户输入,但是不等待它。循环总是继续旋转:

你即使不做任何动作,游戏也会继续更新和渲染,这边可以使用协程模拟这个过程

1
2
3
4
5
6
while (true)
{
processInput();
update();
render();
}
  • 底层对于性能的影响

我们现在写的这个循环是能转多快转多快,两个因素决定了帧率。 一个是每帧要做多少工作。另一个是底层平台的速度。 更快的芯片可以在同样的时间里执行更多的代码。

这就是游戏循环的另一个关键任务:不管潜在的硬件条件,以固定速度运行游戏。

每一个设备具有不同的更新和渲染速度,如同小霸王和外星人

下面案例是如果帧数过快直接使得系统休眠的方法

1
2
3
4
5
6
7
8
9
10
public void FixAlienware(){
float handleTime = 0.5f;

float sleepTime = timer.moveTime - handleTime;

if (sleepTime > e) {
timer. moveTime = sleepTime + handleTime;
}else {
timer. moveTime = handleTime;
}

一个简单的游戏循环流程图。处理输入 → 更新游戏 → 渲染 → 等待,然后从头开始。

  • 拉伸每一帧需要处理的内容

每一帧,我们计算上次游戏更新到现在有多少真实时间过去了(即变量elapsed)。 当我们更新游戏状态时将其传入。 然后游戏引擎让游戏世界推进一定的时间量。

假设有一颗子弹跨过屏幕。 使用固定的时间间隔,在每一帧中,你根据它的速度移动它。 使用变化的时间间隔,你根据过去的时间拉伸速度。 随着时间间隔增加,子弹在每帧间移动得更远。 无论是二十个快的小间隔还是四个慢的大间隔,子弹在真实时间里移动同样多的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void FixFixAlienware(){
float handleTime =5.5f;
//我的一帧虽然快,经过拉伸以后,但是每一帧处理的表现就比较少
float mul = handleTime / timer.moveTime;

float movex=1;
//假如我们游戏需要真实时间1s移动的距离是1
Debug.Log("我一让方块运行的距离是" + mul* moveX +"我一秒让方块移动的距离是"+movex + mul * timer.moveTime / handleTime);

timer.moveTime = handleTime;
//引入了新问题,联网对战中,两个设备的配置不同,经过拉伸的比例也不同,浮点数的每一次拉伸运算都有误差,那么拉伸比例大的那个误差也就多了。
}

  • 将渲染和逻辑更新分离

幸运的是,我们给自己了一些喘息的空间。 技巧在于我们将渲染拉出了更新循环。 这释放了一大块CPU时间。 最终结果是游戏以固定时间步长模拟,该时间步长与硬件不相关。 只是使用低端硬件的玩家看到的内容会有抖动。

对于高性能的电脑来说,更新和渲染可以进行同步,但对于低端性能电脑来说可以把更新和渲染不同步以实现同步效果,可以理解为你更新了俩次,才渲染了一次。

一条时间线,上面是均匀分布的更新时刻和不均匀的渲染时刻。

  • 将渲染和逻辑更新分离后的问题

在渲染中有可能更新于渲染的帧率不同导致你会做出些无效操作,如死亡后继续释放技能,这边就需要在需要的位置添加关键帧,如同释放技能时或者死亡时。

这边涉及到了帧同步和状态同步。

时间线的一部分,渲染发生在两次更新之间。

更新方法

行为模式

字节码

子类沙箱

理解

用一系列由基类提供的操作定义子类中的行为。

要点

子类沙箱模式:基类定义抽象的沙箱方法和几个提供操作的实现方法,将他们设为protected,表明它们只为子类所使用。每个推导出的沙箱子类用提供的操作实现了沙箱方法。

使用场合

子类沙箱模式是潜伏在编程日常中简单常用的模式,哪怕是在游戏之外的地方。 如果有一个非虚的protected方法,你可能早已在用类似的技术了。

沙箱方法在以下情况适用:

  • 你有一个能推导很多子类的基类。
  • 基类可以提供子类需要的所有操作。
  • 在子类中有行为重复,你想要更容易的在它们间分享代码。
  • 你想要最小化子类和程序的其他部分的耦合。

类型对象

解耦模式

组件模式

事件队列

服务定位器

优化模式

数据局部性

脏标识模式

对象池模式

理解

放弃单独地分配和释放对象,从固定的池中重用对象,以提高性能和内存使用率。

要点

  • 对象池模式:定义一个包含了一组可重用对象的对象池。其中每个可重用对象都支持查询“使用中”状态,说明它是不是“正在使用”。 对象池被初始化时,就创建了整个对象集合(通常使用一次连续的分配),然后初始化所有对象到“不在使用中”状态。
  • 当我们需要新对象时,就从对象池中获取。从对象池取到一个可用对象,初始化为“使用中”然后返回给我们。当不再需要某对象时,将其设置回“不在使用中”状态。 通过这种方式,便可以轻易地创建和销毁对象,而不必每次都分配内存或其他资源。

使用场合

  • 这个模式广泛使用在可见事物上,比如游戏物体和特效。但是它也可在不那么视觉化的数据结构上使用,比如正在播放的声音。

  • 满足以下情况可以使用对象池:

    • 需要频繁创建和销毁对象。
    • 对象大小相仿。
    • 在堆上分配对象缓慢或者会导致内存碎片。
    • 每个对象都封装了像数据库或者网络连接这样很昂贵又可以重用的资源。

空间分区

番外

最近看到了ECS模式与MVC模式的对比,参考一下链接

【Unity3D】MVC框架在Unity项目中的理解与使用_unity mvc框架工作原理-CSDN博客

除了 ECS,还有什么游戏架构 - Bob Nystrom_哔哩哔哩_bilibili

MVC和ECS两种设计架构的初浅理解_对mvc和ecs框架有一定理解-CSDN博客

对于(MVC 、MVCS、MVVM)的理解-CSDN博客

参考

《游戏设计模式》(游戏编程模式)全书笔记+Unity 实现
游戏设计模式

【游戏设计模式】之四 《游戏编程模式》全书内容提炼总结 - 知乎 (zhihu.com)

【Unity】设计模式_游戏循环解读_哔哩哔哩_bilibili