整个游戏程序的核心流程控制称为游戏循环。之所以是一个循环,是因为游戏总是不断地执行一系列动作直到玩家退出。每迭代一次游戏循环为1帧。大部分实时游戏每秒钟更新30-60帧。如果一个游戏跑60FPS(帧/秒),那么这个游戏循环每秒要执行60次。
假设对于某个游戏来说,渲染整个场景需要30毫秒,它还需要额外的20毫秒去更新游戏世界。如果这些都在同一个线程执行,每帧将耗时50毫秒,最终导致低帧率——20FPS。但如果渲染和更新逻辑同步执行,每帧只要30ms,30FPS的目标就可以完成。
为了完成以上想法,主线程必须处理所有输入、更新游戏世界、处理所有图形以外的输出。它必须提交相关数据给第二条线程,那么第二条线程就可以渲染所有图像。
但是,当渲染线程绘制的时候,主线程该干什么?我们不想它简单地等着渲染结束,因为这样比单线程还慢。解决的办法是让渲染线程比主线程慢1帧。这个方法的缺点就是会增加输入延迟,玩家的输入要更久才能在画面上有所反馈。假设跳跃键在第2帧就按下。在多线程游戏循环下,输入直到第3帧才开始处理。图形要到第4帧结束才能看到。
------------------------------------ 早期的游戏经常以特定处理器速度来处理逻辑。例如,处理敌人位置的代码可能如下:
// 更新x位置5个像素 enemy.position.x += 5 在这种情况下,敌人一定伪代码每秒执行30次(30FPS),敌人在1秒就移动150个像素。可是在60FPS的帧率下,敌人在同样的1秒会移动300个像素。为了解决这样的问题,需要引入增量时间的概念:从上一帧起流逝的时间。
为了应用增量时间,先前的伪代码中的移动不能再使用每帧移动的像素来表示,而是应该使用每秒移动的像素。如果理想的移动速度是每秒150像素,那么代码可以改成这样:
// 更新x位置150像素/每秒 enemy.position.x += 150 * deltaTime 现在代码不管帧率如何都能正常工作。在30FPS的帧率下,敌人会每帧移动5个像素,总共每秒150个像素。在60FPS的帧率下,敌人每秒只会移动2.5个像素,但总共还是每秒150个像素。虽然在60FPS下的移动更加平滑,但是总体上每秒的移动速度是一致的。 ------------------------------------ 限制帧率,强制游戏循环等待到指定帧率才继续。比如一个目标帧率为30FPS的游戏,如果游戏循环本身只用了30ms,那么还要等待额外的3.3ms才能开始执行下一次的游戏循环。
还有一种情况需要考虑:如果游戏突然遇上复杂情形,导致某帧比目标帧率的时长长怎么办?有很多解决办法,最常见的就是为了跟上目标帧率,而丢弃这一帧的渲染。这就是有名的卡帧,这么做会引起视觉上的卡顿。你可能会注意到有时候玩游戏的时候,做某些事情就会卡一下。 ------------------------------------ 游戏对象的类型 在3种游戏对象中,最常见的就是更新和绘制都需要的对象。任何角色、生物或者可以移动的物体都需要在游戏循环中的update game world阶段更新,还要在generate outputs阶段渲染。
只绘制不更新的对象,称为静态对象。这些对象就是那些玩家可以看到,但是永远不需要更新的对象。它可以是游戏背景中的建筑。一栋建筑不会移动也不会攻击玩家,但是需要绘制。
第三种游戏对象,就是那些需要更新,但不需要绘制的对象。例如,摄像机和触发器。 ------------------------------------- while game is running realDeltaTime = time since last frame gameDeltaTime = realDeltaTime * gameTimeFactor
//处理输入 ...
//更新游戏世界 foreach Updateable o in GameWorld.updateableObjects o.Update(gameDeltaTime) loop
//渲染输出 foreach Drawable o in GameWorld.drawableObejcts o.Draw() loop
//帧数限制代码 ---------------------------------- 接着的思路是基于上帧到现在有多少真实时间流逝来选择前进的时间。 这一帧花费的时间越长,游戏的间隔越大。 它总能跟上真实时间,因为它走的步子越来越大。 有人称之为变化的或者流动的时间间隔。它看上去像是:
double lastTime = getCurrentTime(); while (true) { double current = getCurrentTime(); double elapsed = current - lastTime; processInput(); update(elapsed); render(); lastTime = current; } 每一帧,我们计算上次游戏更新到现在有多少真实时间过去了(即变量elapsed)。 当我们更新游戏状态时将其传入。 然后游戏引擎让游戏世界推进一定的时间量。 ------------------------------------
|