基于计时器的动画
作为计时器动画使用的关键类,不出意料,它就是 flash.utils.Timer。同时我们还需要
flash.events.TimerEvent 类。
使用计时器实际上与使用 enterFrame 没什么两样。只需要我们去创建一个计时器
(Timer),告诉它多久“滴答响”一声,并侦听 TimerEvent.TIMER 事件,就像对
Event.ENTER_FRAME 事件的侦听一样。哦,还要告诉计时器何时开始!接下来,计时器
就会每隔一段时间广播一个计时事件,它将调用赋给它的函数进行处理。计时器触发的间隔
以毫秒为单位,在创建该计时器时指定。让我们来看一个简单的例子(可在 Timer1.as 中找
到):
package { import flash.display.Sprite; import flash.utils.Timer; import flash.events.TimerEvent; public class Timer1 extends Sprite { private var timer:Timer; public function Timer1() { init(); } private function init():void { timer = new Timer(30); timer.addEventListener(TimerEvent.TIMER, onTimer); timer.start(); } private function onTimer(timer:TimerEvent):void { trace("timer!"); } } }重要的部分加粗表示。我们创建一个计时器,告诉它每隔 30 毫秒触发一次,意味着每秒约
33 次。添加一个事件的侦听器并将它起动。 onTimer 方法与我们以前用的 onEnterFrame
类似。
这是我们所要知道计时器的大部分内容。它还有其它两个漂亮的特征。一个是在创建计
时器时,可以通过第二个参数,repeatCount,告诉它运行的次数。假设我们要让计时器每秒
运行一次,总共执行 5 秒。就可以这样做:
timer = new Timer(1000, 5);
如果没有指定重复的次数,或传入 0,那么计时器将无限地运行。
另一个好东西是可以让计时器在某个点上启动或停止,只需要调用 timer.stop 或
timer.start 即可。在某些例子中,这样做比删除和重新加入事件侦听器更简单一些。
与 enterFrame 相比,很多朋友更喜欢使用计时器的原因是,理论上讲,计时器可以让
我们控制动画的速度——这是对于帧的不精确性的一个重大改进。我之所以说“理论上讲”,
是因为这里有些事情需要弄清楚。
首先,实际上计时器要依赖于帧频。另一个原因是计时器的事件函数中的代码会增加整
个计时器间隔。稍后我会解释一下第二点。现在,让我们看看计时器是如何与帧频相关联的。
下面是文档类 Timer2.as,使用到我们著名的 Ball 类。
package { import flash.display.Sprite; import flash.utils.Timer; import flash.events.TimerEvent; public class Timer2 extends Sprite { private var timer:Timer; private var ball:Ball; public function Timer2() { init(); } private function init():void { stage.frameRate = 1; 340 ball = new Ball(); ball.y = stage.stageHeight / 2; ball.vx = 5; addChild(ball); timer = new Timer(20); timer.addEventListener(TimerEvent.TIMER, onTimer); timer.start(); } private function onTimer(event:TimerEvent):void { ball.x += ball.vx; } } }这里我们把创建出来的小球放在舞台的左侧。让它以 vx 为 5 的速度穿过舞台。然后设置
一个 20 毫秒的计时器,每秒约调用 50 次。同时设置影片的帧频为 1 就是为了看看帧频
是否会对计时器有影响。
测试后,我们会看到小球没有平稳地穿过屏幕,而是每秒钟跳一下 —— 以帧的频率。
每跳一次都会大于 5 像素。为什么呢?
回想一下一、二章的动画基础,我们知道模型是需要更新的,所以屏幕要根据新的模型
被刷新。这里时间间隔函数确实将更新了模型并将小球每次移动 5 像素,但是只有在 Flash
进入新的一帧时才进行刷新。仅仅运行一个函数不会驱使 Flash 进行重绘。
幸运的是,Macromedia (现在的 Adobe) 的好人们看到了这个问题并给了我们另一个小
工具:updateAfterEvent。最初是在 Flash MX 中介绍的,现在它是传给计时器事件函数中
TimerEvent 对象的一个方法。就像它的名字一样,在事件之后刷新屏幕的。当然,由于它
是 TimerEvent 类的一个方法,所以只有在收到一个事件后才能被调用。(事实上,它也是
KeyboardEvent 和 MouseEvent 的方法,因此也能在这些处理函数中调用。)
这样一来,我们可以修正一下 onTimer 事件:
private function onTimer(event:TimerEvent):void { ball.x += ball.vx; event.updateAfterEvent(); }现在一切有所好转了。非常流畅。但是如果您意识到小球应该每秒更新 50 次,我们看到的
基本上应该是一个 50 fps 的动画。这就意味着小球的运动应该比第四章创建的 fps 小于
50 的 enterFrame 事件的例子更为流畅。但是实际的运动更为缓慢。
问题出来了,计时器在某种程度上要依赖于帧频。通过我的测算,在帧频为 1 fps 时,
我们所得到的计时器运行的最快间隔大约为 100 毫秒。
我已经听到了嘲笑:每帧只得到了 10 次间隔。所以,试将帧频改为 5。它允许每秒更
新 50 次。在我看来,仍然不是很流畅。如果不大于每秒 10 帧的话是不会达到真正流畅的
效果。因此,我们可以看到使用计时器并不能完全让我们从帧频的铐链中解脱出来。
下一个问题是计时器内部是怎样工作的,它会对计时的精确度产生多大的影响。当
timer.start() 被调时,实际上发生了什么,Flash 等待一段指定的时间,然后广播事件,运行
与该计时器相关的处理函数。只有当函数执行完成后计时器才开始定时下一次计时。看一个
例子,假设我们有一个每 20 毫秒运行一次计时器。假设在处理函数中有大量的代码需要执
行 30 毫秒。下一轮定时只有在所有的代码都运行完成后才开始。这样一来,我们的函数会
在大约每 50 毫秒调用一次。因为在用户的机器上没法精确地知道代码会运行多快,所以多
数情况下,计时器动画不会比帧动画精确多少。
如果您需要真正的精确,那么基于时间的动画则是我们的必经之路。
341
基于时间的动画
如果要让动画中物体的速度是一致的,那么基于时间的动画就是我们要使用的方法。这
种方法在一些游戏中比较常用。我们知道,帧和计时器动画都不能以特定的速率播放。一个
复杂的动画在一台又老又慢的电脑上运行可能要比最初设计的速度慢上许多。我们马上会看
到,使用基于时间的动画无论最终动画运行的帧频如何,都将获得可靠的速度。
首先要改变考虑速度的方法。到目前为止,在我说 vx = 5 时,我们使用的单位是像素
每帧(pixels per frame)。换句话讲,每进入新的一帧物体都将在 x 轴上运动 5 像素。在计
时器动画中,当然,就应该是每次定时间隔移动 5 像素。
对于时间动画,我们将使用真正的时间单位,如秒。由于我们是处理完整的一秒,而非
其中的一部分,因此这个速度值就要更大一些。如果某个物体的速度是每帧 10 像素,每秒
30 帧的速度运动,大约每秒 300 像素。比如下面这个例子,我从第六章的 Bouncing2.as 文
档类中截取了一部分并进行了一些变化,见下面粗体部分(也可在 TimeBased.as 中找到):
package { import flash.display.Sprite; import flash.events.Event; import flash.utils.getTimer; public class TimeBased extends Sprite { private var ball:Ball; private var vx:Number; private var vy:Number; private var bounce:Number = -0.7; private var time:Number; public function TimeBased() { init(); } private function init():void { stage.frameRate = 10; ball = new Ball(); ball.x = stage.stageWidth / 2; ball.y = stage.stageHeight / 2; vx = 300; vy = -300; addChild(ball); time = getTimer(); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { var elapsed:Number = getTimer() - time; time = getTimer(); ball.x += vx * elapsed / 1000; ball.y += vy * elapsed / 1000; var left:Number = 0; var right:Number = stage.stageWidth; var top:Number = 0; var bottom:Number = stage.stageHeight; if (ball.x + ball.radius > right) { ball.x = right - ball.radius; vx *= bounce; 342 } else if (ball.x - ball.radius < left) { ball.x = left + ball.radius; vx *= bounce; } if (ball.y + ball.radius > bottom) { ball.y = bottom - ball.radius; vy *= bounce; } else if (ball.y - ball.radius < top) { ball.y = top + ball.radius; vy *= bounce; } } } }如上所描述,我改变了对速度的计算,让它们使用固定的值,而非随机值。然后我创建了一
个名为 time 的变量,让它等于 flash.utils.getTimer 函数的结果。getTimer 函数非常简单。
它返回影片已经运行了的毫秒数 —— 这就是它全部的工作。我们没有办法清除它,重启它,
改变它,等等。它只是一个计数器。
看起来它似乎用处不大,但是如果先调用一次 getTimer 将值保存起来,稍后再调用它
一次,将两个结果相减,我们就能知道确切的—— 毫秒 ——这两次调用之间经过了多少时
间。
这就是策略:在每帧的开始时调用 getTimer 计算与上一帧间隔了多长时间。如果将它
除以 1,000,我们将得到以秒为单位的间隔时间,这是个以秒为单位的分数。由于我们的 vx
和 vy 现在是以像素每秒来计算的,因此可以让它们去乘以这个分数,这样就知道要对物体
移动多少了。同样,不要忘记重新设置 time 变量的值,以便让下一帧进行计算。
测试一下,我们将看到小球移动的速度几乎与最初的速度相同!但是真正令人激动的是
我们可以以任何帧频来发布这个影片,它仍然可以以同样的速度运动!通过修改
stage.frameRate 的值,试验一下高到 1,000 fps,低到 10 fps,你会看到小球的速度是相同
的。当然,较高的频率会让动画更加流畅,而较低的频率则会十分不连贯,但是速度是一致
的。
大家可以把这个技术应用在本书任何一个包含速度的例子中。如果这样的话,还需要将
相似的技术应用在加速度或外力上,如重力,因为它们也是基于时间的。加速度肯定要比转
前要大很多,因为加速度被定义为速度对时间的变化率。例如,重力大约为 32 英尺/秒/
秒。
如果在一个 30 fps 帧的动画中,重力为 0.5 的话,那么现在就应该是 450。计算方法
0.5 * 30 * 30。然后像这样将它加入:
vy += gravity * elapsed / 1000;
在最后一个例子中加入 450 重力后测试一下。我们会看到它与帧动画中加入重力 0.5 的效
果是相同的。使用这种技术的一个技巧是将帧频设置得非常高,如 100。虽然没有机器能够
与真正的帧频相吻合,但是基于时间的动画将保证每个人看到的影片运行得都是最流畅