《JavaScript高效图形编程(修订版)》——2.3 定时器、速度和帧速率

  1. 云栖社区>
  2. 博客>
  3. 正文

《JavaScript高效图形编程(修订版)》——2.3 定时器、速度和帧速率

异步社区 2017-05-02 13:29:00 浏览882

本节书摘来自异步社区《JavaScript高效图形编程(修订版)》一书中的第2章,第2.3节,作者:【美】Raffaele Cecco著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 定时器、速度和帧速率

本节将讨论如何在JavaScript中控制图形的更新速度以保证用户体验。我们想要图形有平滑流畅的移动,不要太快也不要太慢。用户计算机的性能会影响图形更新的速度。下面我们将讨论减少不同机器上速度差异的解决方案。

2.3.1 使用setInterval和setTimeout
JavaScript的setInterval()和setTimeout()函数使你可以定期调用JavaScript代码。需要定期更新图形的应用,比如电脑游戏,几乎都离不开这些函数。

你可以将回调函数传给setInterval()来重复调用此函数:

screenshot

注意执行bigFunction()花费20毫秒。如果循环间隔比这个值小呢?

screenshot

看起来20毫秒的bigFunction()会在第一个回调函数返回之前被重新调用。实际上,新的回调将排在队列中直到前面的回调函数结束。

如果延时更短点呢?

screenshot

可以预计每执行一个回调函数,若干个回调函数在排队。事实上,通常的行为是只有一个排队的bigFunciton()会激活。排队的回调函数会在第一个回调函数结束后立即执行吗?有可能,但不一定。其他时间和浏览器中运行的代码可能使得setInterval()回调函数被延时或丢弃。回调函数甚至有可能连续发生(比规定的间隔短),如果JavaScript发现一个时间窗口,可以清除队列。

这里想要说明的是:不能保证回调函数以指定的间隔执行。

setTimeout()在指定的延时后调用一个函数,和setInterval()类似,但可预见性更强。

screenshot

这会在50毫秒后,调用一次bigFunction()。和setInterval()一样,这个延时仅仅是一个参考。

你可以用setTimeout()连续调用一个函数,其行为将比setInterval()更可预见:

screenshot

每当bigFunction()结束,它设置另一个以自己作为回调函数的setTimeout()。

在这个例子中,尽管设置的timeout值比bigFunction()执行时间要短,回调函数只会在bigFunction()结束后再执行。实际上,执行的频率和下面使用setInterval()的代码类似:

screenshot

2.3.2 定时器精度
Windows下的浏览器只有粗粒度的定时器。例如Windows XP的底层操作系统定时器提供15毫秒精度。这意味着Date()、setInterval()和setTimeout()等JavaScript函数不能提供可靠的15毫秒以下的定时。Google Chrome是例外之一,它将Windows切换到一个准确的定时器模式并提供1毫秒的精度。

这里的要点是一个应用程序不应该依赖低于15毫秒(约1/64秒)的定时器。这个问题严重吗?一般情况下不严重。浏览器中不太可能或不应该运行对时间这么敏感的应用程序。动画也许会比预计的慢一点或快一点,游戏等应用程序中帧率也不是绝对的稳定。如果在一段时间内细致检查这些不精确的累加效果,也许可以看到一定的误差。不过,在通常情况下,比如玩游戏或看菜单特效时,这些误差是察觉不到的。

不过在使用Date()进行代码性能分析时要小心。下面的例子中,如果执行的代码太快结束的话,将得到不准确的结果:

screenshot

一个更好的解决方案是在较长的时间段(如1秒)内循环执行代码,然后用期间完成的迭代次数来衡量执行速度。

2.3.3 保持速度一致
前面的sprite实现,具体来说是移动sprite的代码,存在一个问题——不同的浏览器下动画和移动的速度(即帧率)不一样。比如2.8GHz的PC、Opera或Google Chrome等浏览器可以在移动100个Sprite时轻松达到50FPS(每秒帧数),Firfox也许能有30FPS,而IE8也许只有25FPS。如果考虑不同的硬件,帧率的差异会更大。

这对装饰性的动画和特效不是大问题,但游戏等应用程序需要一致的移动速度来保证可玩性。

为在不同的软硬件环境中保持速度一致,必须在sprite移动和动画涉及的计算中考虑帧率的不同。具体来说,一个以30FPS每帧移动两个像素的sprite,和一个以60FPS每帧移动一个像素的sprite看起来速度一样。这两者之间的主要视觉区别是30FPS sprite的移动不如60FPS sprite的移动流畅。不过至少看起来他们是以同样的速度在屏幕上移动。

为此,必须计算一个时间系数,并在移动和动画代码中使用。表2-4显示了一个例子。

screenshot

很明显,时间系数=目标FPS/实际FPS。

为计算实际FPS,可以用JavaScript的Date对象记录当前时间(即开始时间,单位为毫秒位)。在执行所有应用逻辑后再记录时间(结束时间)。下面是代码:

screenshot

如果CPU负荷过重,帧率会很慢。以6FPS、每帧10像素在屏幕上移动的sprite看起来不平稳,肯定不适合游戏。表2-5列出了帧率和流畅度的对应关系。
screenshot

这不是说10FPS的低帧率毫无用处。对俄罗斯方块这样的游戏而言,这种帧率也许就可以接受了。

现在我们创建一个timeInfo对象,它将提供保持应用速度一致所需的所有功能。它接受一个goalFPS参数,即我们想要达到的目标FPS。如果达不到,函数将调整移动速度使其至少看起来达到了goalFPS。timeInfo对象中还提供了其他时间相关的信息。

下面的函数返回一个对象,其中包含getInfo()方法。getInfo()方法返回一个对象,其属性如表2-6所示。

screenshot

paused变量表明这是在应用程序开始或暂停后,getInfo()第一次被调用。它保证在经过一个很长的暂停之后,getInfo()传回的值是良性的,并且不会返回一个非常大的值。

screenshot

我们通过从上一次getInfo()中记录的oldTime,减去新时间,得到经过时间(elapsed time)。然后用经过时间来计算帧率。+new Date()语句等价于new Date(). getTime();:

screenshot

然后返回一些有用的信息属性,如表2-6所示。

screenshot

接着我们定义pause()方法,在暂停应用程序时都应调用此方法。

screenshot

现在,我们可以在原始的bouncySprite和bouncyBoss代码中使用timeInfo对象了:

screenshot

moveAndDraw方法现在接受时间系数作为参数。计算和原来相似,但使用了时间系数。changeImage()函数的参数应该是整数,但因为animIndex受时间系数影响,也许不是整数。为此,我们复制一个整数版的animIndex为animIndx2,并传入changeImage():
screenshot

bouncyBoss对象现在要创建一个目标FPS为40的timeInfo实例(存在timer变量中)。moveAll()在每个迭代调用timeInfo.getInfo()得到时间系数,并将其传给每个bouncySprite实例的moveAndDraw()方法。注意只需要一个timeInfo实例即可,因为每个bouncySprite实例可以使用同一个系数。