基于计时器与时间的动画
原文地址:http://hi.baidu.com/inbyte/blog/category/Flash
另外参考:http://www.allwiki.com/wiki/Flash%E4%B8%AD%E7%9A%84%E7%89%A9%E7%90%86%E8%BF%90%E5%8A%A8%E5%8A%A8%E7%94%BB-%E9%80%9F%E5%BA%A6
基于计时器与时间的动画
到目前为止本书的所有例子都是通过把运动代码放到 onEnterFrame 方法中并将它赋给一个 enterFrame 事件的处理函数来实现的。我一直认为这是最简单的一种方式,因为帧的概念在 Flash 中根深蒂固,它就是给我们准备的;我猜我们大多都习以为常了。
然而,对于那些从非 Flash 编程环境转来的朋友,对于这种模式可能并不习惯。对于他们来说,时序动画模型(使用 Interval 或 Timer)似乎可以更加精准地控制动画。
稍后,我们要来看看“基于时间的动画”,一种即能用作帧又能用作计时器的技术。
基于计时器的动画
作为计时器动画使用的关键类,不出意料,它就是 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;
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 毫秒调用一次。因为在用户的机器上没法精确地知道代码会运行多快,所以多数情况下,计时器动画不会比帧动画精确多少。
如果您需要真正的精确,那么基于时间的动画则是我们的必经之路。
基于时间的动画
如果要让动画中物体的速度是一致的,那么基于时间的动画就是我们要使用的方法。这种方法在一些游戏中比较常用。我们知道,帧和计时器动画都不能以特定的速率播放。一个复杂的动画在一台又老又慢的电脑上运行可能要比最初设计的速度慢上许多。我们马上会看到,使用基于时间的动画无论最终动画运行的帧频如何,都将获得可靠的速度。
首先要改变考虑速度的方法。到目前为止,在我说 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;
} 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。虽然没有机器能够与真正的帧频相吻合,但是基于时间的动画将保证每个人看到的影片运行得都是最流畅的。
同质量物体的碰撞
回忆一下第十一章的动量守恒。那里有非常严谨的代码。不过,当两个相同质量的物体发生碰撞时,我们实现起来可以更简单一些。基本原理是,两个物体沿着碰撞的线路交换它们的速度。同时还要用坐标旋转来决定碰撞的线路,以及物体的速度,这样就摆脱了复杂的动量守恒公式。来看看它是如何工作的,让我们回到文件 MultiBilliard2.as 中,将用它作为下一个例子 SameMass.as 的基础。我就不再列出原先所有的代码了,因为它实在是一个很大的文件。但是,我们要来看一下创建所有小球的 for 循环:
for(var i:uint = 0; i < numBalls; i++) {
var radius:Number = Math.random() * 50 + 20;
var ball:Ball = new Ball(radius);
ball.mass = radius;
ball.x = Math.random() * stage.stageWidth;
ball.y = Math.random() * stage.stageHeight;
ball.vx = Math.random() * 10 - 5;
ball.vy = Math.random() * 10 - 5;
addChild(ball);
balls.push(ball);
}
对于新的文件来说,要把粗体字的部分改为这一行:
var radius:Number = 30;
让所有小球大小都相同,消除了质量的概念,相当于给它们相同的质量。
接下来,看一下 checkCollision 函数。请找到这一部分:
// 旋转 ball0 的速度
var vel0:Point = rotate(ball0.vx,
ball0.vy,
sin,
cos,
true);
// 旋转 ball1 的速度
var vel1:Point = rotate(ball1.vx,
ball1.vy,
sin,
cos,
true);
// 碰撞反应
var vxTotal:Number = vel0.x - vel1.x;
vel0.x = ((ball0.mass - ball1.mass) * vel0.x +
2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass);
vel1.x = vxTotal + vel0.x;
这一部分找出了碰撞线路上的速度,根据物体的质量求出碰撞的结果。“碰撞反应”部分是动量守恒的要素,这就是我们可以消除的部分。我们可以让 vel0 和 vel1 进行交换,就可以很容易地替换它了。整个代码段如下:
// 旋转 ball0 的速度
var vel0:Point = rotate(ball0.vx,
ball0.vy,
sin,
cos,
true);
// 旋转 ball1 的速度
var vel1:Point = rotate(ball1.vx,
ball1.vy,
sin,
cos,
true);
// 交换两个速度
var temp:Point = vel0;
vel0 = vel1;
vel1 = temp;
这里甚至还可以再优化,但是为了代码的清晰我就不做改变了。现在我们已经摆脱了一小块儿数学问题,测试一下修改前与修改后的文件,所得的结果是相同的。
声音整合
本书一直没有提到声音的使用。因为声音并不是动画的直接组成部分,优质的声音效果可以让 Flash 影片更加真实、引人入胜。
我们可以使用不同的方法来加入声音。回溯到 Flash IDE 最早的程序版本,我们有一种使用声音的特殊方式 —— 将声音导入到库,再把它加入到帧里面。这不是我要介绍的方法。我将介绍在 AS 3 中使用声音的一些基础。
AS 3 的 Sound 类有了很大的变化,而且还有几个额外的类可以帮助我们对其进行修饰。这里没有太多的空间进行深入的讨论,但是有一个方面我想应该对于我们这本书来说会很有用。这就是当动画中发生某种事件时,应该播放声音。最明显的就应该是碰撞了。一个小球碰到墙上或其它小球上,我们会听到“砰”、“啵嘤”、“啪”或其它什么声音。因此,我们需要掌握通过 ActionScript 来启动声音的能力。
这个例子中,我们还要回到 Bouncing2.as,小球会与墙壁产生碰撞。每次碰撞到墙壁时,我想让它发出声音。新的类请见 SoundEvents.as。
首先,需要有声音效果。在网上有许多免费的声音效果资源。其中最火的 Flash 声音网站是 FlashKit。他们的音乐文件除了有 loop 以外,还有一个声音效果库 www.flashkit.com/soundfx/。这些效果被分类为 Cartoon,Interfaces,Mechanical 等等,而且这个网站有超过 6,000 多个声音效果文件,所以您应该能够找到适合自己的音效。我们可以在页面上直接进行预览(preview),找到自己喜欢的文件后,以 MP3 格式进行下载。将它保存到硬盘上与最终发部的影片放在同一目录下。
有时我们要重新给文件一个更为简单的名字。例如,我下载了一个“boing”声音,我就将它重命名为 boing.mp3。
在 AS 3 中使用声音的基础实际上要比 AS 2 中简单一些。
首先,我们需要创建声音对象。假设在类中已经声明了一个名为 boing 的变量:
private var boing:Sound;
创建一个声音对象就这么简单:
boing = new Sound();
当然,如同大多数 AS 3 的类一样,Sound 类也在包中,flash.media 包,因此要确保先导入 flash.media.Sound。
连接一个外部声音对象最简单的方法是在构造函数中传入一个请求(request)。
就像读取外部图像(第四章)一样,我们不能直接传入外部声音文件的 URL。而是要将它包装到 URLRequest 中(flash.net.URLRequest,需要导入它)。应该像这样:
boing = new Sound(new URLRequest("boing.mp3"));
全部内容就是这样。现在声音已经准备好。我们要做的就是:
mySound.play();
无论在哪儿都会播放出这个音效。在 play 中有一些可选参数,如偏移的毫秒数,以及播放的次数,但是默认情况下是从声音的起始位置播放一次声音,这已经满足了我们通常的需求。以下是 SoundEvents.as 的全部代码,展示了 Sound 对象的创建,无论何时小球碰撞到墙上,都会播放声音。
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.media.Sound;
import flash.net.URLRequest;
public class SoundEvents extends Sprite {
private var ball:Ball;
private var vx:Number;
private var vy:Number;
private var bounce:Number = -0.7;
private var boing:Sound;
public function SoundEvents() {
init();
}
private function init():void {
boing = new Sound(new URLRequest("boing.mp3"));
ball = new Ball();
ball.x = stage.stageWidth / 2;
ball.y = stage.stageHeight / 2;
vx = Math.random() * 10 - 5;
vy = -10;
addChild(ball);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
ball.x += vx;
ball.y += vy;
var left:Number = 0;
var right:Number = stage.stageWidth;
var top:Number = 0;
var bottom:Number = stage.stageHeight;
if (ball.x + ball.radius > right) {
boing.play();
ball.x = right - ball.radius;
vx *= bounce;
} else if (ball.x - ball.radius < left) {
boing.play();
ball.x = left + ball.radius;
vx *= bounce;
}
if (ball.y + ball.radius > bottom) {
boing.play();
ball.y = bottom - ball.radius;
vy *= bounce;
} else if (ball.y - ball.radius < top) {
boing.play();
ball.y = top + ball.radius;
vy *= bounce;
}
}
}
}
测试一下影片看一看 … … 听一听拥有声音以后带来的不同感受。当然,要找到正确的声音用在正确的环境下,也不要加得太多,因为这本身也是一门艺术。

浙公网安备 33010602011771号