定义
一个游戏循环会在游玩时不断运行。 每一次循环,它都会无阻塞地处理玩家的输入,更新游戏的状态,渲染游戏。它追踪时间的消耗并控制游戏的速度。游戏循环需要做到始终以固定的速度运行游戏。
一个游戏循环中通常包含处理输入部分,物理,AI等游戏逻辑部分,以及渲染部分。这一章给出了一些实现游戏循环的方法。
1.固定时间步长,没有同步
用while,尽可能快地运行游戏, 能跑多快跑多块。
while (true)
{
processInput();
update();
render();
}
优点就是一个简单,没了
缺点是,游戏速度直接会受到硬件处理的速度以及单次循环中的复杂度的影响。在你的外星人上,子弹可能会飞的非常快。在你爷爷的大屁股电脑上,子弹会变得非常慢。
2. 固定时间步长,有同步
就是说在之前的基础上,每次循环后等一会。调用sleep()方法,让这个循环在固定时间段运行。
while (true)
{
double start = getCurrentTime();
processInput();
update();
render();
sleep(start + MS_PER_FRAME - getCurrentTime());
}
优点,简单,省电,游戏不会运行得太快。
缺点是如果执行方法花了很多时间,游戏仍然会运行的过慢。
3.动态时间步长
用真实的时间做循环,每次循环会计算上一次循环到这一次循环所用的时间间隔,然后在update的时候传入这个时间间隔。这个方法很少见,几乎没有人会用。
double lastTime = getCurrentTime();
while (true)
{
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}
优点,能适应并且调整,避免游戏运行的过快或者过慢
缺点,这会导致很多东西都不稳定。在物理和网络部分使用动态时间步长会遇见更多的困难。例如有些电脑上一个子弹一秒更新50次位置,另一台电脑上这颗子弹只更新了5次,浮点数的舍入偏差会导致这个子弹不同步。
4.扔掉渲染帧
以固定的时间步长更新游戏,但是在动态时刻渲染。
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;
processInput();
while (lag >= MS_PER_UPDATE)
{
update();
lag -= MS_PER_UPDATE;
}
render();
}
在代码中可以看到,这个循环会计算上一次循环到现在间隔的时间,如果间隔时间过长,就会进入里边的小循环,多次调用update方法以追上现实时间。当最终追上后,才会结束,调用渲染,结束整个大循环。
技巧在于我们将渲染拉出了更新循环。 这释放了一大块CPU时间。 最终结果是游戏以固定时间步长模拟,该时间步长与硬件不相关。 高端硬件玩家会有多次渲染,游戏体验更加平滑,而使用低端硬件的玩家看到的内容会有抖动。
优点是能适应并调整,避免运行得太快或者太慢。 只要能实时更新,游戏状态就不会落后于真实时间。如果玩家用高端的机器,它会回以更平滑的游戏体验。
缺点是比较复杂,你需要将更新的时间步长调整得尽可能小来适应高端机,同时不至于在低端机上太慢。一种方法是限制小循环的最大次数,来防止低端机上跑得太慢的问题,本质上就是扔掉一些逻辑帧。
有时候会出现这种情况:
由于渲染是动态的,在每次大循环结束才开始,中间有时候会隔着好多个小循环。所以有时候会出现渲染在两次更新之间的情况,而这个时候,渲染的往往是前一次更新的时候的情况,这会造成困扰,例如玩家看见子弹在左边,下一次渲染的时候应该在中间但还是显示在左边。在下一次渲染的时候就变成了右边,而不是玩家期待的中间。
解决方法就是 给render()方法也传入时间,让其插值预测来达到平滑的目的。
当我们要渲染时,我们将它传入:
render(lag / MS_PER_UPDATE);
假设子弹在屏幕左边20像素的地方,正在以400像素每帧的速度向右移动。 如果在两帧正中渲染,我们会给render()传0.5。 它绘制了半帧之前的图形,在220像素,啊哈,平滑的移动。