您的位置:首页 > 其它

坐标旋转及角度反弹

2015-04-21 17:00 239 查看
本章介绍了一项特殊技术,著名的坐标旋转。如同其名,它是物体指绕着某点旋转其坐

标,在制作一些非常有趣的效果时,坐标旋转是必不可少的。其中就包括在 Flash 界讨论

了很多年的问题:“如何在斜面上进行反弹?”,本章我会给大家一一解答。

另一个用坐标旋转完成的程序是两物体之间的交互反弹效果。 我们会在下一章讨论动量

守衡时进行讲解。而本章的坐标旋转,我们之前也已经接触过了。如果大家想跳过这章的话,

我劝您还是先坐下来,浏览一遍为好。

简单的坐标旋转

虽然我们在第三章讲三角学的时候介绍过计算的坐标旋转的方法,但还是先来做一下回

顾。假设知道一个中心点,一个物体,一个半径和一个角度。通过不断地增加或减少角度,

并运用基本的三角学知识让物体绕着中心点旋转。我们可将变量设为 vr (旋转速度)来控

制角度的增加或减少。还有,不要忘记角度应用弧度制来表示。代码的结构如下所示:

vr = 0.1;

angle = 0;

radius = 100;

centerX = 250;

centerY = 200;

// 在 enterFrame 处理函数中:

sprite.x = centerX + cos(angle) * radius;

sprite.y = centerY + sin(angle) * radius;

angle += vr;

根据角度与半径使用简单的三角函数设置物体的 x,y 属性,并在每帧中改变角度。我

们用 Flash 动画演示一下。下面是第一个例子,文档类 Rotate1.as:

package {

import flash.display.Sprite;

import flash.events.Event;

public class Rotate1 extends Sprite {

private var ball:Ball;

private var angle:Number = 0;

private var radius:Number = 150;

private var vr:Number = .05;

public function Rotate1() {

init();

}

private function init():void {

ball = new Ball();

addChild(ball);

addEventListener(Event.ENTER_FRAME, onEnterFrame);

}

private function onEnterFrame(event:Event):void {

ball.x = stage.stageWidth / 2 + Math.cos(angle) * radius;

ball.y = stage.stageHeight / 2 + Math.sin(angle) * radius;

angle += vr;

}

}

}

这段代码中没有什么新的知识点。大家可以改变一下角度与半径,试验运行结果。但是

如果我们只知道物体与中心点的位置又该怎么办呢?用 x,y 坐标计算出当前的角度(angle)

与半径(radius)也并非难事。代码如下:

var dx:Number = ball.x - centerX;

var dy:Number = ball.y - centerY;

var angle:Number = Math.atan2(dy, dx);

var radius:Number = Math.sqrt(dx * dx + dy * dy);

这种基于坐标的旋转只对单个物体的旋转效果比较好,尤其是一次性就可确定角度和半

径的情况下。但是在动态的程序中,有时需要旋转多个物体,而它们与中心点的相对位置可

能会发生改变。因此,对于每个物体来说,都需要计算距离,角度和半径,还要用 vr 来增

加角度,最后才能算出新的 x,y 坐标,每帧都如此。这就显得太麻烦了,并且效率也不会

很高。没关系,我们还有更好的办法。





图 10-1 坐标旋转

角度(angle)就是某一时刻内旋转物体位置的大小。这并非当前的角度,也不是旋转后

的角度,而是这两者之间的差值。换句话讲,如果物体在离中心点 45 度的位置,而这次的

角度是 5 度,我们就要在当前角度的基础上再旋转 5 度,到达 50 度这个位置上。这里我

们并不关心最初或最终的角度,只关心旋转了多少度。通常来讲,这个角度都是用弧度制表

示的。OK,让我们来看例子吧。

单物体旋转

本例中将一个小球放在随机的位置上,并使用前面介绍的方法对它进行旋转(文档类

Rotate2.as):

package {

import flash.display.Sprite;

import flash.events.Event;

public class Rotate2 extends Sprite {

private var ball:Ball;

private var vr:Number = .05;

private var cos:Number = Math.cos(vr);

private var sin:Number = Math.sin(vr);

public function Rotate2() {

init();

}

private function init():void {

ball = new Ball();

addChild(ball);

ball.x = Math.random() * stage.stageWidth;

ball.y = Math.random() * stage.stageHeight;

addEventListener(Event.ENTER_FRAME, onEnterFrame);

}

private function onEnterFrame(event:Event):void {

var x1:Number = ball.x - stage.stageWidth / 2;

var y1:Number = ball.y - stage.stageHeight / 2;

var x2:Number = cos * x1 - sin * y1;

var y2:Number = cos * y1 + sin * x1;

ball.x = stage.stageWidth / 2 + x2;

ball.y = stage.stageHeight / 2 + y2;

}

}

}

在使用 vr 之前将它设成了 0.05,再计算这个角度的正弦和余弦值。根据小球与舞台

中心点的位置计算出 x1 和 y1。然后使用前面讲到的坐标旋转公式,计算出小球的新位置

x2 和 y2。由于这个位置是小球与中心点的相对位置,所以我们还需要把 x2 和 y2 与中心

点相加求出最终小球的位置。

实验一下,我们发现这个例子与早先那个版本执行的结果是一样的。也许大家会问,既

然功能完全一样,为什么还要使用这个看起来很复杂的公式呢?如果处理的内容非常简单,

也许您的说法是正确的。下面让我们来看看这个公式在简化问题时的应用。首先,考虑多物

体旋转的情况。

多物体旋转

假设要旋转多个物体,所有的影片都保存在命为 sprites 的数组中。那么 for 循环应

该是这样的:

for (var i:uint = 0; i < numSprites; i++) {

var sprite:Sprite = sprites[i];

var dx:Number = sprite.x - centerX;

var dy:Number = sprite.y - centerY;

var angle:Number = Math.atan2(dy, dx);

var dist:Number = Math.sqrt(dx * dx + dy * dy);

angle += vr;

sprite.x = centerX + Math.cos(angle) * dist;

sprite.y = centerY + Math.sin(angle) * dist;

}

然而如果使用高级坐标旋转的方法应该是这样的:

var cos:Number = Math.cos(vr);

var sin:Number = Math.sin(vr);

for (var i:uint = 0; i < numSprites; i++) {

var sprite:Sprite = sprites[i];

var x1:Number = sprite.x - centerX;

var y1:Number = sprite.y - centerY;

var x2:Number = cos * x1 - sin * y1;

var y2:Number = cos * y1 + sin * x1;

sprite.x = centerX + x2;

sprite.y = centerY + y2;

}

请注意第一个版本中我们在 for 循环里调用了四次 Math 函数,这意味着每个物体的

旋转都要执行四次函数调用。第二个版本中只执行了两次函数调用,而且都是在 for 循环

以外面执行的, 意味着它们只执行了一次,与物体的数量无关。举个例子,如果我们有 30 个

影片,如果使用第一个版本的代码每帧需要调用 120 次 Math 函数。大家可以想想哪个版

本的效率最高。

在前上一个例子程序中,删去 enterFrame 中的 sin 和 cos 函数。 这是因为这段程序

中的角度是固定的,因此可以直接给出结果。但是在很多情况下,旋转的角度不是固定不变

的,这就需要每次进行重新计算正余弦的值。

解释一下最后这个概念。举个例子,用鼠标位置控制多个物体旋转的速度。如果鼠标在

屏幕中心,则不产生旋转。如果鼠标向左移动,则物体逆时针旋转,并且越向左速度越快。

如果向右移动,则顺时针旋转。除了创建多个 Ball 实例,并以数组存储以外,这个例子与

前面那个非常相似。下面是文档类(Rotation3.as):

package {

import flash.display.Sprite;

import flash.events.Event;

public class Rotate3 extends Sprite {

private var balls:Array;

private var numBalls:uint = 10;

private var vr:Number = .05;

public function Rotate3() {

init();

}

private function init():void {

balls = new Array();

for (var i:uint = 0; i < numBalls; i++) {

var ball:Ball = new Ball();

balls.push(ball);

addChild(ball);

ball.x = Math.random() * stage.stageWidth;

ball.y = Math.random() * stage.stageHeight;

}

addEventListener(Event.ENTER_FRAME, onEnterFrame);

}

private function onEnterFrame(event:Event):void {

var angle:Number = (mouseX - stage.stageWidth / 2) * .001;

var cos:Number = Math.cos(angle);

var sin:Number = Math.sin(angle);

for (var i:uint = 0; i < numBalls; i++) {

var ball:Ball = balls[i];

var x1:Number = ball.x - stage.stageWidth / 2;

var y1:Number = ball.y - stage.stageHeight / 2;

var x2:Number = cos * x1 - sin * y1;

var y2:Number = cos * y1 + sin * x1;

ball.x = stage.stageWidth / 2 + x2;

ball.y = stage.stageHeight / 2 + y2;

}

}

}

}

我们可以看到,代码并不是那么复杂。如果大家已经掌握了这个方法,可以试用角度与

弧度的方法各写一遍代码,看看执行的效果是好还是坏。

在第十五章讨论 3D 时,回来再看这个公式。事实上,我们将会在同一个方法中使用两

次这个公式让物体绕着两个轴与三个维旋转。不要被我说的这些吓到了,因为在学到这里以

前我们还要学习很多的内容。

角度反弹

我还记得在我刚刚开始痴迷于 Flash,数学和物体时,我就解决了物体撞墙地面、天花

板后反弹的效果。如果这些障碍物只是水平和垂直的,我就知道该怎么去做。但是渐渐地仅

知道这些已经不能满足需求了。在现实情况下,障碍物不仅仅只是水平或垂直的,而是带有

一定角度的。这时就没法在 Flash 中模拟反弹效果了。于是,我就去各个 Flash 论坛问了

一圈,发现我不是第一个问这个问题的人。在论坛里分别有三个帖子,讨论的题目都是“角

度反弹”。

一些精通数学的版主试着回答这些问题。比如说反射角等于入射角。 记得有个非常简单

的公式告诉我们运动物体在碰撞到有角度的平面时,角度的变化。看起来不错,但也只是解

决了一部分问题。我们回忆一下物体碰撞在障碍物后反弹的问题,总结出如下几步:

1.确实何时越过边界。

2.直接在边界上重置物体的位置。

3.改变碰撞轴上的速度。

知道最终的角度只解决了步骤 3 中一半的问题。但是并不能知道何时物体与斜面发生

碰撞,也不知道物体碰撞前停止在斜面上的位置。似乎没人能回答这些问题。我试着用学过

的所有知识来回答,画的图稿可以放满整个仓库,写的程序装满整个硬盘,可最后还是失败

了。如果物体与平面碰撞那就太简单了,如果与斜面碰撞就太难了。但是为什么要在本章讨

论这个内容呢。

那时,www.illogicz.com 的 Stuart Schoneveld 已经制作出超强的在线物理引擎,并

且可以实现这种平滑干净的碰撞。后来,我求他给我一些参考信息,他没有给我任何代码,

只用了一两句话说明了整体思想,让我茅塞顿开。

他是这样说的“斜面反弹吗?先把斜面旋转成平面,然后执行反弹,最后再把它旋转回

去。”

哇!这正是我想要的。我们只需要旋转坐标系,让斜面像平面一样。这就意味着旋转斜

面,旋转物体坐标,再旋转物体速度向量。

现在考虑一下旋转速度向量的问题, 我们把速度向量保存到 vx 和 vy 中,变量中简单

地定义一个向量(角度与速度) 如果知道角度,

。 可以直接进行旋转。但是如果只知道 vx


vy,就可以使用高级坐标旋转公式实现。

用图解释一下也许效果比文字要好些。 如图 10-2 所示,我们看到斜面与小球发生了碰

撞,而这个带箭头的向量表示小球运动的方向与速度。





图 10-2 小球与斜面发生碰撞

在图 10-3 中,我们看到整个斜面被旋转成为一个水平面。注意速度向量也随之改变。





图 10-3 旋转后的情景

现在就很容易实现反弹了吧。调整小球位置,并改变 y 轴速度,如图 10-4 所示。





图 10-4 反弹之后

现在小球有了新的位置和速度。接下来,将所有的一切再旋转成为最初时的样子,如图 10-5

所示。





图 10-5 旋转之后

瞧!我们在斜面上检测出了碰撞的发生,调整了位置,改变了速度向量。希望这些图会

对大家有所帮助,下面来看程序。

实现旋转

首先需要一个类似斜面的东西,只为了能够看到,并无实际用途。对于平面反弹,我们

可以使用舞台的边界。对于斜面反弹,我们就需要一条带有角度的线(line)来表示,以便看

到小球在斜面上的反弹。

因此,创建一个 Sprite 影片,加入显示列表,然后使用绘图 API 绘制一条水平线,

再将影片进行一定角度的旋转。

我们同样需要 Ball 类,现在应该保证它就在手边。在为物体定位时,要确保小球在线

上,以便小球可以落在线上。下面是文档类(AngleBounce.as):

package {

import flash.display.Sprite;

import flash.events.Event;

public class AngleBounce extends Sprite {

private var ball:Ball;

private var line:Sprite;

private var gravity:Number = 0.3;

private var bounce:Number = -0.6;

public function AngleBounce() {

init();

}

private function init():void {

ball = new Ball();

addChild(ball);

ball.x = 100;

ball.y = 100;

line = new Sprite();

line.graphics.lineStyle(1);

line.graphics.lineTo(300, 0);

addChild(line);

line.x = 50;

line.y = 200;

line.rotation = 30;

addEventListener(Event.ENTER_FRAME, onEnterFrame);

}

private function onEnterFrame(event:Event):void {

// 普通的运动代码

ball.vy += gravity;

ball.x += ball.vx;

ball.y += ball.vy;

// 获得角度及正余弦值

var angle:Number = line.rotation * Math.PI / 180;

var cos:Number = Math.cos(angle);

var sin:Number = Math.sin(angle);

// 获得 ball 与 line 的相对位置

var x1:Number = ball.x - line.x;

var y1:Number = ball.y - line.y;

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

var y2:Number = cos * y1 - sin * x1;

// 旋转速度向量

var vx1:Number = cos * ball.vx + sin * ball.vy;

var vy1:Number = cos * ball.vy - sin * ball.vx;

// 实现反弹

if (y2 > -ball.height / 2) {

y2 = -ball.height / 2;

vy1 *= bounce;

}

// 将一切旋转回去

x1 = cos * x2 - sin * y2;

y1 = cos * y2 + sin * x2;

ball.vx = cos * vx1 - sin * vy1;

ball.vy = cos * vy1 + sin * vx1;

ball.x = line.x + x1;

ball.y = line.y + y1;

}

}

}

开始,声明变量 ball,line,gravity,bounce。在 enterFrame 函数中执行基本运动代

码。

然后,获得 line 的角度并转化为弧度制。有了角度,就可以求出正余弦的值。

接下来, ball 的位置减去 line 的位置,

用 获得 ball
的 x,y 与 line 的相对位置。

下面,准备对物体进行旋转!在看到这两代码时,注意到好像是错的。

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

var y2:Number = cos * y1 - sin * x1;

这里的加号与减号与最初的坐标旋转公式是相反的,最初我们的公式是:

x1 = cos(angle) * x - sin(angle) * y;

y1 = cos(angle) * y + sin(angle) * x;

这并没有错。考虑一下,假如 line 旋转了 17 度,如果使用最初的公式,求出旋转度

数会比 17 度大,是 34 度!我们实际想要旋转 -17 度,这样才会使度数变为 0。那么就

应该这样计算正余弦值 Math.sin(-angle) 和 Math.cos(-angle)。但是,最后还需要用到

最初的角度来使所有物体都旋转回去。

因此,可以使用一个角度旋转的变形公式进行反角度的旋转。我们看到,只需要调换加

减符号即可,就这么简单。如果 line 旋转了 17 度,那么所有的物体都要旋转 -17 度,

使得 line 为 0 度,或说是平面,速度向量也是如此。

请注意,我们实际不需要旋转影片中的 line。只是为了让人们看到小球是在哪里被反

弹的。

接下来执行反弹,我们使用 x2,y2 的位置和 vx1,vy1 向量的值。注意因为 y2 是与 line

影片相对的,“底部”边界就是 line 自己。考虑一下小球的位置,判断什么时候 y2 大于

0 – ball.height / 2。简短的写法就是这样:

if(y2 > -ball.height / 2)

边界的设置非常明显。

最后使用最初的公式将所有的物体旋转回去。这些新的数值都是对 x1, y1, ball.vx,

ball.vy 更新过的值。我们所需要的就是重新设置小球的实际位置,通过 x1 和 y1 与

line.x 和 line.y 相加得出。

花些时间来实验一下这个例子。试改变 line 的 rotation 属性以及改变 line 与

ball 不同的位置,观查运行结果。确保它们都能正常工作。

优化代码

前面我们已经看过一些代码优化的例子。通常是使用一次执行代替多次执行,或干脆不

执行。

我们前面写的那段代码只是为了看得比较清楚。其中有一些代码实际上并不需要执行。

多数代码只有在 ball 与 line 产生接触时才执行。因此,多数时间只需要执行基本的运动

代码。换句话讲,我们要将代码放到 if 语句中去:

if(y2 > -ball.height / 2)

所以我们只需知道变量 y2。为了得到它需要 x1 和 y1 以及 sin 和 cos。但是如果

ball 没有碰到 line,就不需要知道 x2 或 vx1 和 vy1。因此,这些都可以只在 if 语句

中出现。同样,如果没有产生碰撞,就不需要对任何物体进行旋转或设置 ball 的位置。因

此,所有 if 语句后面的内容都可以放在 if 语句里面执行。于是就得出了优化版的

onEnterFrame 方法(见 AngleBounceOpt.as):

private function onEnterFrame(event:Event):void {

// 普通的运动代码

ball.vy += gravity;

ball.x += ball.vx;

ball.y += ball.vy;

// 获得角度及正余弦值

var angle:Number = line.rotation * Math.PI / 180;

var cos:Number = Math.cos(angle);

var sin:Number = Math.sin(angle);

// 获得 ball 与 line 的相对位置

var x1:Number = ball.x - line.x;

var y1:Number = ball.y - line.y;

// 旋转坐标

var y2:Number = cos * y1 - sin * x1;

// 实现反弹

if(y2 > -ball.height / 2) {

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

// 旋转速度向量

var vx1:Number = cos * ball.vx + sin * ball.vy;

var vy1:Number = cos * ball.vy - sin * ball.vx;

y2 = -ball.height / 2;

vy1 *= bounce;

// 将一切旋转回去

x1 = cos * x2 - sin * y2;

y1 = cos * y2 + sin * x2;

ball.vx = cos * vx1 - sin * vy1;

ball.vy = cos * vy1 + sin * vx1;

ball.x = line.x + x1;

ball.y = line.y + y1;

}

}

所有粗体的内容都是从 if 语句外面移到里面去的,所以只有产生碰撞时它们才会执

行,这样做比每一帧都执行要好很多。可以想象我们节省了多少 CPU 资源吗?这样的考虑

是非常重要的,尤其是当影片变得越来越多,代码变得越来越复杂时。

动态效果

现在我们可以将这个程序变得更加动态些,实时地改变 line 的角度。只需要一行代码

即可搞定,在 onEnterFrame 方法的第一行写入:

line.rotation = (stage.stageWidth/ 2 - mouseX) * .1;

现在我们只要前后移动鼠标,line 就会随之倾斜,小球也会立即进行调整。完整的代

码可见文档类 AngleBounceRotate.as。

修正“跌落”问题

大家也许注意到这样一个问题,即使 ball 离开了 line,它依然会沿着那条边运动。

看起来有些奇怪,但是要记住 ball 并不是真的与 line 影片产生交互。虽然执行的结果是

非常准确的,我们可以认为实际上没有碰撞,一切都是计算出来的结果。因此 ball 根本就

不知道 line 影片,也不知道影片的起点与终点。但是我们可以告诉它 line 在哪里——使

用简单的碰撞检测或更精确的边界检测方法。下面让我们看看这两种方法,由您决定使用哪

一种。

碰撞测试

找出 line 位置最好的方法是将除了基本移动代码以外所有的内容都放到碰撞检测的

if 语句中,如下:

private function onEnterFrame(event:Event):void {

// 普通的运动代码

ball.vy += gravity;

ball.x += ball.vx;

ball.y += ball.vy;

if(ball.hitTestObject(line) {

// 剩下的所有内容都在这个函数中

}

}

虽然方法很简单,但是已经可以满足大部分的实际需要,下面还有一种更加精确一些的

方法,也是我比较喜欢的一种方法。当然,这就需要更加复杂一些的计算。

判断边界

个人认为 getBounds 方法是 ActionScript 中最不被充分利用的方法。我曾考虑在第

九章中配合碰撞检测提一下,但那章的内容已经很多了, 而且我发现最适合使用的地方应该

是下面这个地方。所以把它放在这一章为大家讲解。

回顾一下碰撞检测一章,一定还记得矩形边界吧。为了让大家回忆起来,我们一起来回

顾一下。矩形边界是指用于包围舞台上一个显示对象可见图形元素的矩形边框。

hitTestObejct 和 hitTestPoint 函数都应用了这种边界。

getBounds 函数直接给出了矩形边界的位置和大小的值。下面是这个函数的基本用法:

bounds = displayObject.getBounds(targetCoordinateSpace)

可以看到,这个方法作为任何显示对象的方法来调用,并返回 flash.geom.Rectangle

的实例,描述了矩形的大小与位置。

首先,来看一下这个唯一的参数,targetCoordinateSpace。是什么意思?

我们使用 targetCoordinateSpace 参数来指定用哪种视角来描述矩形边界。大多数情

况下,这个参数是该物体的父级显示对象。比如,如果主文档类就是一个 Sprite 影片,我

们叫它 sprite,那么 getBounds(this),就表示“根据主影片的坐标,给出这个 sprite 的

矩形边界”。另外,如果在一个 sprite 里面又创建或加载了其它 sprite,就需要通过外

层影片的位置得到矩形边界。写法如下:

childSpite.getBounds(parentSprite);

这个意思是说,我们要得到 childSprite 影片的矩形边界,而这个影片位于

parentSprite 的里面,并且我们想要用 parentSprite 坐标空间的视角来描述它。显然,

targetCoordinateSpace 应该是个显示对象,或是继承自 DisplayObject 类的实例。文档

类,Sprite, MovieClip 都是显示对象,没问题。

下面看看 getBounds 函数的返回值。前面说过,返回值是一个 Rectangle 的实例,里

面包涵了矩形边界的数据。以前在使用 Rectangle 类时看到过,它里面有四个属性:x, y,

width, heigth。并且我们可以使用这些信息。它还包括其它一些非常有用的属性:

left,right,top,bottom。大家应该可以猜出它们的意思吧。

让我们来试一试。将一个 Ball 类的实例加入显示列表,然后插入如下代码(不要忘记

导入 flash.geom.Rectangle 类):

var bounds:Rectangle = ball.getBounds(this);

trace(bounds.left);

trace(bounds.right);

trace(bounds.top);

trace(bounds.bottom);

做一个有趣的实验,将第一行代码改为:

var bounds:Rectangle = ball.getBounds(ball);

现在,小球的边界是从它自身的视角来看的;换句话讲,这个位置与它自身注册点的位

置是相关的。因为小球是以 0,0 点作为中心绘制的,left 和 top 的值应该是负数,实际

上就等于 –right 和 –bottom。这里值得一提的是,如果我们调用 getBounds 时没有给

出参数,那么结果是相同的,物体本身也就是它的目标坐标空间。

现在也许大家都忘了为什么要讨论边界了吧, 回忆一下。我们想要知道物体何时从 line

的上面跌落下来,还记得吗?因此,可以在 line 上面调用 getBounds, 然后得到它的 left

和 right。如果 ball 的 x 小于 bounds.left,或大于 bounds.right, 就说明它从 line 上

面掉落了。再怎么说也不如看代码的好,以下是程序:

private function onEnterFrame(event:Event):void {

line.rotation = (stage.stageWidth/ 2 - mouseX) * .1;

// 普通的运动代码

ball.vy += gravity;

ball.x += ball.vx;

ball.y += ball.vy;

var bounds:Rectangle = line.getBounds(this);

if(ball.x > bounds.left && ball.x < bounds.right) {

// 剩下的所有内容都在这个函数中

}

}

大家可以在 AngleBounceBounds.as 中找到完整的程序。

修正“线下”问题

碰撞检测与边界判断这两种方法都要先确定 ball 与 line 相互接触,然后进行坐标旋

转获得调整后的位置与速度。如果 ball 的 y 坐标旋转后的位置 y2 超过了 line, 则执行

反弹。但是如果 ball 自下而上穿过了 line,又该怎么办?假设 line 在舞台的中心位置,

然后 ball 从下面反弹上来。如果碰撞检测与边界判断的结果都是 true,Flash 就认为

ball 刚刚从 line 上弹出去,它会把 ball 从 line 下面移到上面去。那么我的解决办法

是比较 vy1 和 y2,并且只在 vy1 大于 y2 时才进行反弹。如图 10-6 所示。





图 10-6 线上通过还是线下通过?

左面 ball 的 y 速度大于 ball 与 line 的相对距离。这就意味着只要它进行运动就

会跑到线上面去。而右面 ball 的速度小于 ball 与 line 的相对距离。换句话讲,在这一

帧内,它位于线下,并且下一帧还是在线下。只有在小球从线上往下掉落时从进行反弹。下

面看一下这段代码的修改。以下是 enterFrame 代码段:

// 旋转坐标

var y2:Number = cos * y1 - sin * x1;

// 实现反弹

if(y2 > -ball.height / 2) {

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

// 旋转速度向量

var vx1:Number = cos * ball.vx + sin * ball.vy;

var vy1:Number = cos * ball.vy - sin * ball.vx;

...

我们只需要把 y2 < vy1 加入到 if 语句中:

if(y2 > -ball.height / 2 && y2 < vy1)

如果这样的话,就需要先计算出 vy1。代码如下:

// 旋转坐标

var y2:Number = cos * y1 - sin * x1;

// 旋转速度向量

var vy1:Number = cos * ball.vy - sin * ball.vx;

// 实现反弹

if(y2 > -ball.height / 2 && y2 < vy1) {

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

// 旋转速度向量

var vx1:Number = cos * ball.vx + sin * ball.vy;

...

这样一来每帧都要多做一些计算,这也是为了获得较高精确度和真实度付出的代价。大

家可以决定是否需要它。如果说小球不可能到达线下面的话,就不必考虑这个问题。

在文档类 AngleBounceFinal.as 中,我加入了墙壁与地面的反弹,以便可以让大家看

到小球从线上掉落后的情况。试将我们刚才讨论的代码删除,观察有何不同。

OK,下面我们进入本章最后一个大型的例子程序。

多角度反弹

目前为止,我们都是在讨论一条线或一个斜面的问题。 要处理多个斜面也不是很复杂的

事情,只需要创建多个斜面并进行循环。我们可以把角度反弹的代码抽象到一个函数中,每

次进行调用。

本章前面的这些例子中,我都让代码尽可能地简单,只给大家必要的代码进行演示。然

而,接下来这段代码是一个完正的程序,用到了前面几章学过的一些技术(为的是唤醒大家

的记忆)。

本例与前面那几个例子很像,都使用了同样的 ball 和 line 影片, 只不过这次我把线

缩短了一些,以便可以多放几条进去。在舞台上放置了五条线和一个小球。线条由数组 lines

保存,并放置于舞台上。如图 10-7 所示。





图 10-7 多线条

以下是代码(可在 MultiAngleBounce.as 中找到):

package {

import flash.display.Sprite;

import flash.events.Event;

import flash.display.StageScaleMode;

import flash.display.StageAlign;

import flash.geom.Rectangle;

public class MultiAngleBounce extends Sprite {

private var ball:Ball;

private var lines:Array;

private var numLines:uint = 5;

private var gravity:Number = 0.3;

private var bounce:Number = -0.6;

public function MultiAngleBounce() {

init();

}

private function init():void {

stage.scaleMode = StageScaleMode.NO_SCALE;

stage.align = StageAlign.TOP_LEFT;

ball = new Ball(20);

addChild(ball);

ball.x = 100;

ball.y = 50;

// 创建 5 个 line 影片

lines = new Array();

for (var i:uint = 0; i < numLines; i++) {

var line:Sprite = new Sprite();

line.graphics.lineStyle(1);

line.graphics.moveTo(-50, 0);

line.graphics.lineTo(50, 0);

addChild(line);

lines.push(line);

}

// 放置并旋转

lines[0].x = 100;

lines[0].y = 100;

lines[0].rotation = 30;

lines[1].x = 100;

lines[1].y = 230;

lines[1].rotation = 45;

lines[2].x = 250;

lines[2].y = 180;

lines[2].rotation = -30;

lines[3].x = 150;

lines[3].y = 330;

lines[3].rotation = 10;

lines[4].x = 230;

lines[4].y = 250;

lines[4].rotation = -30;

addEventListener(Event.ENTER_FRAME, onEnterFrame);

}

private function onEnterFrame(event:Event):void {

// normal motion code

ball.vy += gravity;

ball.x += ball.vx;

ball.y += ball.vy;

// 舞台四周的反弹

if (ball.x + ball.radius > stage.stageWidth) {

ball.x = stage.stageWidth - ball.radius;

ball.vx *= bounce;

} else if (ball.x - ball.radius < 0) {

ball.x = ball.radius;

ball.vx *= bounce;

}

if (ball.y + ball.radius > stage.stageHeight) {

ball.y = stage.stageHeight - ball.radius;

ball.vy *= bounce;

} else if (ball.y - ball.radius < 0) {

ball.y = ball.radius;

ball.vy *= bounce;

}

// 检查每条线

for (var i:uint = 0; i < numLines; i++) {

checkLine(lines[i]);

}

}

private function checkLine(line:Sprite):void {

// 获得 line 的边界

var bounds:Rectangle = line.getBounds(this);

if (ball.x > bounds.left && ball.x < bounds.right) {

// 获取角度与正余弦值

var angle:Number = line.rotation * Math.PI / 180;

var cos:Number = Math.cos(angle);

var sin:Number = Math.sin(angle);

// 获取 ball 与 line 的相对位置

var x1:Number = ball.x - line.x;

var y1:Number = ball.y - line.y;

// 旋转坐标

var y2:Number = cos * y1 - sin * x1;

// 旋转速度向量

var vy1:Number = cos * ball.vy - sin * ball.vx;

// 实现反弹

if (y2 > -ball.height / 2 && y2 < vy1) {

// 旋转坐标

var x2:Number = cos * x1 + sin * y1;

// 旋转速度向量

var vx1:Number = cos * ball.vx + sin * ball.vy;

y2 = -ball.height / 2;

vy1 *= bounce;

// 将一切旋转回去

x1 = cos * x2 - sin * y2;

y1 = cos * y2 + sin * x2;

ball.vx = cos * vx1 - sin * vy1;

ball.vy = cos * vy1 + sin * vx1;

ball.x = line.x + x1;

ball.y = line.y + y1;

}

}

}

}

}

代码很多,不过很容易解释,其中的每一部分大家都应该认识。复杂的程序并不代表复

杂的代码,而常常是由许多熟悉的代码段构成的。本例中,checkLine 方法与前面版本中的

onEnterFrame 是一样的。只不过是被 for 循环调用了五次而已。

大家感兴趣的话可以做一个小小的优化。 比方说每次都要循环判断多个斜面,但在很多

系统中, 只要发现小球与某个斜面发生了碰撞并产生交互的话,我们就不需要再继续判断其

它的斜面了,这时我们就可以退出这个循环。为了实现这个功能,需要让 checkLine 函数

返回 true 或 false, 来告诉我们是否发生了碰撞。然后在 onEnterFrame 函数中执行这样

的循环:

for(var i:uint = 0; i < numLines; i++) {

if(checkLine(lines[i])) {

break;

}

}

在一些情况下,尤其是线条非常密集的时候,需要每帧都判断所有的线条。如果是这样,

那么是否决定使用优化,决于个人的需要。

本章重要公式

下面回忆一下本章的两个主要公式。

坐标旋转:

x1 = Math.cos(angle) * x - Math.sin(angle) * y;

y1 = Math.cos(angle) * y + Math.sin(angle) * x;

反坐标旋转:

x1 = Math.cos(angle) * x + Math.sin(angle) * y;

y1 = Math.cos(angle) * y - Math.sin(angle) * x;

来自:http://blog.sina.com.cn/s/blog_4909f3cb0100kmr4.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: