游戏编程算法与技巧-读书笔记(一)

多线程下的游戏循环

假定对每个游戏来讲,渲染整个场景需要30ms,它还需要额外的20ms去更新游戏世界。如果这些都在同一个线程执行,每帧将耗时50ms,最终导致降低帧率–20FPS,这是不可接受的。但如果渲染和更新逻辑同步执行,每帧只要30ms,30FPS的目标就可以完成。
主线程必须区里所有输入、更新游戏世界、处理所有图形以外的输出。它必须提交相关数据给第二条线程,那么第二条线程就可以渲染所有图像。渲染线程绘制的时候,主线程该干什么?让渲染线程比主线程慢一帧。

增量时间的出现,避免了固定帧率导致固定循环次数运行在不同设备上的出现的Bug

输入延迟,第二帧按下。在多线程游戏循环下,输入直到第三帧才开始处理,图形要到第4帧结束之后才能看到,如图所示。
2-6

C++中的钻石问题与解决方案

菱形继承
虽然有很多解决方案,但是通常要尽量避免,除非有很好的理由这么做。
2-5

1
2
class Tiger : virtual public Animal { /* ... */ };
class Lion : virtual public Animal { /* ... */ }

这种写法在当有类C同时继承这两种之后就不会出现两个Animal类对象了

真实时间和游戏时间-子弹时间,一盘篮球游戏的时间

##游戏循环中的游戏对象

一个基础的游戏对象GameObject

任何游戏对象公有的功能,不管什么对象类型,都应该放在基类里。这样就可以声明两个接口,一个是Drawable对象,一个是Updateable对象。

1
2
3
4
5
6
7
interface Drawable
function Draw()
end

Updateable
function Update(float deltaTime)
end

就可以这两个接口和一个基类来表示3种游戏对象
只更新的对象

1
class UGameObject inherits GameObject,implements Updateable

只渲染的对象

1
class DGameObject inherits GameObject,implements Drawable

更新且绘制的游戏对象

1
class DUGameObject inherits UGameObject,implements Drawable

这里没有直接继承DGameObject 和 UGameObject就是避免了钻石问题

实现这三种对象之后,把它们整合在游戏循环中是很简单的。GameWorld类拥有两个列表,分别在游戏世界中管理Updateable对象和Drawable对象

强制锁定30FPS的帧率 将targetFrameTime = 33.3f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
targetFrameTime = 33.3f
while game is running
realDeltaTime = time since last frame
gameDeltaTime = realDeltaTime * gameTimeFactor

//处理输入
...
update game world with gameDeltaTime

//渲染输出
...

while(time spent this frame < targetFrameTime)

...
loop
loop

2D渲染基础

对于屏幕撕裂
喷枪在屏幕上绘制到一半时,刚好游戏循环到了”generater outputs”阶段。它开始为新的一帧往像素缓冲区写像素时,CRT还在上一帧的绘制过程中。

一个解决方案就是同步游戏循环,等到场消隐期在开始渲染。这样会消除分裂图像的问题,但是它限制了游戏循环的间隔,只有场消隐期间才能进行渲染,对于现在的游戏来说是不行的。

另一个解决方案叫作双缓冲技术。双缓冲技术里,有两块像素缓冲区。游戏交替地绘制在这两块缓冲区里。在一帧内,游戏循环可能将颜色写入缓冲区A,而CRT正在显示缓冲区B。到了下一帧CRT显示缓冲区A,而游戏循环写入缓冲区B。由于CRT和游戏循环都在使用不同的缓冲区,所以没有CRT绘制不完整的风险。

缓冲区交换要放在场消隐期进行。

1
2
3
4
5
6
function RenderWorld()
// 绘制游戏世界中所有对象
...
wait for VBLANK
swap color buffers
end

绘制精灵的算法
绘制方式先画背景色后画角色。这就像画家在画布上画画一样,也因为这样,这个算法叫做画家算法。在画家算法中,所有精灵是从后往前排序的,如下图所示。当它绘制场景时预先排好序的场景可以直接遍历渲染,得到正确的结果。
2-7

画家算法也可以运用在3D环境下,但它有很多缺陷。而在2D场景中,画家算法工作得很好。

为了保证动画的连续性,帧率最少要达到24FPS

用一组图片去表示一个角色所有状态,一个有走动和跑步的角色,每个用10帧表示,总共用了20张图片。顺序存储也就是0-9帧表示走路,10-19帧表示跑步
2-8

AnimatedSprite 要能够跟踪当前的动画数量,知道当前帧属于哪一个动画及当前动画需要用到多长时间,FPS也作为成员变量被存储了。可以通过修改FPS来让动画动态加速或减速。角色获得加速效果,让角色跑动的快一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AnimatedSprite inherits Sprite
//所有动画数据
AnimData animData
//当前运行中的动画
int animNum
//当前帧播放了多长时间
int frameNum
//
float frameTime
float animFPS = 24.0f

function Initialize(AnimData myData,int startingAnimNum)
function UpdateAnim(float deltaTime)
function ChangeAnim(int num)
------ 本文结束 ------