11.3.2 两个轴上的动量守恒

OK,深呼吸,进入下一级。现在,我们已经用过了这个冗长的公式,而它几乎是即插即用型的。

只需要把两个物体的质量和速度插入公式中,就可以得到结果。下面,进入更为复杂的一级——二维空间。本章开始时说过,其策略是使用坐标旋转。让我们来看看原因。

1.理解原理及策略

图 11-2 表示刚才看到那个例子:一维的碰撞。

图11-2

图 11-2 一维碰撞

可以看到,两个物体有着不同的大小,不同的质量以及不同的速度。速度用箭头表示,就是向量。回忆一下,速度向量指针表示运动的方向,长度表示速度的大小。

一维的例子非常简单,因为两个速度向量都在 x 轴上。所以,可以直接加上或减去它们的量值(长度)。现在,请看图 11-3,小球在二维空间中的碰撞。

图11-3

图 11-3 二维碰撞

速度向量完全不同了,不能单纯地将速度代入到动量守恒公式中,因为,这会导致完全错误的结果。那么应该如何解决呢?

通过旋转第二张图,可以得到与第一张非常相似的图。首先,要知道两个小球之间形成的角度并且旋转整个场景——坐标和速度——逆时针旋转。例如,如果角度是 30 度,就将所有的物体旋转 -30 度,这与第十章的斜面反弹是一样的。结果如图 11-4 所示。

图11-4

图 11-4 旋转后的二维碰撞

两个小球之间的角度非常重要,这个角度称为碰撞角度。重要的原因在于,它是小球速度的一部分——速度的一部分要依赖于角度。

下面请看图 11-5。这里,在每个向量上面加入了 vx 和 vy。注意,两个小球的 vx 都严格地依照碰撞的角度来定。

图11-5

图 11-5 加入 x,y 速度

我们说过,只关心碰撞的角度。那么现在它就是 vx,可以忘掉 vy。如图 11-6 所示。

图11-6

图 11-6 只关心 x 速度

看得眼熟吗?这就是第一张图! 现在可以使用即插即用的动量公式来解决这个问题了。(请注意解题的步骤!)

在应用这个公式时,会得出两个新的 vx 值。记住 vy 的值永远不变。vx 的变化如图11-7 所示。

图11-7

图 11-7 新的 x 速度,y 速度不变,得出新的速度

猜到下面该怎么办了吗?只需要把一切旋转回去,如图 11-8 所示。

图11-8

图 11-8 将一切旋转回来

下面把这个过程转换为代码。对我来说最难的地方就是要一再地说服大家“这很容易”。

2.编写代码

首先要让两个小球以一定的角度运动并且最后相互碰撞。开始的设置与前面相同,两个小球实例:ball0 和 ball1。这次让它们变大一点,如图 11-9 所示,这样它们碰撞的机会就会大一些。

图11-9

图11-9 二维动量守恒,设置舞台

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;

	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
		{
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			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)
			{
				// calculate angle, sine and cosine
				var angle:Number = Math.atan2(dy, dx);
				var sin:Number = Math.sin(angle);
				var cos:Number = Math.cos(angle);
				
				// rotate ball0's position
				var x0:Number = 0;
				var y0:Number = 0;
				
				// rotate ball1's position
				var x1:Number = dx * cos + dy * sin;
				var y1:Number = dy * cos - dx * sin;
				
				// rotate ball0's velocity
				var vx0:Number = ball0.vx * cos + ball0.vy * sin;
				var vy0:Number = ball0.vy * cos - ball0.vx * sin;
				
				// rotate ball1's velocity
				var vx1:Number = ball1.vx * cos + ball1.vy * sin;
				var vy1:Number = ball1.vy * cos - ball1.vx * sin;
				
				// collision reaction
				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;

				// rotate positions back
				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;

				// adjust positions to actual screen positions
				ball1.x = ball0.x + x1Final;
				ball1.y = ball0.y + y1Final;
				ball0.x = ball0.x + x0Final;
				ball0.y = ball0.y + y0Final;

				// rotate velocities back
				ball0.vx = vx0 * cos - vy0 * sin;
				ball0.vy = vy0 * cos + vx0 * sin;
				ball1.vx = vx1 * cos - vy1 * sin;
				ball1.vy = vy1 * cos + vx1 * sin;
			}
		}
	}
}

这些内容想必大家睡梦中都可以写出来。设置边界,随机的速度,加入质量,根据速度移动小球,判断边界。注意,我把边界的判断单独放在了 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.display.StageScaleMode;
	import flash.display.StageAlign;
	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
		{
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			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)
			{
				// calculate angle, sine and cosine
				var angle:Number = Math.atan2(dy, dx);
				var sin:Number = Math.sin(angle);
				var cos:Number = Math.cos(angle);
				
				// rotate ball0's position
				var pos0:Point = new Point(0, 0);
				
				// rotate ball1's position
				var pos1:Point = rotate(dx, dy, sin, cos, true);
				
				// rotate ball0's velocity
				var vel0:Point = rotate(ball0.vx,
										ball0.vy,
										sin,
										cos,
										true);
				
				// rotate ball1's velocity
				var vel1:Point = rotate(ball1.vx,
										ball1.vy,
										sin,
										cos,
										true);
				
				// collision reaction
				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;

				// update position
				pos0.x += vel0.x;
				pos1.x += vel1.x;
				
				// rotate positions back
				var pos0F:Object = rotate(pos0.x,
										  pos0.y,
										  sin,
										  cos,
										  false);
										  
				var pos1F:Object = rotate(pos1.x,
										  pos1.y,
										  sin,
										  cos,
										  false);

				// adjust positions to actual screen positions
				ball1.x = ball0.x + pos1F.x;
				ball1.y = ball0.y + pos1F.y;
				ball0.x = ball0.x + pos0F.x;
				ball0.y = ball0.y + pos0F.y;
				
				// rotate velocities back
				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 属性。虽然这个版本并不是很好读,但是可以省去很多重复的代码。

3.加入更多的物体

让两个影片碰撞并带有反作用力不是件容易的事,但我们已经做到了。恭喜各位。下面要让多个物体进行碰撞——比如说八个。听起来要复杂四倍,其实不然。之前的函数每次要判断两个小球,而这并不是我们真正想要的。将多个物体放到舞台上,让它们运动,判断碰撞,这是我们在碰撞检测的例子中(第九章)作过。现在所要作的就是把这些代码插入到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 变量即可,并确保它们最初不会发生碰撞。

4.解决潜在问题

前面提醒过:两个物体之间仍有可能在某些情况下粘在一起。最有可能发生在影片非常拥挤的条件下,并且在运动速度很快时结果会更糟糕。我们有可能看到两三个小球在舞台的边角上发生碰撞。

假设舞台上有三个小球 —— ball0, ball1, ball2 —— 它们恰好要碰撞在一起。下面是发生的基本情况:

这种情况最容易发生在空间很小物体很多,移动速度很快的情况下。也发生在,物体间一开始就产生接触的情况下。可能迟早我们都会遇到这种情况,所以最好来看看问题出在哪。确切的位置是在 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 个小球,并让它们的体积大一些,在舞台上随机分布。也许在几帧之内,小球之间可能发生重叠,但是由于新加入这些代码的作用,让它们平静了下来。

当然,您也可以去研究自己的解决方案,如果您找到了更简单,更有效,更精确的方法,请拿出来一起分享!

文件下载(已下载 2382 次)

发布时间:2011/6/28 上午10:15:34  阅读次数:9683

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号