根据这些规律可以得到下列方程组:
解该方程组,得到下面的公式:
把这二个公式相减,可以得到:
即:
我们也经常利用这个公式简化运算
基本的动量守恒演示:
先给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自身为旋转中心,所以自身旋转后的相对坐标都是0 var 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了,同时对于粘连问题的解决,采用了更一种算法
后记:弄懂了本文中的这些玩意儿有啥用呢?让我想想,或许...公司需要开发一款桌面台球游戏时,这东西就能派上用场吧.