游戏编程概述

游戏编程中的3个核心概念:游戏循环、游戏时间管理和游戏对象模型。

游戏循环

整个程序的核心流程控制称为游戏循环。之所以是一个循环是因为游戏总在不断执行一系列动作直到玩家退出。每迭代一次游戏循环称为1帧。大部分游戏每秒钟更新30~60帧。60FPS即游戏循环每秒钟执行60次。一个传统的游戏循环分成3部分:处理输入、更新游戏世界、生成输出。

1
2
3
4
5
while game is running
process inpus
update game world
generate outputs
loop

游戏时间管理

游戏时间管理使得我们的游戏速度可以在任何机器上得到保证。通过处理时间增量来表示游戏逻辑。如果在8MHz下游戏的FPS为30,则在16MHz下FPS为60。也就是说在30FPS的时候敌人每秒钟移动150px,则在60FPS的时候敌人每秒移动300px。为了解决这个问题,通常需要引入时间增量:从上一帧起流逝的时间。更新逻辑可以写成这样:

1
emeny.position.x += 150 * deltaTime

在30FPS下每帧移动5px,而60FPS下每帧移动2.5px。结果是每秒还是移动150px,但是60FPS的显然更加平滑。但是这也会导致奇怪的角色行为:低帧率下跳的更高。这个问题的简单解决方案是限制帧率。例如一个目标帧率为30的游戏,如果游戏循环本身只用了30ms,则需要再等待3.3ms才能进入下一次游戏循环。如果游戏遇上复杂情形导致某帧比目标帧率时长长,最常见的就是为了跟上目标帧率放弃这一帧的渲染,这就是有名的卡帧

游戏对象模型

游戏对象是每一帧需要更新或者绘制的对象:

既绘制又更新的对象:玩家、角色、敌人
只更新的对象:摄像机和看不见的触发器
只绘制的对象:静态网格,例如背景中的树木

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class GameWorld
List updateableObjects
List drawableObjects
end

while game is running
realDeltaTime = time since last frame
gameDeltaTime = realDeltaTime * gameTimeFactor

process inpus

foreach o in GameWorld.updateableObjects
o.Update(gameDeltaTime)
loop
foreach o in GameWorld.drawableObjects
o.Draw()
loop

// 帧数限制
...
loop

Q:一个简单的多线程游戏循环是如何提升帧率的?
A:如果渲染使用了30ms,游戏世界的更新使用了20ms,传统游戏循环会花费每帧50ms。但是如果将渲染放入一条自己单独的线程就可以和游戏更新并行完成。每帧总花费会降低到30ms。

Q:什么是输入延迟?多线程游戏循环是如何导致延迟的?
A:输入延迟是按下按钮到看见屏幕上按钮产生效果的时间。在多线程游戏循环中输入延迟会增加。因为渲染总是比游戏更新慢一帧,因此多了一帧的延迟。

2D图形

像素缓冲区和垂直同步

将新的一帧向像素缓冲区中写像素的时候,CRT还在上一帧的绘制当中,这就导致了屏幕撕裂,具体表现就是屏幕上同时显示了不同的两帧的各自一半的画面。更糟糕的是,新一帧的数据提交时,上一帧还没开始,这就会导致没有画面。可以采用双缓冲来解决:有两块像素缓冲区,游戏交替地绘制在这两块缓冲区里。为了完全消除屏幕撕裂,缓冲区交换必须在场消隐期进行,这就是垂直同步。游戏游戏确实允许缓冲区交换在绘制完成前尽快进行,这就会导致屏幕撕裂的可能,这种情况通常是玩家想要会的比屏幕刷新速度快的帧率。如果一款显示器有60Hz的刷新频率,则同步缓冲区交换到场消隐期最多只有60Hz,但是玩家为了减少输入延迟,可能选择消除同步以达到更高的帧率。

动画精灵

2D动画的原理是快速切换静态图片。为了保证动画的流畅性,帧率至少是24FPS(电影帧率)