6.1环境边界
到目前为止,我们已经学会了如何在影片中绘制图形,并且通过施加外力使影片运动起来。然而,在这些例子中也许会遇到这样的烦恼:物体移动到屏幕外后就不到了。如果在某个角度上运动得过快,那么就没有办法再让物体退回来,只能选择重新运行影片。
我们常常忽视边界的存在如:墙和屋顶,最平常的就是地面。通常在制作太空模拟时,要用环境边界作为一道屏障,保证物体能够在一个可见的范围内运动。
另一个常被忽略的问题是,所处的环境如何改变物体的运动。惯性一词是用来形容物体在空间中穿梭,并保持以同样的方向及速度运动,只有对其施加外力,才会使它的运动发生改变。改变物体速度向量的力,可能是摩擦力的一种——甚至可以是空气的阻力。目前,我们已经能够模拟真空环境下的物体运动了,但是大家一定还想模拟真实环境下的物体运动。
那么本章就要解决前面这两个问题。首先,学习边界环境下物体的运动,然后学习如何模拟阻力,Let's go。
先来学习边界的设置,就像我们的日常活动一样:开运动会,做某项工作,盖房子等,这里边界是指为这项活动保留的活动空间。意思是“我只关心发生在这个范围内的事情,如果超出了这个范围,就不再关注它了。”
当物体超出了这个范围后,我们可以对它进行一些操作。可以再把它移回来,或把它从关注的对象中移除,另一种选择是跟随它。只要物体是运动的,那么它就有机会离开这个范围。当物体离开后,我们可以选择忘记它,或将它移动回来,或跟随它。我们将介绍前两种方法,不过先要确定边界的位置,再学习如何定义边界。
6.1.1设置边界
通常,边界就是一个矩形。从最简单的例子开始——基于舞台大小的边界,在 AS 3 中,舞台由一个名为 stage 的属性表示,它是每个显示对象的一部分。所以在文档类(继承自 MovieClip 或 Sprite)中,我们可以直接使用这个属性访问舞台及舞台的相关属性。如果在播放器窗口改变大小后希望舞台的尺寸与播放器尺寸相匹配的话,就应该设置这两个属性:
stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE;
请注意,还需要导入 flash.display.StageAlign 和 flash.display.StageScaleMode 这两个类。这样一来,影片的左上边界将为零,而右下边界将为 stage.stageWidth 和stage.stageHeight。可将它们保存为变量,如下:
private var left:Number = 0; private var top:Number = 0; private var right:Number = stage.stageWidth; private var bottom:Number = stage.stageHeight;
如果使用变量保存设置的话,那么舞台大小的改变将不会对这些变量产生影响。如果要使用一个固定的区域作为边界的话,这样做是非常合适的。
然而,如果使用整个舞台区域作边界的话,那么即使舞台大小发生了改变,只要在代码中直接调用 stage.stageWidth 和 stage.stageHeight 就可以了。
比如, 我们可以为对象创建一个居住的“房间”,在这个例子中, 边界可以是 top = 100, bottom = 300, left = 50, right = 400。
OK,边界已经有了,用它们能做什么呢?判断所有移动的对象,看它们是否仍在这个空间内,这里可以使用 if 语句,简化的样式如下:
if(ball.x > stage.stageWidth) { // do something } else if(ball.x < 0) { // do something } if(ball.y > stage.stageHeight) { // do something } else if(ball.y < 0) { // do something }
使用 if 和 else 语句判断边界,如果小球 x 坐标大于右边界,就意味着它超出了右边界。但不可能同时超出左边界,所以不需要再用一条 if 语句进行判断。因此,只需要在第一个 if 语句失败后再判断左边边界即可,顶部和底部也是如此。然而, 物体有可能在 x,y轴上同时超出边界,所以要把这两个判断语句分开。如果物体出界后,应该对它们执行什么样的操作呢?答案有四种:
- 将对象移除;
- 重置到舞台上,像生成一个新对象一样(重置对象);
- 重置到舞台上,将同一个对象放置在不同位置;
- 将其反弹回去。
我们从最简单的移除对象开始。
6.1.2移除物体
如果对象是不断产生的,那么使用一次性删除对象的方法是非常有效的。被删除的对象将会由新的对象所取代,这样舞台就永远不为空。但也不能生成太多的可移动对象,因为这样会使 Flash Player 变慢。
调用 removeChild(对象名),删除影片或显示对象,会将对象实例从舞台上移除。请注意,被移除的显示对象仍然存在,只是看不到而已。如果要将该对象彻底删除,还应该调用delete 对象名 将其完全删除。
如果移动的对象只是一些影片实例,并且物体的运动只由 enterFrame 函数进行处理,那么要停止整个程序的执行只需调用 removeEventListener(Event.ENTER_FRAME, onEnterFrame);就可以了。另一方面,如果运动的对象很多,要通过持续执行代码使每个对象都动起来(除了被删除的对象外),就应该在数组中保存所有对象的引用,然后循环这个数组使里面的每个对象都动起来。随后,当删除了其中的某一个对象后,使用 Array.splice 方法同时将该对象的引用在数组中删除,后面会有代码。
所以,如果想要删除影片,就要知道边界在哪儿,使用 if 语句来完成:
if(ball.x > stage.stageWidth ||ball.x < 0 || ball.y > stage.stageHeight || ball.y < 0) { removeChild(ball); }
|| 符号意思是“或者”。这句话是说“如果物体超出了右边界,或左边界,或上边界,或下边界,就将它删除。”。 这里有一个小问题,也许大家现在还没有意识到,见图 6-1。
图 6-1 小球并没有完成超出舞台,就被移除了
图6-1中,小球的位置由中心的注册点的位置决定,而注册点超出了屏幕的右边界,小球将被删除。如果小球的运动足够快,也许看上去问题不大。但如果运动得非常缓慢,每帧运动一像素的话,那会是怎样?我们会看着它走向屏幕的边界,但还差一半没有走完就被移除了!这就像一个演员只离开了舞台的一半就把戏服脱掉了,破坏了塑造人物的形象。所以,要让小球完全离开场景,要等到它完全离开视野后再采取处理。实现这个计划,需要考虑到物体的宽度。因为注册点在中心,所以,可以将宽度的一半保存为 radius 属性。代码如下:
if(ball.x - ball.radius > stage.stageWidth || ball.x + ball.radius < 0 || ball.y - ball.radius > stage.stageHeight || ball.y + ball.radius < 0) { removeChild(ball); }
如图 6-2 所示。
图 6-2 小球完全超出了舞台,可放心移除
虽然使用球形或圆形是个比较特殊的例子, 但这样的代码对于所有注册点在中心的物体来说,都是适用的。下面一个例子中, 将要使用 Ball 类,这个类前面也用过,但这次要加上一点新的内容。为该类加入公共属性 vx 和 vy ,让每个小球都有自己的速度向量。全部代码如下:
package { import flash.display.Sprite; public class Ball extends Sprite { public var radius:Number; private var color:uint; public var vx:Number = 0; public var vy:Number = 0; public function Ball(radius:Number=40, color:uint=0xff0000) { this.radius = radius; this.color = color; init(); } public function init():void { graphics.beginFill(color); graphics.drawCircle(0, 0, radius); graphics.endFill(); } } }
对于纯面向对象编程来说,也许不会用这些公共属性,而是将它们转为私有属性,再使用 getter/setter 方法进行操作。但是为了方便起见,就不再遵循这个规则了,仅使用公共属性作替代。下面一个文档类,Removal.as,设置了许多小球,并在它们离开舞台后进行删除:
package { import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.events.Event; public class Removal extends Sprite { private var count:int = 20; private var balls:Array; public function Removal() { init(); } private function init():void { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align=StageAlign.TOP_LEFT; balls = new Array(); for(var i:int = 0; i < count; i++) { var ball:Ball = new Ball(10); ball.x = Math.random() * stage.stageWidth; ball.y = Math.random() * stage.stageHeight; ball.vx = Math.random() * 2 - 1; ball.vy = Math.random() * 2 - 1; addChild(ball); balls.push(ball); } addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { for(var i:Number = balls.length - 1; i > 0; i--) { var ball:Ball = Ball(balls[i]); ball.x += ball.vx; ball.y += ball.vy; if(ball.x - ball.radius > stage.stageWidth || ball.x + ball.radius < 0 || ball.y - ball.radius > stage.stageHeight || ball.y + ball.radius < 0) { removeChild(ball); balls.splice(i, 1); if(balls.length <= 0) { removeEventListener(Event.ENTER_FRAME, onEnterFrame); } } } } } }
应该很容易理解吧。首先。创建 20 个小球的实例,随机安排它们在舞台上的位置,给出随机的 x,y 速度向量,并将它们加入显示列表,然后 push 到数组中。
onEnterFrame 方法通过速度向量使小球移动,判断边界,并将出界的小球删除。请注意,不但要将小球从显示列表中删除,同时还要使用 Array.splice 函数将数组中的引用也删除。Array.splice 有两个参数:开始删除元素的索引,删除元素的个数。在这个例子中,只删除一个元素即当前索引处的元素。
大家也许注意到了本例中的 for 语句,与其它例子中的不太一样:
for(var i:Number = balls.length - 1; i > 0; i--)
这是让 for 循环倒着执行,遍历整个数组。因为如果在数组中使用 splice ,数组的索引就会改变。
最后,在删除数组元素后,还要判断数组长度是否小于零。如果小于零,则撤消对enterFrame 的侦听器。
6.1.3重新生成物体
下一个策略是将超出舞台范围的对象进行重置。实际上就是重新配置,重新设置属性。当一个对象离开了舞台后,它就没有作用了,不过,可以将其重置到舞台上,让它作为一个新对象再加入进来。永远不要担心对象的数量过多,因为这个数量是固定不变的。这个技术用于制作喷泉效果非常合适:一串粒子不停地喷射,超出舞台的粒子重新加入到水流中。
现在就来制作一个喷泉效果。作为喷泉的粒子,我们同样使用 Ball 类,但只把它设为2 像素的大小并给它一个随机的颜色。水源在舞台底部的中心位置,所有的粒子都从这里发出,当它们超出舞台边界后,将会被重置回来。所有粒子都以一个随机的负 y 速度和一个随机的 x 速度作为开始。这样就会上喷射,并伴有轻微的左右移动。当粒子重置后,它们的速度向量也将被重置,同时粒子也要受到重力的牵引。文档类 Fountain.as:
package { import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.events.Event; public class Fountain extends Sprite { private var count:int = 100; private var balls:Array; private var gravity:Number = 0.5; public function Fountain() { init(); } private function init():void { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align=StageAlign.TOP_LEFT; balls = new Array(); for(var i:int = 0; i < count; i++) { var ball:Ball = new Ball(2, Math.random() * 0xffffff); ball.x = stage.stageWidth / 2; ball.y = stage.stageHeight; ball.vx = Math.random() * 2 - 1; ball.vy = Math.random() * -10 - 10; addChild(ball); balls.push(ball); } addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function onEnterFrame(event:Event):void { for(var i:Number = 0; i < balls.length; i++) { var ball:Ball = Ball(balls[i]); ball.vy += gravity; ball.x += ball.vx; ball.y += ball.vy; if(ball.x - ball.radius > stage.stageWidth || ball.x + ball.radius < 0 || ball.y - ball.radius > stage.stageHeight || ball.y + ball.radius < 0) { ball.x = stage.stageWidth / 2; ball.y = stage.stageHeight; ball.vx = Math.random() * 2 - 1; ball.vy = Math.random() * -10 - 10; } } } } }
请试着加入风力效果(提示:设置 wind 变量,并加入到 vx )。
6.1.4屏幕折回
下一个处理越界对象的方法,我称其为屏幕折回。概念很简单:一个对象超出了屏幕的左边界,就让它在屏幕右边出现;在右边出界,则将它置到左边;上面出界就回到下面。明白了吧。这个思想与重置对象的概念非常相似,只是位置有所不同。
再回到前面那个老游戏,小行星。第五章的这个飞船影片有些问题:一旦太空船飞出了舞台,就很难将它找回。如果使用屏幕环绕技术,那么影片超出屏幕边界的距离不会大于一像素。
让我们为太空船示例重新加入一些行为,这里是文档类(ShipSim2.as),新的部分用粗体标出:
package { import flash.display.Sprite; import flash.events.Event; import flash.events.KeyboardEvent; import flash.ui.Keyboard; import flash.display.StageAlign; import flash.display.StageScaleMode; public class ShipSim2 extends Sprite { private var ship:Ship; private var vr:Number = 0; private var thrust:Number = 0; private var vx:Number = 0; private var vy:Number = 0; public function ShipSim2() { init(); } private function init():void { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align=StageAlign.TOP_LEFT; ship = new Ship(); addChild(ship); ship.x = stage.stageWidth / 2; ship.y = stage.stageHeight / 2; addEventListener(Event.ENTER_FRAME, onEnterFrame); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDownHandler); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUpHandler); } private function onKeyDownHandler(event:KeyboardEvent):void { switch(event.keyCode) { case Keyboard.LEFT : vr = -5; break; case Keyboard.RIGHT : vr = 5; break; case Keyboard.UP : thrust = 0.2; ship.draw(true); break; default : break; } } private function onKeyUpHandler(event:KeyboardEvent):void { vr = 0; thrust = 0; ship.draw(false); } private function onEnterFrame(event:Event):void { ship.rotation += vr; var angle:Number = ship.rotation * Math.PI / 180; var ax:Number = Math.cos(angle) * thrust; var ay:Number = Math.sin(angle) * thrust; vx += ax; vy += ay; ship.x += vx; ship.y += vy; var left:Number = 0; var right:Number = stage.stageWidth; var top:Number = 0; var bottom:Number = stage.stageHeight; if (ship.x - ship.width / 2 > right) { ship.x = left - ship.width / 2; } else if (ship.x + ship.width / 2 < left) { ship.x = right + ship.width / 2; } if (ship.y - ship.height / 2 > bottom) { ship.y = top - ship.height / 2; } else if (ship.y < top - ship.height / 2) { ship.y = bottom + ship.height / 2; } } } }
这里同样使用了第五章的 Ship 类,请确保该文件与这个类在同一路径下。由于飞船是用白色线条绘制的,所以不要忘记将背景色改为黑色。大家可以看到,在新的类中加入了边界的定义及判断。
6.1.5反弹
本节中的弹性处理也许是最常用也是最复杂的,但也没有屏幕环绕那么复杂,所以不用担心。当检测到物体超出舞台后,开始应用弹性,不改变改变物体的位置,只改变它的速度向量。方法很简单:如果物体超出了左、右边界,只需要使它的 x 速度向量取反。如果超出了上、下边界,只需要让 y 速度向量取反。坐标轴取反非常简单,只需要乘以 -1 。如果速度向量等于 5,则变成-5。如果是-13,则变成 13。代码也非常简单:vx *= -1 或 vy *= -1。
对于反弹的时机来说,我们不并希望等到物体完全超出了舞台后开始反弹。同样,也不希望出现半张图片的效果。比如,往墙上扔一个球,不希望球的一半进入墙体后再反弹回来。因此,首先要判断出小球首次超出边界的瞬间。然后,将小球的运动路径取反,再加上小球宽度/高度的一半。比如:
if(ball.x – ball.radius > right) . . .
就要变为
if(ball.x + ball.radius > right) . . .
两者的区别见图 6-3。
图 6-3 小球略微超出了舞台,需要反弹
只要物体超出了舞台,即使只有一少部分,都要使它的速度向量取反,并且还需要将物体重新定位到边界处,这就形成了一个非常明显的撞击反弹的效果。如果不调整物体的位置,到下一帧,在物体移动之前,也许仍然处在边界外。如果这样的话,物体的速度向量又将取反,则向墙内运动!就会产生物体进出墙体的情形,然后在这附近振荡。x 轴的全部 if 语句如下:
if(ball.x + ball.radius > right) { ball.x = right – ball.radius; vx *= -1; } else if(ball.x – ball.radius < left) { ball.x = left + ball.radius; vx *= -1; }
图 6-4 所示显示了重新置位后小球的位置。
反弹的步骤如下:
- 判断物体是否超出了边界;
- 如果是,将其置到边界处;
- 然后将它的速度向量取反。
图 6-4 小球被重新置位到依靠边界处
叙述部分就到这里,下面来看代码。下一个示例,仍使用 Ball 类,文档类(Bouncing.as):
package { import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.events.Event; public class Bouncing extends Sprite { private var ball:Ball; private var vx:Number; private var vy:Number; public function Bouncing() { init(); } private function init():void { stage.scaleMode = StageScaleMode.NO_SCALE; stage.align=StageAlign.TOP_LEFT; ball = new Ball(); ball.x = stage.stageWidth / 2; ball.y = stage.stageHeight / 2; vx = Math.random() * 10 - 5; vy = Math.random() * 10 - 5; 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) { ball.x = right - ball.radius; vx *= -1; } else if(ball.x - ball.radius < left) { ball.x = left + ball.radius; vx *= -1; } if(ball.y + ball.radius > bottom) { ball.y = bottom - ball.radius; vy *= -1; } else if(ball.y - ball.radius < top) { ball.y = top + ball.radius; vy *= -1; } } } }
多进行几次测试,观察不同角度的运动,并试着将速度向量变大或变小。不得不承认,这是一个数学计算与现实情况不完全一致的例子。从图 6-5中可以看到,小球实际撞击墙面的位置与模拟的位置的差别。
图 6-5 这个技术并不完美,但非常简单,高效且与实际情况非常接近
要达到真正的位置,就要使用更为复杂的计算方法。虽然我们完全可以做到这一点(使用第三章三角学),但是我保证人们不会注意到这些细微的差别。如果在某些模拟中,对位置的要求至关重要的话,那么我们还需要去查阅其它的书籍,并重新考虑使用哪种软件来完成。但是对于大多用 Flash 制作的数游戏或视觉效果而言,用这种方法已经足够了。
再比如,我们手握一个橡胶球,然后松手使它落到地上,当小球落到地面时,会发生向上的反弹,但它永远不会回到我们的手中。这是因为在反弹的过程中小球损失了一部分能量。损失的能量也许是制造声音了,也可能是制造热量了,地面或周围的空气也会吸收一部分能量。重要一点是小球在发生反弹后运动的速度比之前要慢一些。换句话讲,它在反弹到某一轴上时,损失了速度矢量。这样一来,可以简单地重建一个 Flash,前面使用 -1 作为弹性系数。这就意味着,物体反弹的力量为 100%。为了制造能量的损失,可以用弹性系数作为阻力。为了能在代码中使用这个参数,最好将它定义成一个变量。创建一个名为 bounce 的变量,并将它设置为-0.7 这样的数字:
private var bounce:Number = -0.7;
在 if 语句中使用 bounce 这个变量代替 -1。试过后大家会发现与现实中的弹性是多么地相似。为 bounce 变量使用不同的系数,试试效果吧。我们在学习知识的时候,最好与前面所学的原理结合起来。大家可以看一下本书的源文件文档类 Bouncing2.as,在这个示例中还包括了重力,我相信大家可以通过已掌握的知识自行将它加上去。
发布时间:2011/3/30 下午1:41:16 阅读次数:7951