每个游戏基本上都包含自己独有的时间计算方式,从简单扫雷到文明那种大型的策略游戏。有些时间是显示的,例如扫雷中的计时器,而有些是隐性的,例如 FPS 射击游戏,但不管怎样,你应该都或多或少的感觉到时间的流逝,并且很容易注意到游戏中的时间和现实中的时间一般是不匹配的,例如模拟人生中的小人,几个小时就能让你的小人经历从出生到死亡的整个人生,或许这也算是一种游戏中所独有的特殊体验吧。

1. 游戏中的时间轴

当然今天我们并不准备讨论时间的哲学概念,而是想研究研究游戏中的时间是怎样计算。要想计算游戏中的时间,首先我们要有一个基准时间,只有这样才有参考的依据,这里可以直接选用现实中的时间作为参考。虽然有了参考,但是我们仍然无法度量我们游戏中消耗的时间,因为我们无法在游戏中得知现实中已经过去了多久,这显然是一个需要解决的问题,如果无法解决这个问题,我们是无法将现实中的时间和计算机中的时间进行准确换算的。

其实这个问题,早就已经被解决了,因为计算机硬件本身是包含时钟的,我们可以直接使用 CPU 的高分辨率计时寄存器来度量时间,这种时间是可以和现实中的时间相互转换的,我们可以将他们称之为真实时间轴

这东西是怎么计算的,又是怎么和现实中的时间换算的呢?

在 Windows 中,有一个 API 接口叫做 QueryPerformanceCounter ,这个函数的作用就是获取当前 CPU 从开机时刻起到执行这个函数之间所经历的时间间隔,这个时间间隔并不是真正现实中的时间单位,而是一个“嘀嗒”计数。最重要的是我们可以用另一个 API 接口 QueryPerformanceFrequency 来获取 1 秒钟的“嘀嗒”个数,这样用总的嘀嗒时间 除以 1秒钟的嘀嗒个数,就可以算出从开机开始 CPU 经过的现实时间。

用代码描述如下:

LARGE_INTEGER nFrequency, nPrevTime, nCurrTime, nElapsedCounter;

//获取频率,每秒钟的嘀嗒个数
QueryPerformanceFrequency(&nFrequency);
//获取第一个嘀嗒数
QueryPerformanceCounter(&nPrevTime);

//TODO: 做一些事情
Sleep(5)

//获取第二个嘀嗒数
QueryPerformanceCounter(&nCurrTime);

//中间间隔的嘀嗒数 
nElapsedCounter.QuadPart = nCurrTime.QuadPart - nPrevTime.QuadPart;

//计算现实的消耗时间
nElapsedCounter.QuadPart * 1000.0f / nFrequency.QuadPart;

因为 QueryPerformanceFrequency 返回的是微妙,所以这里乘以 1000,换算成毫秒单位。SDL 中一样存在这样的 API 接口:

Uint64 SDL_GetPerformanceCounter(void)
Uint64 SDL_GetPerformanceFrequency(void)

用法和前面的 Windows 版本没什么区别,只不过名字和返回值和参数类型略有差异而已。

2. 游戏中帧率的计算方法

知道了游戏中时间的计算方法,那么就可以学习如何计算游戏中的帧率,首先说一下什么是“帧”。

帧就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。 一帧就是一幅静止的画面,连续的帧就形成动画,如电视图象等。 我们通常说帧率,简单地说,就是在1秒钟时间里传输的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次,通常用 fps(Frames Per Second)表示。每一帧都是静止的图像,快速连续地显示帧便形成了运动的假象。高的帧率可以得到更流畅、更逼真的动画。

既然知道帧率的概念,结合上面讲述游戏中计算时间的方法,计算帧率是非常容易的。首先我们先通过 SDL_GetPerformanceCounter 得到一帧消耗的嘀嗒数,然后除以 SDL_GetPerformanceFrequency 获取的频率,这样就可以得到一帧所消耗的时间,这个时间单位是纳秒,所以我们可以通过 1 * 1000 * 1000 除以上面计算的时间就可以得到我们想要的帧率了。

//main.c

Uint64 nFrequency, nPrevCounter, nCurrCounter, nElapsedCounter;
float elapsed = 0.0f, totalTime = 0.0f;
int fps = 0, fpsCount = 0;

nFrequency = SDL_GetPerformanceFrequency();
nPrevCounter = SDL_GetPerformanceCounter();

while (!quit) {
	if (SDL_PollEvent(&evt)) {
		if (evt.type == SDL_QUIT) {
			quit = 1;
		}
	}
	else {
		nCurrCounter = SDL_GetPerformanceCounter();
		nElapsedCounter = nCurrCounter - nPrevCounter;
		nPrevCounter = nCurrCounter;

		//前后两帧的耗时(ms)
		elapsed = (nElapsedCounter * 1000.0f) / nFrequency;

		//清除背景
		SDL_SetRenderDrawColor(pRenderer, 0, 0, 0, 255);
		SDL_RenderClear(pRenderer);

		//TODO: 绘制其它

		//显示图像
		SDL_RenderPresent(pRenderer);


		totalTime += elapsed;
		//已经过了 1 秒
		if (totalTime > 1000.0f) {
			totalTime -= 1000.0f;
			fps = fpsCount;
			fpsCount = 1;

			SDL_Log("%d\n", fps);

		}
		else {
			fpsCount++;
		}
	}
}

其中 fps 中保存的就是当前游戏的帧率。