第一章 游戏循环

前言

该内容是 《C++游戏编程:创建3D游戏》 的笔记,作者用它来学习游戏引擎最基础的知识,即归纳要点,因此不会在里面插入过多的代码内容。

什么是游戏循环

通常来讲,游戏程序与别的程序不同的是,只要游戏程序在运行,每一秒都需要对游戏程序多次更新(纯文字游戏可能不需要)。通常来讲,每一次完整的游戏循环被称为一帧(frame)。60帧率(FPS frame per second)的游戏意思是游戏每秒完成60次循环。

游戏循环的工作

归纳来讲,每次游戏循环游戏引擎都在做这三个动作:

  1. 处理输入
    指处理和游戏有关的任何输入,对于通常的PC即处理键鼠输入,如果有手柄等外接设备则需要处理手柄的输入。而对于联网游戏来说,需要处理来自服务器端的输入。对于AR游戏,也需要处理相机相关的输入。

  2. 更新游戏世界
    检查游戏中每个对象的状态,并且对这些对象进行更新。更新游戏世界可能涉及非常多的对象,包括游戏中的人物,UI,音频等内容。以及时钟等非直观可见的游戏对象。

  3. 生成输出
    生成游戏的一切输出,最直观的输出即图形输出,同时也包括音频等输出内容,震动也是一种输出形式。

注:这是标准的单线程游戏循环,多线程游戏循环会更加复杂。

基本的2D图形

绝大多数显示屏使用的是**光栅图形(raster graphics)**,即显示屏是具有图片元素(像素 pixel)的二位网格。分辨率指的是显示屏像素网格的大小,如1920x1080分辨率通常称为1080p,表示有1080行像素,每行有1920个像素点。RGB(red green blue)是一种最常见的颜色表示方式,显示屏可以通过这三种颜色混合组成多种颜色。

上面提到的是屏幕图形的显示,在图形具体相关计算中,有着不同的色彩表示法,比如HSV,RGBA。在RGBA中,A(alpha)表示透明度。对计算机色彩感兴趣可以了解更多的色彩空间 Color Space相关内容。

颜色缓冲区

在计算机图形中,颜色缓冲区(不严谨可以也称为 帧缓冲 frame buffer,但是帧缓冲是个更大的范围,其包括颜色缓冲以及其它缓冲)是在包含整个屏幕颜色信息的一段内存位置。显示器通过缓冲区中的信息绘制屏幕内容,颜色缓冲区的具体大小取决于分辨率与颜色深度(color depth)。常用的颜色深度为24位,即八位存储的RGB信息。具体的RGB值通常使用无符号数表示,即范围为[0,255]的整数。还有一种做法是使用浮点数(0.0~1.0)表示颜色,这样做的好处是可以对色彩进行舍入。

双缓冲

游戏画面在动的是因为屏幕在快速地绘制,但是刷新频率与游戏的帧速率并不相同,比如一个60FPS的游戏可能在一个屏幕刷新频率为144Hz的电脑上运行。屏幕更新并不是一次性的,而是按行按列或者棋盘方式更新的。如果游戏直接使用屏幕当前的颜色缓冲区作为颜色输出缓冲,那么屏幕与游戏更新速率的不匹配可能会导致当屏幕正在输出上一帧的图像,游戏已经将下一帧图像的内容写道缓冲区了,这会造成屏幕撕裂(screen tear)。
screen tear

双缓冲(double buffer) 是一种处理屏幕撕裂的有效手段,其使用两个颜色缓冲区,其中一个用于显示,另一个用于写入。假设现在有两个游戏缓冲区A B,屏幕现在正在使用缓冲区A,游戏现在在写入缓冲区B。当游戏完成写入后,将缓冲区A和B交换,接下来显示屏将显示缓冲区B的内容,而游戏将会写入缓冲区A。用于写入的缓冲区称为**后台缓冲区(back buffer),用于显示的缓冲区称为前台缓冲区(front buffer)**。这两个缓冲区会不断交换,直到游戏结束。
double buffer

但是双缓冲不能完美解决屏幕撕裂的问题,如果游戏刷新速率过快,那么会出现当屏幕正在读取缓冲区A的时候游戏突然开始写入A。这种现象很罕见,但是仍然是需要避免的。处理这种问题最常采用的方式是**垂直同步(vsync)**。其机制是只有当屏幕完成刷新后,才允许游戏交换缓冲区。这样做有个缺点就是无法确保游戏循环达到某个精确的FPS,这可能会导致游戏的卡顿现象。因此垂直同步通常都是一个提供给玩家的选项,让玩家来决定是否使用。

现在有部分计算机能提供自适应刷新频率的功能,即让游戏决定什么时候进行刷新。

游戏更新

绝大多数游戏具有时间的概念,尤其对于实时游戏。但是游戏并不是连续刷新的,而是按帧刷新的。30fps的游戏更新间隔为33ms左右,因此30fps的每一次游戏循环都应该模拟33ms的时间间隔。

游戏时间与现实时间

游戏时间与现实时间并不是完美的1:1关系,比如有暂停,枪弹时间等机制的游戏。这些机制会导致游戏与现实世界的时间不同,但是我们更新游戏的时候,仍然应该以游戏时间更新。

基于增量时间的游戏更新

早期的处理器比较慢,游戏程序通常有个特定帧率。一个在8MHz处理器运行的一段代码可能如这样:

1
2
// enemy moves 5 pixels in x direction every update
enemy.position.x += 5;

如果处理器为16MHz,那敌人的移动速度将会是两倍与其在8MHz处理器上的表现。现在的处理器比以往快得多,如果只是这样写的话游戏简直没法玩。因此,为了解决这个问题,引擎使用**增量时间(delta time)**的方式使得将游戏更新从每帧更新逻辑变成每秒更新逻辑。这样就不会导致不同速率的处理器上的游戏更新画面差异过大。游戏中的所有内容都应该根据游戏增量时间更新。

1
2
//enemy moves 5 pixels in x direction every update
enemy.position.x += 5*deltaTime;

因此,游戏更新的逻辑伪代码如下:

1
2
3
4
5
6
lastUpdate // shows the time of last update 
while game is running
deltaTime = currentTime - lastUpdate
update all game objects with deltaTime
lastTime = currentTime
end while