作者:Robert Nystrom
第1章 概述
- 抽象和解耦能够使你的程序开发变得更快更简单,但不要浪费时间来做这件事,除非你确信存在问题的代码需要这种灵活性。
- 开发周期中要对性能进行设计和思考,弹药推迟降低灵活性的、底层的、详尽的优化,能晚则晚。
第2章 命令模式
第3章 享元模式
第4章 观察者模式
- 及时删除观察者 如销毁场景
第5章 原型模式
- 使用特定原型实例来创建特定种类的对象,并且通过拷贝原型来创建新的对象
1 | // 基类Monster 抽象方法clone |
- Self语言
- 基于类的语言的语法:为了获取对象的某些状态,需要获取该对象在内存里的实例,状态被包含在实例当中。为了调用该实例的一个方法,需要在类的声明中查找这个方法,实例的行为被包含在类中。
- Self语言:一个实例可以包含状态和行为
- 为了查找一个对象的属性和方法,现在该对象自身查找,如果找到了直接返回,反之,从它的父类中继续查找,直至查找到没有父节点为止。
- 在Self中,每一个对象都自动支持原型模式,任意对象都可以被克隆。
- JavaScript
- 去掉clone
- new关键字所做的
- 调用构造函数方法
- 把this指针绑定到新创建的对象上
- 该方法内部给this对象添加了一些属性
- 把初始化属性的对象返回
- 过程
- 通过new操作符来创建对象,并且通过构建函数来初始化对象
- 状态被存储在对象本身之中
- 对象的行为被定义在原型对象之中,这样可以让这些方法被所有的特定类型所共享
- 数据建模
1
2
3
4
5{
"name" : "goblin wizard",
"prototype" : "goblin grunt",
"spells" : ["fire ball","lightning bolt"]
}
第6章 单例模式
- 如果我们不使用它,就不会创建实例
- 它在运行时初始化
- 你可以继承单例
基本结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59class FileSystem
{
public:
virtual -FileSystem() {}
virtual char* read(char* path) = 0;
virtual void write(char* pash,char* text) = 0;
};
// 之后,我们为不同平台定义派生类
class PS3FileSystem:public FileSystem
{
public:
virtual char* read(char* path)
{
// Use Sony file 10 API
}
virtual void write(char* path,char* text){
// Use Sony file 10 API
}
};
class WiiFileSystem:public FileSystem
{
public:
virtual char* read(char* path)
{
// Use Nintendo file I0 API
}
virtual void write(char* path,char* text){
// Use Nintendo file I0 API
}
}
// 接下来,我们将FileSystem变为一个单例:
class FileSystem
{
public:
static FileSystem& instance();
virtual -FileSystem() {}
virtual char* read(char* path) = 0;
virtual void write(char* path,char* text) = 0;
protected:
FileSystem() {}
}
// 创建实例
FileSystem& FileSystem::instance()
{
static FileSystem *instance = new PS3FileSystem();
static FileSystem *instance = new WiiFileSystem();
return *instance;
}
整个代码库都可以通过FileSystem:instance()访问文件系统
- 全局变量的缺点
- 代码晦涩难懂
- 促进了耦合
- 对并发不友好
第7章 状态模式
- 允许一个对象在其内部状态改变时改变自身的行为,对象看起来好像是在修改自身类
- 有限状态机
- 你拥有一组状态,并且可以在这组状态之间进行切换
- 状态机同一时刻只能处于一种状态
- 状态机会接收一组输入或者事件
- 每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态
- 并发状态机
- 并发的两种状态组合
- 层次状态机-继承
- 当我们把主角的行为更加具象化后,她可能包含大量相似的状态。比如,她可能站立、走路、跑步…,这些状态中的任意状态按下B键都要跳跃。
- 下推状态机
- 我们知道当前状态,但是,我们并不知道之前的状态是什么。而且,我们也没有简便的方法可以获取之前的状态。
- 解决方案:在一个有限状态机里面,当一个状态切进来时,则替换掉之前的状态。
- 你可以把这个新的状态放入栈里。当前的状态永远存在栈顶,所以你总能转换到当前状态。但是当前状态会将前一个状态压在栈中自身的下面而不是抛弃丢掉它。
- 你可以弹出栈顶的状态,该状态将被抛弃,与此同时,上一个状态就变成了新的栈顶状态。
第8章 双缓冲
- 使用两个帧缓存
- 当要从缓冲区读取信息时,总是从当前缓冲区读取。当要往缓冲区写入数据时,总在后台缓冲区上进行。
- 使用场景
- 我们需要维护一些被逐步改变着的状态量
- 同个状态可能会在其被修改的同时被访问到
- 我们希望避免访问状态的代码能看到具体的工作过程
- 我们希望能够读取状态但不希望等待写入操作的完成
第9章 游戏循环
- 处理用户的输入,但并不等待用户输入
- 两个因素决定了FPS:
- 循环每一帧要处理的信息量
- 底层平台速度
- 当需要16ms以上的时间来更新帧速为16ms每帧的游戏时,可以不用那么频繁地更新游戏并且能够追赶上游戏的行进速度。
- 计算这一帧距离上一帧的实际时间间隔以作为更新步长
- 物理引擎不稳定原因:
- 高帧率更新位置50次,低帧率只执行了5次,每次计算结果不完全一致,浮点数会带来舍入误差
- 为了以实时来运行,游戏的物理引擎会做实际物理规则的近似。为了防止近似计算“炸飞上天”,系统进行了减幅计算。这些减幅运算被小心地安排成以某个固定时长进行
- 把时间追回来
- 允许在渲染的时候进行一些灵活的调整以释放出一些处理器时间
- 在每帧开始,基于实际流逝的时间更新变量lag,这一变量表示了游戏时钟相对现实时间落后的差量。接着我们使用一个内部循环来更新游戏,每次以固定时长进行,直到它追赶上现实时间。一旦赶上现实时间,我们开始渲染并进行下一次游戏循环
- 固定步长不是视觉上的帧率,只是更新游戏的间隔。越短,追赶上实际时间所花费的处理次数越多,越长,跳帧越明显。
- 安全措施:当内部更新循环次数超过一定迭代上线,让循环终止。这样游戏会变慢,但不会卡死。
- 以固定步长更新游戏,但在随机时间点进行渲染,渲染频率<更新频率
- 设置帧率上限-假如游戏循环在本时间片内已经完成了处理,那么剩余时间它将休眠
第10章 更新方法
- 为游戏中的每个实体封装其自身的行为,这将使游戏循环保持整洁并便于往循环中增加或移除实体。
- 在每一帧都给予每个对象一次更新自己行为的机会。
- 所有对象都在每帧进行模拟,但并非真正同步-顺序更新,每次增量式的更新会改变游戏世界,从一个有效的状态到下一个
- 优化:单独维护一个需要被更新的“存活”对象表,当一个对象被禁用时,将它从其中移除。
- update方法存在于:
- 实体类中
- 组件类中
- 代理类中
第11章 字节码
- 解释器模式
- 字节码模式
- 基于栈的虚拟机
- 指令小
- 代码生成简单
- 指令数多
- 基于寄存器的虚拟机
- 指令大
- 指令数少
- 基于栈的虚拟机
- 应该有哪些指令
- 外部基本操作
- 内部基本操作
- 控制流
第12章 子类沙盒
- 一个基类定义了一个抽象的沙盒方法和一些预定义的操作集合。通过将它们设置为受保护的状态以确保它们仅供子类使用。每个派生出的沙盒子类根据父类提供的操作来实现沙盒函数
- 使用场景
- 你有一个带有大量子类的基类
- 基类能够提供所有子类可能需要执行的操作集合
- 在子类之间有重叠代码,你希望在它们之间更简便地共享代码
- 你希望使这些继承类与程序其它代码之间的耦合最小化
- 操作分流
- 减少基类的函数数量
- 在辅助类中的代码更容易维护
- 降低了基类和其它系统之间的耦合
- 分段初始化
- 构造函数不带参数,仅负责创建对象
- 调用一个直接定义在基类中的函数来传递它所需的数据
第13章 类型对象
- 定义一个类型对象类和一个持有类型对象类。每个类型对象的实例表示一个不同的逻辑类型。每个持有类型对象类的实例引用一个描述其类型的类型对象。
第14章 组件模式
- 使用场景:
- 有一个涉及多个域的类
- 一个类越来越庞大
- 希望定义许多共享不同能力的对象
- 组件通信
- 通过修改容器对象的状态
- 直接相互引用
- 传递信息
- 在容器类中建立一个小的消息传递系统,让需要传递信息的组件通过广播的方式去建立组件间的联系。
- 先定义一个所有组件都能实现的基本组件接口,包含receive方法,组件通过实现它来监听传入消息;在容器类中添加一个方法来发送消息。
- 如果一个组件访问它的容器,先把信息发送给容器,并通过容器将信息广播给容器所包含的所有组件。
- 组件之间唯一的耦合在于组件本身。
- 容器类仅仅将消息发出,不必知道消息内容。
第15章 事件队列【发布/订阅】
- 事件循环-每当用户与程序交互时,操作系统会为之生成一个事件。系统将这个事件抛给应用程序,为了收到这些事件,必然有个事件循环。
- 中心事件总线-游戏的任何一个系统都可以向它发送事件,也可以从队列中收取事件
- 事件队列模式
- 发出通知时系统会将该请求置入队列并随即返回
- 请求处理器随后从事件队列中获取并处理这些请求
- 请求可由处理器直接处理或转交给对其感兴趣的模块
- 队列提供给拉取请求的代码块一些控制权
- 接受者可由延迟处理、聚合请求或完全废弃
- 汇总请求
- 请求入列使合并,而非处理时
第16章 服务定位器
- 一个服务类为一系列操作定义了一个抽象的接口,一个具体的服务提供器实现这个接口。一个单独的服务定位器通过查找一个合适的提供器来提供这个服务的访问,同时屏蔽了提供器的具体类型和定位这个服务的过程。
- 定位方式:在使用这个服务之前依赖一些外部代码来注册一个服务提供器
- ”可配置的单例模式“
- 空服务:在注册之前使用报错-提供一个结构一直的空服务
- 限制服务的访问域
第17章 数据局部性
- 数据的组织方式是游戏性能的一个关键部分
- CPU的速度飞快增长,RAM的访问速度增长迟缓
- 合理使用缓存,检测缓存命中
- 热/冷分解-将每帧需要检测的状态量和不会频繁使用的数据划分开
- 合理设计数据结构
第18章 脏标记模式
- 延时重算
- 将父链上物体的多个局部变换的改动分解为每个物体的一次重计算
- 把耗时的工作推迟到真正需要才进行
- 当任何原始数据改动时都需要设置脏标记
第19章 对象池
- 当你需要频繁地创建或销毁对象
- 对象大小一致
- 在堆上进行内存分配较慢或会产生内存碎片
- 每个对象封装获取代缴昂贵且可重用的资源
第20章 空间分区
- 检测对象附近有什么物体-对象分区
- 常见结构
- 网格[Gird(spatial_index)]
- 四叉树
- 二叉空间分割(BSP)
- k-dimensional树
- 层次包围盒
- 网格是一个连续的桶排序
- 二叉空间分割、k-d树、层次包围盒都是二叉查找树
- 四叉树和八叉树都是Trie树
- 每个分区都有大致相同的对象个数