编写代码
首先要让两个小球以一定的角度运动并且最后相互碰撞。开始的设置与前面相同,两个
小球实例:ball0 和 ball1。这次让它们变大一点,如图 11-9 所示,这样它们碰撞的机会
就会大一些。
图 11-9 二维动量守恒,设置舞台
package {
import flash.display.Sprite;
import flash.events.Event;
public class Billiard3 extends Sprite {
private var ball0:Ball;
private var ball1:Ball;
private var bounce:Number = -1.0;
public function Billiard3() {
init();
}
private function init():void {
ball0 = new Ball(150);
ball0.mass = 2;
ball0.x = stage.stageWidth - 200;
ball0.y = stage.stageHeight - 200;
ball0.vx = Math.random() * 10 - 5;
ball0.vy = Math.random() * 10 - 5;
addChild(ball0);
ball1 = new Ball(90);
ball1.mass = 1;
ball1.x = 100;
ball1.y = 100;
ball1.vx = Math.random() * 10 - 5;
ball1.vy = Math.random() * 10 - 5;
addChild(ball1);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
ball0.x += ball0.vx;
ball0.y += ball0.vy;
ball1.x += ball1.vx;
ball1.y += ball1.vy;
checkWalls(ball0);
checkWalls(ball1);
}
private function checkWalls(ball:Ball):void {
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;
}
}
}
}
这些内容想必大家睡梦中都可以写出来。设置边界,随机的速度,加入质量,根据速度
移动小球,判断边界。注意,我把边界的判断单独放在了 checkWalls 函数中,以便重复使
用。
同样,将碰撞判断放到名为 checkCollision 的函数中。onEnterFrame 变为:
private function onEnterFrame(event:Event):void {
ball0.x += ball0.vx;
ball0.y += ball0.vy;
ball1.x += ball1.vx;
ball1.y += ball1.vy;
checkCollision(ball0, ball1);
checkWalls(ball0);
checkWalls(ball1);
}
这样一来,我只需要给大家介绍 checkCollision 函数以及与之相配套的函数就可以
了。其它的代码没有变化,大家可以在 Billiard3.as 中看到完整的程序。
函数一开始非常简单,就是进行距离碰撞检测。
private function checkCollision(ball0:Ball, ball1:Ball):void {
var dx:Number = ball1.x - ball0.x;
var dy:Number = ball1.y - ball0.y;
var dist:Number = Math.sqrt(dx*dx + dy*dy);
if (dist < ball0.radius + ball1.radius) {
// 处理碰撞的代码
}
}
二分之三的代码已经写好了,目前为止都是小意思!首先判断碰撞需要知道两个 ball
之间的角度,使用 Math.atan2(dy, dx) 得到。(如果读到这里你还没有想到这点,请复习
第三章三角学)然后,保存计算出的正余弦值,因为我们要反复使用。
// 计算角度和正余弦值
var angle:Number = Math.atan2(dy, dx);
var sin:Number = Math.sin(angle);
var cos:Number = Math.cos(angle);
下面,对速度和小球的位置进行坐标旋转。调用旋转后的位置 x0, y0, x1, y1 然后旋
转 vx0, vy0, vx1, vy1。
因为我们使用 ball0 作为“中心点”,它的坐标就是 0,0。这个值在旋转后都不会改
变,只要写:
// 旋转 ball0 的位置
var x0:Number = 0;
var y0:Number = 0;
接下来,ball1 的位置是与 ball0 的相对位置,与刚刚计算出来的距离值 dx 和 dy 相
对应。因此,只需对这两个数进行旋转,就可以得到 ball1 旋转后的位置:
// 旋转 ball1 的位置
var x1:Number = dx * cos + dy * sin;
var y1:Number = dy * cos - dx * sin;
最后,旋转速度。写法如下:
// 旋转 ball0 的速度
var vx0:Number = ball0.vx * cos + ball0.vy * sin;
var vy0:Number = ball0.vy * cos - ball0.vx * sin;
// 旋转 ball1 的速度
var vx1:Number = ball1.vx * cos + ball1.vy * sin;
var vy1:Number = ball1.vy * cos - ball1.vx * sin;
所有的旋转代码如下所示:
private function checkCollision(ball0:Ball, ball1:Ball):void {
var dx:Number = ball1.x - ball0.x;
var dy:Number = ball1.y - ball0.y;
var dist:Number = Math.sqrt(dx*dx + dy*dy);
if (dist < ball0.radius + ball1.radius) {
// 计算角度和正余弦值
var angle:Number = Math.atan2(dy, dx);
var sin:Number = Math.sin(angle);
var cos:Number = Math.cos(angle);
// 旋转 ball0 的位置
var x0:Number = 0;
var y0:Number = 0;
// 旋转 ball1 的位置
var x1:Number = dx * cos + dy * sin;
var y1:Number = dy * cos - dx * sin;
// 旋转 ball0 的速度
var vx0:Number = ball0.vx * cos + ball0.vy * sin;
var vy0:Number = ball0.vy * cos - ball0.vx * sin;
// 旋转 ball1 的速度
var vx1:Number = ball1.vx * cos + ball1.vy * sin;
var vy1:Number = ball1.vy * cos - ball1.vx * sin;
}
}
现在怎么样,不那么可怕了吧?先叫个暂停。我们已经完成了这个艰难历程的三分之一。
接下来只需要用 vx0,ball0.mass 和 vx1,ball1.mass 就可以执行一维碰撞。根据早
先那个一维碰撞的例子可知:
var vxTotal:Number = ball0.vx - ball1.vx;
ball0.vx = ((ball0.mass - ball1.mass) * ball0.vx +
2 * ball1.mass * ball1.vx) /
(ball0.mass + ball1.mass);
ball1.vx = vxTotal + ball0.vx;
现在重写这段代码:
var vxTotal:Number = vx0 - vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 +
2 * ball1.mass * vx1) /
(ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;
只需将 ball0.vx 和 ball1.vx 替换成了旋转后的版本 vx0 和 vx1。然后插入到函数
中:
private function checkCollision(ball0:Ball, ball1:Ball):void {
var dx:Number = ball1.x - ball0.x;
var dy:Number = ball1.y - ball0.y;
var dist:Number = Math.sqrt(dx*dx + dy*dy);
if (dist < ball0.radius + ball1.radius) {
// 计算角度和正余弦值
var angle:Number = Math.atan2(dy, dx);
var sin:Number = Math.sin(angle);
var cos:Number = Math.cos(angle);
// 旋转 ball0 的位置
var x0:Number = 0;
var y0:Number = 0;
// 旋转 ball1 的位置
var x1:Number = dx * cos + dy * sin;
var y1:Number = dy * cos - dx * sin;
// 旋转 ball0 的速度
var vx0:Number = ball0.vx * cos + ball0.vy * sin;
var vy0:Number = ball0.vy * cos - ball0.vx * sin;
// 旋转 ball1 的位置
var vx1:Number = ball1.vx * cos + ball1.vy * sin;
var vy1:Number = ball1.vy * cos - ball1.vx * sin;
// 碰撞的作用力
var vxTotal:Number = vx0 - vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 +
2 * ball1.mass * vx1) /
(ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;
x0 += vx0;
x1 += vx1;
}
}
这段代码同样也把新的 x 速度加到 x 位置上,为了使小球分开,同一维碰撞的例子。
现在更新工作已完成,接下来将一切再反转回来:
// 将位置旋转回来
var x0Final:Number = x0 * cos - y0 * sin;
var y0Final:Number = y0 * cos + x0 * sin;
var x1Final:Number = x1 * cos - y1 * sin;
var y1Final:Number = y1 * cos + x1 * sin;
回忆一下旋转方程中 + 和 – 的调换,因此现在是向另一个方向旋转。最终的位置与
系统中心点 ball0 的位置相对的。因此,需要把它们都加到 ball0 的位置上,从而得到实
际在屏幕上的位置。先从 ball1 开始,因此就要用到 ball0 的初始位置,而不是更新后的
位置:
// 将位置调整为屏幕的实际位置
ball1.x = ball0.x + x1Final;
ball1.y = ball0.y + y1Final;
ball0.x = ball0.x + x0Final;
ball0.y = ball0.y + y0Final;
最后,将速度旋转回来。可以直接使用 ball 的 vx 和 vy 属性:
// 将速度旋转回来
ball0.vx = vx0 * cos - vy0 * sin;
ball0.vy = vy0 * cos + vx0 * sin;
ball1.vx = vx1 * cos - vy1 * sin;
ball1.vy = vy1 * cos + vx1 * sin;
让我们看一下这段完整的函数:
function checkCollision(ball0:Ball, ball1:Ball):void {
var dx:Number = ball1.x - ball0.x;
var dy:Number = ball1.y - ball0.y;
var dist:Number = Math.sqrt(dx*dx + dy*dy);
if (dist < ball0.radius + ball1.radius) {
// 计算角度和正余弦值
var angle:Number = Math.atan2(dy, dx);
var sin:Number = Math.sin(angle);
var cos:Number = Math.cos(angle);
// 旋转 ball0 的位置
var x0:Number = 0;
var y0:Number = 0;
// 旋转 ball1 的位置
var x1:Number = dx * cos + dy * sin;
var y1:Number = dy * cos - dx * sin;
// 旋转 ball0 的速度
var vx0:Number = ball0.vx * cos + ball0.vy * sin;
var vy0:Number = ball0.vy * cos - ball0.vx * sin;
// 旋转 ball1 的速度
var vx1:Number = ball1.vx * cos + ball1.vy * sin;
var vy1:Number = ball1.vy * cos - ball1.vx * sin;
// 碰撞的作用力
var vxTotal:Number = vx0 - vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 +
2 * ball1.mass * vx1) /
(ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;
x0 += vx0;
x1 += vx1;
// 将位置旋转回来
var x0Final:Number = x0 * cos - y0 * sin;
var y0Final:Number = y0 * cos + x0 * sin;
var x1Final:Number = x1 * cos - y1 * sin;
var y1Final:Number = y1 * cos + x1 * sin;
// 将位置调整为屏幕的实际位置
ball1.x = ball0.x + x1Final;
ball1.y = ball0.y + y1Final;
ball0.x = ball0.x + x0Final;
ball0.y = ball0.y + y0Final;
// 将速度旋转回来
ball0.vx = vx0 * cos - vy0 * sin;
ball0.vy = vy0 * cos + vx0 * sin;
ball1.vx = vx1 * cos - vy1 * sin;
ball1.vy = vy1 * cos + vx1 * sin;
}
}
实验一下这个例子。试改变 Ball 实例的大小,初始速度,质量等。
至于 checkCollision 函数,非常显眼。通过读注释,可以看出它实际上是被分做很多
简单代码段的。我们还可以做优化,或再进行因式分解消除多余的重复内容。请培养这个良
好的习惯,请见 Billiard4.as:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Point;
public class Billiard4 extends Sprite {
private var ball0:Ball;
private var ball1:Ball;
private var bounce:Number = -1.0;
public function Billiard4() {
init();
}
private function init():void {
ball0 = new Ball(150);
ball0.mass = 2;
ball0.x = stage.stageWidth - 200;
ball0.y = stage.stageHeight - 200;
ball0.vx = Math.random() * 10 - 5;
ball0.vy = Math.random() * 10 - 5;
addChild(ball0);
ball1 = new Ball(90);
ball1.mass = 1;
ball1.x = 100;
ball1.y = 100;
ball1.vx = Math.random() * 10 - 5;
ball1.vy = Math.random() * 10 - 5;
addChild(ball1);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
ball0.x += ball0.vx;
ball0.y += ball0.vy;
ball1.x += ball1.vx;
ball1.y += ball1.vy;
checkCollision(ball0, ball1);
checkWalls(ball0);
checkWalls(ball1);
}
private function checkWalls(ball:Ball):void {
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;
}
}
private function checkCollision(ball0:Ball, ball1:Ball):void {
var dx:Number = ball1.x - ball0.x;
var dy:Number = ball1.y - ball0.y;
var dist:Number = Math.sqrt(dx*dx + dy*dy);
if (dist < ball0.radius + ball1.radius) {
// 计算角度和正余弦值
var angle:Number = Math.atan2(dy, dx);
var sin:Number = Math.sin(angle);
var cos:Number = Math.cos(angle);
// 旋转 ball0 的位置
var pos0:Point = new Point(0, 0);
// 旋转 ball1 的速度
var pos1:Point = rotate(dx, dy, sin, cos, true);
// 旋转 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;
// 更新位置
pos0.x += vel0.x;
pos1.x += vel1.x;
// 将位置旋转回来
var pos0F:Object = rotate(pos0.x, pos0.y,
sin, cos, false);
var pos1F:Object = rotate(pos1.x, pos1.y,
sin, cos, false);
// 将位置调整为屏幕的实际位置
ball1.x = ball0.x + pos1F.x;
ball1.y = ball0.y + pos1F.y;
ball0.x = ball0.x + pos0F.x;
ball0.y = ball0.y + pos0F.y;
// 将速度旋转回来
var vel0F:Object = rotate(vel0.x, vel0.y,
sin, cos, false);
var vel1F:Object = rotate(vel1.x, vel1.y,
sin, cos, false);
ball0.vx = vel0F.x;
ball0.vy = vel0F.y;
ball1.vx = vel1F.x;
ball1.vy = vel1F.y;
}
}
private function rotate(x:Number, y:Number,
sin:Number, cos:Number, reverse:Boolean):Point {
var result:Point = new Point();
if (reverse) {
result.x = x * cos + y * sin;
result.y = y * cos - x * sin;
} else {
result.x = x * cos - y * sin;
result.y = y * cos + x * sin;
}
return result;
}
}
}
这里我设计了一个用作旋转的函数,rotate,传入所需的参数值,返回一个
flash.geom.Point 实例。这个对象已经定义好了 x 和 y 属性(还有许多其他的这里用不
到的属性),返回的旋转后的 Point 的 x,y 属性。虽然这个版本并不是很好读,但是可以
省去很多重复的代码。
加入更多的物体
让两个影片碰撞并带有反作用力不是件容易的事,但我们已经做到了。恭喜各位。下面
要让多个物体进行碰撞——比如说八个。听起来要复杂四倍,其实不然。之前的函数每次要
判断两个小球,而这并不是我们真正想要的。将多个物体放到舞台上,让它们运动,判断碰
撞,这是我们在碰撞检测的例子中(第九章)作过。现在所要作的就是把这些代码插入到
checkCollision 函数中的碰撞检测中去。
例子程序(MultiBilliard.as),开始将八个小球存入数组,循环执行,为它们设置不
同的属性:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Point;
public class MultiBilliard extends Sprite {
private var balls:Array;
private var numBalls:uint = 8;
private var bounce:Number = -1.0;
public function MultiBilliard() {
init();
}
private function init():void {
balls = new Array();
for (var i:uint = 0; i < numBalls; i++) {
var radius:Number = Math.random() * 20 + 20;
var ball:Ball = new Ball(radius);
ball.mass = radius;
ball.x = i * 100;
ball.y = i * 50;
ball.vx = Math.random() * 10 - 5;
ball.vy = Math.random() * 10 - 5;
addChild(ball);
balls.push(ball);
}
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
private function onEnterFrame(event:Event):void {
// 稍后给出… ...
}
// checkWalls, checkCollision, rotate 函数与上一个例子相同
}
}
大家也许注意到了,我将每个小球的初始位置人为地进行了设置,为的是不让它们一开
始就发生接触。如果那样的话,它们就会粘在一起。
onEnterFrame 方法惊人的简单。只需要做两次循环:一次是基本运动,一次是碰撞检
测。
private function onEnterFrame(event:Event):void {
for (var i:uint = 0; i < numBalls; i++) {
var ball:Ball = balls[i];
ball.x += ball.vx;
ball.y += ball.vy;
checkWalls(ball);
}
for (i = 0; i < numBalls - 1; i++) {
var ballA:Ball = balls[i];
for (var j:Number = i + 1; j < numBalls; j++) {
var ballB:Ball = balls[j];
checkCollision(ballA, ballB);
}
}
}
第一次循环,遍历所有舞台上的小球,让它们运动并在撞墙后反弹。接下来,在一个嵌
套循环中让每个小球与其它小球进行比较, 就像第九章碰撞检测中讨论的那样。 获得两个小
球的引用,分别叫作 ballA 和 ballB,将它们传入 checkCollision 函数。这样就可以了,
checkWalls, checkCollision, rotate 函数与上一个例子完全相同,这里不再敷述。
要加入更多的小球,只需改变 numBalls 变量即可,并确保它们最初不会发生碰撞。
解决潜在问题
前面提醒过:两个物体之间仍有可能在某些情况下粘在一起。最有可能发生在影片非常
拥挤的条件下, 并且在运动速度很快时结果会更糟糕。我们有可能看到两三个小球在舞台的
边角上发生碰撞。
假设舞台上有三个小球 —— ball0, ball1, ball2 —— 它们恰好要碰撞在一起。下
面是发生的基本情况:
■ 依照物体的速度移动物体。
■ 先判断 ball0 与 ball1,ball0 与 ball2,发现没有产生碰撞。
■ 判断 ball1 与 ball2。发现它俩发生了碰撞,然后计算出所有新的速度及位置,为的是
让它们不会接触到一起。但却不小心让 ball1 与 ball0 接触上了。然而,这一组判断已经
执行过了,因此这次就被忽略了。
■ 在下一次循环中,代码继续依照物体的速度移动物体。但是无意中把 ball0 和 ball1 移
动得更近了。
■ 现在代码注意到了 ball0 与 ball1 发生了碰撞。然后计算出新的速度并加到物体的当
前位置上,让它们分离。但是,因为它们已经接触上了,不能将它们真正地分开。于是又粘
到了一起。
这种情况最容易发生在空间很小物体很多,移动速度很快的情况下。也发生在,物体间
一开始就产生接触的情况下。可能迟早我们都会遇到这种情况,所以最好来看看问题出在哪。
确切的位置是在 checkCollision 函数中,定义的这两条语句:
// 更新位置
pos0.x += vel0.x;
pos1.x += vel1.x;
这里我们只是假设产生的碰撞是由两个小球的速度引起的,再给它们加入的新的速度,
就会使它们分开。多数情况下,这是可以的。但是我刚才描述的情况除外。如果那样的话,
就要明确地知道影片在运动前是分离的。于是想出了如下方法:
// 更新位置
var absV:Number = Math.abs(vel0.x) + Math.abs(vel1.x);
var overlap:Number = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x);
pos0.x += vel0.x / absV * overlap;
pos1.x += vel1.x / absV * overlap;
这些都是我自己创造的,所以我能不确定它的精确度如何,但是看上去工作得还不错。
首先确定绝对速度(自创的一个词),是所有速度的绝对值之和。例如,如果一个速度是 -5
另一个速度是 10,那么绝对值就是 5 和 10,总和就是 5 + 10 = 15。
接下来,确定小球之间重叠部分的大小。用总半径长度减去总距离。
然后根据小球速度与绝对速度的百分比,让小球移动出重叠的那一部分。
结果会让小球之间没有重叠。这种方法比早前的版本要复杂一些,但确消除了很多 bug。
在 MultiBilliard2.as 中,创建了 20 个小球,并让它们的体积大一些,在舞台上随
机分布。也许在几帧之内,小球之间可能发生重叠,但是由于新加入这些代码的作用,让它
们平静了下来。
当然,您也可以去研究自己的解决方案,如果您找到了更简单,更有效,更精确的方法,
请拿出来一起分享!
本章重要公式
本章重要公式只有一个动量守恒定理。
动量守恒的数学表达式:
动量守恒的 ActionScript 表达式:
var vxTotal:Number = vx0 - vx1;
vx0 = ((ball0.mass - ball1.mass) * vx0 +
2 * ball1.mass * vx1) /
(ball0.mass + ball1.mass);
vx1 = vxTotal + vx0;