動能公式:
動量公式:
動量守恒:
能量守恒:
根據這些規律可以得到下列方程組:
解該方程組,得到下面的公式:
把這二個公式相減,可以得到:
即:
我們也經常利用這個公式簡化運算
基本的動量守恒示範:
先給ball類添加一個品質"屬性"
package {import flash.display.Sprite;//小球 類public class Ball extends Sprite {public var radius:uint;//半徑public var color:uint;//顏色public var vx:Number=0;//x軸速度public var vy:Number=0;//y軸速度public var count:uint=0;//輔助計數變數public var isDragged=false;//是否正在被拖動public var vr:Number=0;//旋轉速度public var mass:Number = 1;//品質public function Ball(r:Number=50,c:uint=0xff0000) {this.radius=r;this.color=c;init();}private function init():void {graphics.beginFill(color);graphics.drawCircle(0,0,radius);graphics.endFill();}}}
一維單軸剛體碰撞測試:
package {import flash.display.Sprite;import flash.events.Event;public class Billiard1 extends Sprite {private var ball0:Ball;private var ball1:Ball;private var bounce:Number = -0.6;public function Billiard1() {init();}private function init():void {ball0=new Ball(40);addChild(ball0);ball1=new Ball(20,0x0000ff);addChild(ball1);ReStart();}private function ReStart():void{ball0.mass=2;ball0.x=50;ball0.y=stage.stageHeight/2;ball0.vx=5;ball1.mass=1;ball1.x=300;ball1.y=stage.stageHeight/2;ball1.vx=-5;addEventListener(Event.ENTER_FRAME,EnterFrameHandler);}private function EnterFrameHandler(event:Event):void {ball0.x+=ball0.vx;ball1.x+=ball1.vx;var dist:Number=ball1.x-ball0.x;//如果撞到了if (Math.abs(dist)<ball0.radius+ball1.radius) {var vdx:Number = ball0.vx - ball1.vx;var vx0Final:Number=((ball0.mass-ball1.mass)*ball0.vx + 2*ball1.mass*ball1.vx)/(ball0.mass+ball1.mass);var vx1Final:Number= vx0Final + vdx;ball0.vx=vx0Final;ball1.vx=vx1Final;//不加下面這二句的話,從視覺效果上看,有可能會看到二個球相互撞入對方球體內了,這樣就不符合物理學"剛體"模型的定義ball0.x+=ball0.vx;ball1.x+=ball1.vx;}//舞台邊界反彈if (ball0.x >=stage.stageWidth-ball0.radius || ball0.x<=ball0.radius){ball0.x -= ball0.vx;ball0.vx *= bounce;}if (ball1.x >=stage.stageWidth-ball1.radius || ball1.x<=ball1.radius){ball1.x -= ball1.vx;ball1.vx *= bounce;}trace(ball1.vx,ball0.vx);//如果二球都停了if (Math.abs(ball1.vx)<=0.05 && Math.abs(ball0.vx)<=0.05){removeEventListener(Event.ENTER_FRAME,EnterFrameHandler);ReStart();}}}}
二維座標上的剛體碰撞:
先來看這張圖,紅球a以Va速度運動,藍球b以Vb速度運動,二球的連線正好與x軸平行(即:水平對心碰撞),碰撞的過程可以理解為二球水平速度分量Vax,Vbx應用運量守恒與能力守恒的結果(y軸方向的速度不受影響!)
但很多情況下,二球的連線並非總是與座標軸平行,比如下面這樣:
思路:仍然利用座標旋轉,先將二個球反向旋轉到連線水平位置,然後按常規方式處理,完事後再旋轉回來。
var ballA:Ball=new Ball(80,Math.random()*0xffffff);var ballB:Ball=new Ball(50,Math.random()*0xffffff);var bounce:Number=-1;ballA.x=ballA.radius+100;ballB.x=ballA.radius+200;ballA.y=120;ballB.y=300;ballA.mass=2;ballB.mass=1;ballA.vx = 5*(Math.random()*2-1);ballB.vx = 5*(Math.random()*2-1);ballA.vy = 5*(Math.random()*2-1);ballB.vy = 5*(Math.random()*2-1);addChild(ballA);addChild(ballB);addEventListener(Event.ENTER_FRAME,EnterFrameHandler);function EnterFrameHandler(e:Event):void {ballA.x+=ballA.vx;ballA.y+=ballA.vy;ballB.x+=ballB.vx;ballB.y+=ballB.vy;//運量守恒處理開始var dx:Number=ballB.x-ballA.x;var dy:Number=ballB.y-ballA.y;var dist:Number=Math.sqrt(dx*dx+dy*dy);if (dist<(ballA.radius + ballB.radius)) {var angle:Number=Math.atan2(dy,dx);var cos:Number=Math.cos(angle);var sin:Number=Math.sin(angle);//以ballA中心為旋轉中心反向旋轉var xA:Number=0;//ballA自身為旋轉中心,所以自身旋轉後的相對座標都是0var yA:Number=0;var xB:Number=dx*cos+dy*sin;var yB:Number=dy*cos-dx*sin;//先(反向)旋轉二球相對(ballA的)速度var vxA=ballA.vx*cos+ballA.vy*sin;var vyA=ballA.vy*cos-ballA.vx*sin;var vxB=ballB.vx*cos+ballB.vy*sin;var vyB=ballB.vy*cos-ballB.vx*sin;//旋轉後的vx速度處理運量守恒var vdx=vxA-vxB;var vxAFinal = ((ballA.mass - ballB.mass)*vxA + 2*ballB.mass*vxB)/(ballA.mass + ballB.mass);var vxBFinal=vxAFinal+vdx;//相對位置處理xA+=vxAFinal;xB+=vxBFinal;//處理完了,再旋轉回去//先處理座標位置var xAFinal:Number=xA*cos-yA*sin;var yAFinal:Number=yA*cos+xA*sin;var xBFinal:Number=xB*cos-yB*sin;var yBFinal:Number=yB*cos+xB*sin;//處理最終的位置變化ballB.x=ballA.x+xBFinal;ballB.y=ballA.y+yBFinal;ballA.x+=xAFinal;ballA.y+=yAFinal;//再處理速度ballA.vx=vxAFinal*cos-vyA*sin;ballA.vy=vyA*cos+vxAFinal*sin;ballB.vx=vxBFinal*cos-vyB*sin;ballB.vy=vyB*cos+vxBFinal*sin;}//<--- 運量守恒處理結束CheckBounds(ballA);CheckBounds(ballB);}//舞台邊界檢測function CheckBounds(b:Ball) {if (b.x<b.radius) {b.x=b.radius;b.vx*=bounce;} else if (b.x>stage.stageWidth-b.radius) {b.x=stage.stageWidth-b.radius;b.vx*=bounce;}if (b.y<b.radius) {b.y=b.radius;b.vy*=bounce;} else if (b.y>stage.stageHeight-b.radius) {b.y=stage.stageHeight-b.radius;b.vy*=bounce;}}
粘連問題:
反覆運行上面這段動畫,偶爾可能會發現二個球最終粘在一起,無法分開了,造成這種原因的情況很多,下面的分析了可能的形成原因之一
解決思路:找出重疊部分,然後把二個小球同時反向移動適當距離,讓二個球分開即可
先來一段測試代碼:驗證一下是否有效
var ballA:Ball=new Ball(80,0xff0000);ballA.x=stage.stageWidth/2;ballA.y=stage.stageHeight/2;addChild(ballA);var ballB:Ball=new Ball(60,0x00ff00);ballB.x=stage.stageWidth/2-70;ballB.y=stage.stageHeight/2;addChild(ballB);btn1.x=stage.stageWidth/2;btn1.y=stage.stageHeight-btn1.height;btn1.addEventListener(MouseEvent.MOUSE_DOWN,MouseDownHandler);function MouseDownHandler(e:MouseEvent):void {var overlap:Number=ballA.radius+ballB.radius-Math.abs(ballA.x-ballB.x);//計算重疊部分trace(overlap);//計算每個球所佔重疊部分中的比例var aRadio:Number = ballA.radius/(ballA.radius + ballB.radius);var bRadio:Number = ballB.radius/(ballA.radius + ballB.radius);//分離判斷if (overlap>0){if (ballA.x>ballB.x){ballA.x += overlap*aRadio;ballB.x -= overlap*bRadio;}else{ballA.x -= overlap*aRadio;ballB.x += overlap*bRadio;}}}ballA.addEventListener(MouseEvent.MOUSE_DOWN,startDragHandler);ballB.addEventListener(MouseEvent.MOUSE_DOWN,startDragHandler);ballA.addEventListener(MouseEvent.MOUSE_OVER,MouseOverHandler);ballA.addEventListener(MouseEvent.MOUSE_OUT,MouseOutHandler);ballB.addEventListener(MouseEvent.MOUSE_OVER,MouseOverHandler);ballB.addEventListener(MouseEvent.MOUSE_OUT,MouseOutHandler);stage.addEventListener(MouseEvent.MOUSE_UP,stopDragHandler);var obj:Ball;var rect:Rectangle = new Rectangle(0,stage.stageHeight/2,stage.stageWidth,0);function startDragHandler(e:MouseEvent):void {Mouse.cursor = MouseCursor.HAND;obj=e.currentTarget as Ball;obj.startDrag();}function stopDragHandler(e:MouseEvent):void {if (obj!=null) {obj.stopDrag(true,rect);obj=null;Mouse.cursor = MouseCursor.AUTO;}}function MouseOverHandler(e:MouseEvent):void{Mouse.cursor = MouseCursor.HAND;}function MouseOutHandler(e:MouseEvent):void{Mouse.cursor = MouseCursor.AUTO;}
水平拖動小球故意讓它們重疊,然後點擊“分開”按鈕測試一下,ok,管用了!
再回過頭來解決運量守恒中的粘連問題:
只要把EnterFrameHandler中的
//相對位置處理 xA+=vxAFinal; xB+=vxBFinal;
換成:
//相對位置處理(同時要防止粘連)//xA+=vxAFinal;//xB+=vxBFinal;var sumRadius = ballA.radius + ballB.radius;var overlap:Number=sumRadius-Math.abs(xA-xB);//計算重疊部分//trace(overlap);//計算每個球所佔重疊部分中的比例var aRadio:Number = ballA.radius/sumRadius;var bRadio:Number = ballB.radius/sumRadius;//分離判斷if (overlap>0){if (xA>xB){xA += overlap*aRadio;xB -= overlap*bRadio;}else{xA -= overlap*aRadio;xB += overlap*bRadio;}}
最後老規矩:來一個群魔亂舞,把一堆球放在一塊兒亂撞
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()*40+10;var ball:Ball=new Ball(radius,Math.random()*0xffffff);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 {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);}}}//舞台邊界檢測function checkWalls(b:Ball) {if (b.x<b.radius) {b.x=b.radius;b.vx*=bounce;} else if (b.x>stage.stageWidth-b.radius) {b.x=stage.stageWidth-b.radius;b.vx*=bounce;}if (b.y<b.radius) {b.y=b.radius;b.vy*=bounce;} else if (b.y>stage.stageHeight-b.radius) {b.y=stage.stageHeight-b.radius;b.vy*=bounce;}}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;}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;// 更新位置 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;// 將位置旋轉回來 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;}}}}
註:這段代碼做了最佳化,把一些公用的部分提取出來封裝成function了,同時對於粘連問題的解決,採用了更一種演算法
後記:弄懂了本文中的這些玩意兒有啥用呢?讓我想想,或許...公司需要開發一款案頭撞球遊戲時,這東西就能派上用場吧.