现在您已经来到了最后一章。我将所有想要介绍的一些小东西都放在了这一章,它们不
太合适放在其它地方,或者说与前面章节的主线有些脱离。
本章,我还重组了前面每章课后列出的公式,因此可以当作这些公式的一个参考点。
由于这些课题都是比较零碎的概念,所以我没有办法将这些许许多多的内容组织起来。
因此每一节都是一个独立的单元。好了,不多说了,让我们开始吧。
布朗(随机)运动
先讲讲历史。一天,一个名叫罗伯特-布朗(Robert Brown)的植物学家正在观察一滴水
中的花粉颗粒,随后他发现这些花粉是在随机运动的。虽然它们不是水流或水的运动,但是
这些小小的颗粒却永远不会停下来。 他发现同样的事情会发生在微尘中,但它们不会像花粉
那样游泳。虽然他不知道为什么会有这种现象, 其实不只是他还有其他所有人在几十年内都
不能给出解释,但是他却将这种现象用自己的名字命名 —— 只是为了能意识到它!
当今,我们对布朗运动的解释是大量的水分子在一滴水中不断运动, 虽然水滴看上去是
静止的。这些水分子与花粉和灰尘发生碰撞,将一些动量传给它们。因为即使是一颗小小的
灰尘都要比一个水分子重上一百万倍, 所以一次碰撞不会带来多大的影响。但是当每秒有几
百万次的碰撞时,那么这些动量就会累计起来。
现在,一些水分子也许撞到了灰尘的一边,而另一些则撞在了另一边。最终,它们会达
到总的平均值。但是,随着时间的变化,受到更多撞击的一边就会产生波动,假设为左边,
那么这个粒子就会向右运动一点。底部所受撞击越多,则粒子向上运动得就越多。最后所有
的值趋于平均,最终的结果通常不会在任何一个方向产生太多的动量。 这就是随机悬浮动作。
我们可以在 Flash 中轻松地模拟出这种效果。在每一帧中,计算一个随机数加在运动
物体的 x 和 y 速度中。随机的数值应该即可以是正数也可以是负数, 并且一般来说都非常
小,比如范围从 -0.1 到 +0.1。形式如下:
vx += Math.random() * 0.2 - 0.1;
vy += Math.random() * 0.2 - 0.1;
用 0.2 乘以一个随机的小数,所得的值从 0.0 到 0.2。再减去 0.1 则值变为 -0.1 到 0.1。
在这里加入一些摩擦力(friction)很重要,否则速度会增大,并产生不自然的加速。在
Brownian1.as 中,我创建了 50 个粒子并让它们以布朗运动的形式悬浮。粒子就是我们熟悉
的 Ball 类的实例,让它们为黑色并缩小。以下是代码:
package {
import flash.display.Sprite;
import flash.events.Event;
public class Brownian1 extends Sprite {
private var numDots:uint = 50;
private var friction:Number = 0.95;
private var dots:Array;
public function Brownian1() {
init();
}
private function init():void {
dots = new Array();
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
dot.x = Math.random() * stage.stageWidth;
dot.y = Math.random() * stage.stageHeight;
dot.vx = 0;
dot.vy = 0;
addChild(dot);
dots.push(dot);
}
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = dots[i];
dot.vx += Math.random() * 0.2 - 0.1;
dot.vy += Math.random() * 0.2 - 0.1;
dot.x += dot.vx;
dot.y += dot.vy;
dot.vx *= friction;
dot.vy *= friction;
if (dot.x > stage.stageWidth) {
dot.x = 0;
} else if (dot.x < 0) {
dot.x = stage.stageWidth;
}
if (dot.y > stage.stageHeight) {
dot.y = 0;
} else if (dot.y < 0) {
dot.y = stage.stageHeight;
}
}
}
}
}
这里大多部分内容都不是什么新知识,我已将有关的部分用加粗表示。
如图 19-1 所示,代码运行时屏幕上的显示。
图 19-1 布朗运动
在 Brownian2.as 中,我将 numDots 减少到 20。然后将这行代码:
graphics.lineStyle(0, 0, .5);
加入到 init 函数中,并在 onEnterFrame 中加入一些绘图的代码。
private function onEnterFrame(event:Event):void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = dots[i];
graphics.moveTo(dot.x, dot.y);
dot.vx += Math.random() * 0.2 - 0.1;
dot.vy += Math.random() * 0.2 - 0.1;
dot.x += dot.vx;
dot.y += dot.vy;
dot.vx *= friction;
dot.vy *= friction;
graphics.lineTo(dot.x, dot.y);
}
}
这样就在每个 dot 移动前与移动后的位置之间绘制了一条线。也就绘制出了自己的运动路
径,如图 19-2 所示。如果您知道“布朗运动”这个词的话,那么一定会常常看到这种图像。
图 19-2 带有行迹的布朗运动
当我们要让物体以无意识、无外力的形式运动时,那么布朗运动将是最为实用的。也可
以将它加入到一个带有其它运动的影片中,这样会给人一种随意(randomness)的感觉。例
如到处乱飞的苍蝇或蜜蜂。 也许这些影片已经有了它们各自的运动路径,但是在加入了一些
随机运动后会让它看起来更加栩栩如生。
随机分布
有时候我们会创建一些物体并将它们随机放置。您已经在本书中看到了很多这样的例
子,但是下面我们要来关注几种不同的方法,获得不同的结果。
方形分布
如果你想让物体随机分布在整个舞台上,那是相当简单的。只需要选择一个舞台宽度的
随机数作为 x,一个高度的随机数作为 y。事实上,我们上一节就是这么做的:
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
dot.x = Math.random() * stage.stageWidth;
dot.y = Math.random() * stage.stageHeight;
...
}
但是如果说我们要让这些点集中分布在舞台中心,比如舞台中心上方 100 像素为顶,下方
100 像素为底。我们可以这样做,可见 Random1.as:
package {
import flash.display.Sprite;
public class Random1 extends Sprite {
private var numDots:uint = 50;
public function Random1() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
dot.x = stage.stageWidth / 2 +
Math.random() * 200 - 100;
dot.y = stage.stageHeight / 2 +
Math.random() * 200 - 100;
addChild(dot);
}
}
}
}
这里创建了一个从 -100 到 +100 的随机数,并将它加到舞台中心点上,因此所有的点在每
个轴上都不能超过 100 像素。如图 19-3 所示。
图 19-3 随机安排的点
不赖嘛。但是如果让它们挤在一起,增加点的数量(300)并减少随机范围为 50,我们
将看到一些奇怪的事情。以下代码来自 Random2.as:
package {
import flash.display.Sprite;
public class Random2 extends Sprite {
private var numDots:uint = 300;
public function Random2() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
dot.x = stage.stageWidth / 2 +
Math.random() * 100 - 50;
dot.y = stage.stageHeight / 2 +
Math.random() * 100 - 50;
addChild(dot);
}
}
}
}
图 19-4 为运行结果。
图 19-4 这种方法形成了一个方形。看起来不再像是随机安排的
如您所见,这些点形成了一个正方形。这样没问题,但是如果我们要制作一些爆炸效果
或星系效果等,正方形看起来就不那么自然了。如果正方形分布不是您真正想要的,那么就
继续下一项技术吧。
圆形分布
虽然圆形分布比方形分布稍稍复杂一点,但是它真的也不难。
首先,我们需要知道圆的半径。为了与上一个例子呼应,这里仍然使用 50。这将是从
舞台中心开始能够放置的最大半径。我们将从 0 到 50 之间取一个随机数作为点的位置。
然后从 0 到 PI * 2 个弧度 (360 度)选择一个随机的角度,再使用三角形函数找出点的 x
和 y 坐标。以下是 Random3.as 的代码:
package {
import flash.display.Sprite;
public class Random3 extends Sprite {
private var numDots:uint = 300;
private var maxRadius:Number = 50;
public function Random3() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
var radius:Number = Math.random() * maxRadius;
var angle:Number = Math.random() * (Math.PI * 2);
dot.x = stage.stageWidth / 2 +
Math.cos(angle) * radius;
dot.y = stage.stageHeight / 2 +
Math.sin(angle) * radius;
addChild(dot);
}
}
}
}
运行结果如图 19-5 所示。
图 19-5 圆形随机分布
这就是我前面提到的那种更加自然的分布形式。大家也许注意到了这些点似乎更集中于
圆形的中心。因为点是沿着半径平均的分布的,这就会使中心的点与边上的点同样多。但是
因为中心的空间小,也就会显得更加拥挤。
这样的效果也许对于某些程序来说很好,但是 O’Shell (www.pixelwit.com) 的 Sean 曾
给我一次挑战,让我给出一种方法将这些点均匀地分布成圆形。不得不承认我被难倒了,我
的解决方法非常复杂。最后他给了我一个非常简单的办法,可见 Random4.as:
package {
import flash.display.Sprite;
public class Random4 extends Sprite {
private var numDots:uint = 300;
private var maxRadius:Number = 50;
public function Random4() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
var radius:Number = Math.sqrt(Math.random()) * maxRadius;
var angle:Number = Math.random() *
(Math.PI * 2);
dot.x = stage.stageWidth / 2 +
Math.cos(angle) * radius;
dot.y = stage.stageHeight / 2 +
Math.sin(angle) * radius;
addChild(dot);
}
}
}
}
通过取随机数的平方根,所得的结果偏向 1 而远离 0,这样做足以使分布变均匀。运行结
果如图 19-6 所示。Sean,好人呀!
图 19-6 更为均衡的分布
偏向分布
最后,我们也许还想让物体自由地分布在整个舞台上,但是要让它们趋于在中心区域分
布。我们已经找到了分布在边缘的方法,而现在要让它们越接近中心越多。这就有点像第一
个圆形分配的例子,只不过这次要运用在矩形区域中。
我们为每个坐标生成一个随机数,然后求它们的平均数得到最终的值。例如,假设舞台
有 500 像素宽。 如果为每个对象随机生成一个 x 坐标,那么每个对象分布在哪里的概率都
是相同的。但是如果从 0 到 500 产生两个随机数,再求平均,那么被置在舞台中心的机会
就会略高一些。
让我们更深入地看一看这个问题。我们有一定的概率让两个数都在较“高”的范围内,
假设从 300 到 500。让两个数都得到较低范围的概率也几乎相同,从 0 到 200。但是相比
而言,一个数较高而另一个数较低,或者一个数居中另一个数较高或较低,或者都处于中等
水平的概率要更高。所有这些可能性的平均值将使大多数点更靠近舞台中心。
OK,让我们来看代码。像平常一样,从一维的开始。下面是代码(可以在 Random5.as
中找到):
package {
import flash.display.Sprite;
public class Random5 extends Sprite {
private var numDots:uint = 300;
public function Random5() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
var x1:Number = Math.random() * stage.stageWidth;
var x2:Number = Math.random() * stage.stageWidth;
dot.x = (x1 + x2) / 2;
dot.y = stage.stageHeight / 2 +
Math.random() * 50 - 25;
addChild(dot);
}
}
}
}
这里我们创建了两个随机数 x1 和 x2,并设置 dot 的 x 坐标为它们的平均值。y 坐标只
是邻近舞台中心点的一个简单的随机数。运行结果如图 19-7 所示。
图 19-7 一次迭代的偏向分布
虽然这个效果不是很明显,但是我们已经可以看到中心位置比边缘位置的点更多一些。
创建更多的随机值, 再取它们的平均值,会使效果更加明显。我们可以将它放入一个 for 循
环中动态执行(Random6.as) :
package {
import flash.display.Sprite;
public class Random6 extends Sprite {
private var numDots:uint = 300;
private var maxRadius:Number = 50;
private var iterations:uint = 6;
public function Random6() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
var xpos:Number = 0;
for (var j:uint = 0; j < iterations; j++) {
xpos += Math.random() * stage.stageWidth;
}
dot.x = xpos / iterations;
dot.y = stage.stageHeight / 2 +
Math.random() * 50 - 25;
addChild(dot);
}
}
}
}
这里 iterations 变量控制要取多少个数的平均值。开始让 xpos 变量等于 0,然后将每个随
机数都加在上面。最后,用这个结果除以 iterations 得到最终的值。运行结果如图 19-8。
图 19-8 六次迭代的偏向分布
现在,要 y 轴也做同样的事就非常简单了,Random7.as:
package {
import flash.display.Sprite;
public class Random7 extends Sprite {
private var numDots:uint = 300;
private var maxRadius:Number = 50;
private var iterations:uint = 6;
public function Random7() {
init();
}
private function init():void {
for (var i:uint = 0; i < numDots; i++) {
var dot:Ball = new Ball(1, 0);
var xpos:Number = 0;
for (var j:uint = 0; j < iterations; j++) {
xpos += Math.random() * stage.stageWidth;
}
dot.x = xpos / iterations;
var ypos:Number = 0;
for (j = 0; j < iterations; j++) {
ypos += Math.random() * stage.stageHeight;
}
dot.y = ypos / iterations;
addChild(dot);
}
}
}
}
这种分布如图 19-9 所示。
图 19-9 二维的偏向分布
在我看来,这是所有分布效果中最无规律,最具爆发性,最像星系的一种分布,虽然
它也是最复杂的。好了,我们现在至少已经掌握了四种生成随机位置的方法。
基于计时器与时间的动画
到目前为止本书的所有例子都是通过把运动代码放到 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 毫秒调用一次。 因为在用户的机器上没法精确地知道代码会运行多快,所以多
数情况下,计时器动画不会比帧动画精确多少。
如果您需要真正的精确,那么基于时间的动画则是我们的必经之路。
(如果要转载请注明出处http://blog.sina.com.cn/jooi,谢谢)