游戏主循环、帧速控制

  FPS(Frame Per Second)游戏帧速60帧是指游戏每秒循环更新60次。
  一个游戏程序的基本结构像是这样:

while (isRunning)
{
	updateEverything();
}

  但是这样死循环的话,游戏的更新速度就只与CPU频率有关了,频率高每秒更新的次数更多,频率低就更少。这样我们写的游戏就不能运行得稳定。
  于是我们简单加个限制:

while (isRunning)
{
	updateEverything();
	sleep(1.0/60);//程序进程等待1/60秒
}

  这种方法我把它叫做“固定拖时间更新法”。这样updateEverything()函数每秒调用次数不会多于60次了。但是updateEverything()本身也要消耗时间,而且每次更新做的事消耗的时间也未必相同。
  于是我们用更好的办法:

/* getCurrentTime()每次调用返回当前的时间 */
float lastUpdateTime = getCurrentTime();
while (isRunning)
{
	float currentTime = getCurrentTime();
	float deltaTime = currentTime - lastUpdateTime;
    /* 每调用updateEverything()后检查时间,
     直到过去的时间达到1/60秒就进行下一次更新 */
	if (deltaTime >= 1.0/60)
	{
		lastUpdateTime = currentTime;
		updateEverything();
	}
}

  这种方法我把它叫做“累积时间更新法”。然后考虑屏幕的刷新频率与垂直同步的问题。屏幕的刷新频率一般为60Hz,就是说游戏的画面每秒也只能更新60次,让游戏逻辑和画面一起更新才是比较合理的。垂直同步是说屏幕每完成一次刷新,屏幕上的像素从左往右上往下刷新完成的瞬间会有一个叫做垂直同步完成的信号。如果不管这个信号,我们随意的刷新游戏画面,结果会出现游戏画面上面和下面出现错位或者撕裂。这样当然看起来不让人舒服。所以考虑画面刷新率和垂直同步的问题,我们又改进方法:

while (isRunning)
{
	updateEverything();
	/* 等待垂直同步信号间的空白时间,程序执行到这里会进入等待
	 一般会被封装在类似swapBuffer之类的图形API中执行,
	 但是程序要开启了垂直同步的功能才有效。
	 不过貌似现在的智能手机都默认有垂直同步的效果,
	 当然手机和PC的硬件技术不同,可能也不叫这个名字了。*/
	waitForVerticalBlank();
	drawEverything();
}

  这种方法我把它叫做“图形更新调用法”。等于每次你的逻辑更新和硬件的图形更新是同步的。此外游戏帧速的控制其实还有有一种思路,就是让每两次更新接连进行,同时获取每两次更新的时间差,像这样:

float lastUpdateTime = getCurrentTime();
while (isRunning)
{
	float currentTime = getCurrentTime();
	float deltaTime = currentTime - lastUpdateTime;
	lastUpdateTime = currentTime;
	updateEverything(deltaTime);
}

  这种方法我把它叫做“死循环并计算时间差用于更新法”。然后游戏中所有与时间相关的事件都使用更新之间的时间差进行计算,特别是对于运动物体的坐标计算,需要加入一个经过时间的参数:

void updateMove(float deltaTime)
{
	position.x += speedX * deltaTime;
	position.y += speedY * deltaTime;
}

  这样游戏帧速特别不稳定的时候游戏给人的感受也会更好些,不管更新时间差是多少,游戏在每次更新后的进展都是稳定的。而之前在固定时间差进行更新的做法,当某些更新运算工作量很大的时候,游戏的进展就会被明显的延后。
  结合起来个人认为比较完美的办法是:

//假设目标是60帧/秒更新的游戏
float lastUpdateTime = getCurrentTime();
while (isRunning)
{
	float currentTime = getCurrentTime();
	float deltaTime = currentTime - lastUpdateTime;
	lastUpdateTime = currentTime;
    /* 如果游戏太卡,过长的时间差可能会导致跳过一些不能跳过的游戏逻辑,
     所以做一些人为限制 */
    if (deltaTime > 1.0/30) deltaTime = 1.0/30;
	updateEverything(deltaTime);
	waitForVerticalBlank();
	drawEverything();
}

  这种方法我把它叫做“累积时间半固定时长等图形更新并将时间差用于更新法”(泥垢了)。不过这个办法只针对单机游戏有效,如果是网络游戏,考虑客户端之间的同步问题的话,帧速不稳定地变来变去是不好的,我们可以记录过去的时间里跑过的帧数,如果达不到目标帧数就连续进行更新直到赶上需要的帧数。

//假设目标是60帧/秒更新的游戏
float startTime = getCurrentTime();
long passedFrames = 0;
while (isRunning)
{
	float currentTime = getCurrentTime();
	float totalTime = currentTime - startTime;
	long targetTotalFrames = totalTime/(1.0/60);
	/* 如果某一次更新耗时太久,则会导致passedFrames和targetTotalFrames差太多,
	 所以就连续更新好几次逻辑来赶上目标的更新次数,以保证游戏的进度稳定*/
	while (passedFrames < targetTotalFrames)
	{
		updateEverything();
		passedFrames++;
	}
	/* 图形更新应该在逻辑完全完成更新以后才进行 */
	drawEverything();
}

  最初开始学写游戏的时候我也不知道原来“回字有这么多写法”,在从最早开始用Windows GDI,DirectX写游戏,后来使用游戏框架HGE,XNA,iTownSDK,再后来研究Cocos2d-x的各平台底层代码,发现就是这样的一个点,由不同的人在不同的程序和应用环境下各有写法,有的方法好像研究得很深,有的又比较简单,但是最后发现它们都是有迹可循的,无非都是不断的提出新的需求,并逐步地积累改进而产生的成果。这么多的“回字写法”我把它叫做游戏的程序基础,并非因为它是写游戏程序的必备知识,其实我觉得,游戏程序是很复杂,但无非也是通过时间经验积累而演变和深入的,每多写一行代码,多实现一个新的功能就获得了一个明确的积累和进步。

标题目录