ボールの衝突判定(ActionScript3.0)

前回(ボールの放り投げ)を発展させて、複数のボールに対しての衝突シミュレーションを作ってみました。
サンプル:




↑1個つかんで投げるとエネルギーが増えて面白いです。
まずは、準備としてクラスを2つ用意します。

// Ball.as
package {
    import flash.display.Sprite;
    import flash.events.*;
    import flash.geom.Point;
    class Ball extends Sprite {
     ~
    }
}
// Test01.as
package {
    import flash.display.*;
    import flash.events.Event;
    import Ball;
    public class Test01 extends MovieClip {
       ~
    }
}
こんな感じです。Text01.asを適当な.flaファイルのドキュメントクラスに設定し、コンパイルすれば自分で定義したBallクラスを自由に扱えるようになります。Ball.asの全ソースは以下のようになります。
// Ball.as
package {
    import flash.display.Sprite;
    import flash.events.*;
    import flash.geom.Point;
    class Ball extends Sprite {

        // パラメータ
        private var sw:Boolean = false;
        private var click_x:int = 0;
        private var click_y:int = 0;
        private var mouseX_bef:int;
        private var mouseY_bef:int;
        public var v_x:Number = 0;
        public var v_y:Number = 0;
        private var rub:Number = 1.05; // 摩擦
        private var ela:Number = 0.80; // 弾性率
        private var a_x:Number = 0.0; // 重力x
        private var a_y:Number = 0.2; // 重力y
        private var v_x_0:Number = 2.0;
        private var v_y_0:Number = 2.0;
        private var container:*;
        private var limitX:int;
        private var limitY:int;
        private var color:int;
       
        // construct
        public function Ball(con:*, scale:int, col=0xFF0000):void {
            container = con;
            limitX = con.width-scale*2;
            limitY = con.height-scale*2;
           
            color = col;
           
            var g = this.graphics;
            g.beginFill (col, 1.0);    // 面のスタイル設定
            g.drawCircle  (scale, scale, scale);
            x = limitX * Math.random();
            y = limitY * Math.random();
            v_x = v_x_0 * Math.random();
            v_y = v_y_0 * Math.random();
            container.addChild(this);
           
            // イベントリスナー
            addEventListener(MouseEvent.MOUSE_DOWN, switchOn);
            container.addEventListener(MouseEvent.MOUSE_UP, switchOff);
            container.addEventListener(MouseEvent.MOUSE_MOVE, moveBall);
            container.addEventListener(Event.ENTER_FRAME, throwBall);
        }
       
        // マウスダウンするとスイッチON&ズレ分取得
        private function switchOn(event:MouseEvent):void {
            sw = true;
            click_x = event.stageX - x;
            click_y = event.stageY - y;
        }
       
        // マウスアップするとスイッチ
        private function switchOff(event:MouseEvent):void {
            sw = false;
        }
       
        // ボールを移動する&速度指定
        private function moveBall(event:MouseEvent):void {
            if(sw) {
                x = container.mouseX - click_x;
                y = container.mouseY - click_y;
                v_x  = container.mouseX - mouseX_bef;
                v_y = container.mouseY - mouseY_bef;
                mouseX_bef = container.mouseX;
                mouseY_bef = container.mouseY;
            }
        }
       
        // ボールを投げる
        private function throwBall(event:Event):void {
            if(!sw) { // 捕まえられたら止まる
           
                // 例外対策
                if(!v_x) v_x = 0;
                if(!v_y) v_y = 0;
               
                // 画面外に行かないように
                if(x < 0) x = 0;
                if(x > limitX) x = limitX;
                if(y < 0) y = 0;
                if(y > limitY) y = limitY;
               
                // 端っこで跳ね返る
                if((x + v_x) < 0 || (x + v_x) > limitX) {
                    v_x = -v_x/ela;
                }
                if((y + v_y) < 0 || (y + v_y) > limitY) {
                    v_y = -v_y/ela;
                }
                   
                // 摩擦
                v_x = v_x/rub;
                v_y = v_y/rub;
               
                // 重力
                if(x < stage.stageWidth - width*2) v_x += a_x;
                if(y < stage.stageHeight - width*2) v_y += a_y;
               
                // 移動
                x += v_x
                y += v_y;

               
            }
        }
       
        // 衝突判定
        function crash(obj:Ball):void {
            var o_slf:Point = new Point(x, y);
            var o_obj:Point = new Point(obj.x, obj.y);
            if( Point.distance(o_slf, o_obj) < (width/2 + obj.width/2) )  {
                // 中心方向とx軸との成す角を求める
                var c_x:Number = obj.x - x;
                var c_y:Number = obj.y - y;
                var theta:Number = Math.atan(Math.abs(c_y/c_x));
               
                // 重なりを無くす
                var c_len:Number = Math.sqrt(c_x*c_x + c_y*c_y);
                var len:Number = (width/2 + obj.width/2) - Point.distance(o_slf, o_obj);
                if(c_len) {
                    obj.x += len*c_x/c_len;
                    obj.y += len*c_y/c_len;
                }
               
                // ベクトルをθ回転する
                var v_u:Number = v_x*Math.cos(theta) - v_y*Math.sin(theta);
                var v_v:Number = v_x*Math.sin(theta) + v_y*Math.cos(theta);
                var obj_v_u:Number = obj.v_x*Math.cos(theta) - obj.v_y*Math.sin(theta);
                var obj_v_v:Number = obj.v_x*Math.sin(theta) + obj.v_y*Math.cos(theta);
               
                // u方向のみ運動量保存則を適用(質量が同じと仮定し速度交換)
                var tmp:Number = v_u/ela;
                v_u = obj_v_u/ela;
                obj_v_u = tmp;
               
               
                // ベクトルを -θ回転する
                v_x = v_u*Math.cos(theta) + v_v*Math.sin(theta);
                v_y = -v_u*Math.sin(theta) + v_v*Math.cos(theta);
                obj.v_x = obj_v_u*Math.cos(theta) + obj_v_v*Math.sin(theta);
                obj.v_y = -obj_v_u*Math.sin(theta) + obj_v_v*Math.cos(theta);

            }
        }
    }
}

長くてすみません(汗)。基本的にはこの間と大差ありません。ただしコンストラクタにより引数で与えられたステージやコンテナに自身を追加するようになってるところと、衝突判定がついたところが大きな違いです。それでは衝突判定について説明します。

080605_1.png
fig.1  衝突判定の説明図

fig.1を見てください。ボールとボールとの距離はc_lenで与えられます。これは、それぞれの中心座標をポイントとして、Point.distance()メソッドを使用したり、自身のxと対象となるオブジェクトのxの差であるc_xと同様に求めたc_yの2乗の和のルートを取ったりして取得することが出来ます。Ball.as中には2つの表記方法を用いてみましたので参考にしてください。さて、衝突判定は
if( Point.distance(o_slf, o_obj) < (width/2 + obj.width/2) )
このようにして、中心間の距離が、それぞれの半径の和よりも小さいかどうかで判定出来ます。では衝突後の運動はどのように記述すればいいのでしょうか。高校で"運動量保存則"は習ったでしょうか?質量と速度の積を運動量と呼びますが、2つの物体の衝突に関して運動量の総量は変化しない、といった法則です。
m1*v1 + m2*v2 = m1*v1' + m2 + v2'
こんな感じです。今回は衝突する2つの物体の質量は同じ、と仮定しましたので
v1 + v2 = v1' + v2'
こんな風にかけます。ただしこれでは2変数(衝突後の自身の速度と相手の速度)が分かりませんので、もう1つ何かしらの式が必要です。これには、エネルギー保存の法則の式などが適用できますが、今回は衝突係数=1を用いた式で行いたいと思います。すなわち、壁にボールを投げると同じ速度で跳ね返ってくるのと同じように、ボールが衝突した後は、衝突する前に向かい合っていたスピードで遠ざかっていくという考え方です。これは、
v2 - v1 = -(v2' - v1')
このような式で書くことが出来ます。これらの連立方程式を解くと
v1' = v2
v2' = v1
Quick Lookup:
という速度交換の式が成り立ちます。これを用いて衝突後のプログラムを書いていきましょう。ビリヤードのように、ボールの右端に当てれば左のほうへ飛んでいく、といった様子を再現します。そこで、座標軸の回転を使って一度x,yをu,vという座標に変換して運動量保存則を適用する、といったことをします。

080605_2.png
fig2.  衝突時の運動量保存則

衝突方向をu軸、反射方向をv軸として運動量保存則を適用します。これには先ほど求めたc_xとc_yを使ってθを求めます。
θ = atan(c_y/c_x)
このように、アークタンジェント(タンジェントの逆数)を使って角度を求めます。ActionScriptでは、
theta = Math.atan(Math.abs(c_y/c_x));
と、Mathクラスのメソッドを利用します。さて座標軸の回転ですが、これには数Cの知識が必要で、回転行列を用いて計算します。回転行列とは、
cosθ   -sinθ
sinθ   cosθ
の2*2行列ですが、虚数の掛け算が回転という知識を持っていれば簡単に導けます。
X+i*Y = (x+i*y)*(cosθ + i*sinθ)
         = (x*cosθ - y*sinθ) + i*(x*sinθ + y*cosθ)
∴X = x*cosθ - y*sinθ
   Y = x*sinθ + y*cosθ
といった感じです。これより、座標の回転⇒衝突方向についての運動量保存則⇒座標を元に戻すといった3つの作業を行うことによって衝突を記述できます。
// ベクトルをθ回転する
var v_u:Number = v_x*Math.cos(theta) - v_y*Math.sin(theta);
var v_v:Number = v_x*Math.sin(theta) + v_y*Math.cos(theta);
var obj_v_u:Number = obj.v_x*Math.cos(theta) - obj.v_y*Math.sin(theta);
var obj_v_v:Number = obj.v_x*Math.sin(theta) + obj.v_y*Math.cos(theta);
              
// u方向のみ運動量保存則を適用(質量が同じと仮定し速度交換)
var tmp:Number = v_u/ela;
v_u = obj_v_u/ela;
obj_v_u = tmp;
               
               
// ベクトルを -θ回転する
v_x = v_u*Math.cos(theta) + v_v*Math.sin(theta);
v_y = -v_u*Math.sin(theta) + v_v*Math.cos(theta);
obj.v_x = obj_v_u*Math.cos(theta) + obj_v_v*Math.sin(theta);
obj.v_y = -obj_v_u*Math.sin(theta) + obj_v_v*Math.cos(theta);
いかがでしょうか。これを応用して作ったのが今回のサンプルです。for文で指定個数のBallインスタンスを作成し、enterFrameHandlerで衝突判定を行っています。衝突判定には、
for(var m:int=0; m<ball_num; m++) {
    for(var n:int=m+1; n<ball_num; n++) {
        ball_mc[m].crash(ball_mc[n]);
    }
}
このように、ボールの個数の約2乗の半分回(nC2)だけ判定しています。なので200個とかやると激重です...。どなたかいい判定方法知っていたら教えてください。
...ちなみに、座標の回転をしなくてもx軸方向、y軸方向それぞれについて運動量保存則を適用しても記述できます。
var tmp:Number;
tmp = v_x;
v_x = obj.v_x;
obj.v_x = tmp;

tmp = v_y;
v_y = obj.v_y;
obj.v_y = tmp;
が、これだと挙動がおかしくなります(例えば少しずらして正面衝突しても、普通に跳ね返るだけです)。試してみてください。
長くなりましたが、今回は以上です。

ボールの放り投げ

ActionScript3.0始めました。今日はボールの放り投げについてのソースを書いてみました。ステージ内でボールをクリックするとドラッグ出来ます。そのまま放り投げたとき、ステージの端で跳ね返る運動を書いてみました。
サンプル:

// イベントリスナー
my_ball.addEventListener(MouseEvent.MOUSE_DOWN, switchOn);
stage.addEventListener(MouseEvent.MOUSE_UP, switchOff);
stage.addEventListener(MouseEvent.MOUSE_MOVE, moveBox);
stage.addEventListener(Event.ENTER_FRAME, throwBox);

// パラメータ
var sw:Boolean = false;
var click_x:int = 0;
var click_y:int = 0;
var mouseX_bef:int;
var mouseY_bef:int;
var v_x:Number = 0;
var v_y:Number = 0;
var rub:Number = 1.01; // 摩擦
var ela:Number = 1.2; //弾性率
var limitX:int = stage.stageWidth - my_ball.width;
var limitY:int = stage.stageHeight - my_ball.height;

// マウスダウンするとスイッチON&ズレ分取得
function switchOn(event:MouseEvent):void {
    sw = true;
    click_x = event.stageX - my_ball.x;
    click_y = event.stageY - my_ball.y;
}

// マウスアップするとスイッチ
function switchOff(event:MouseEvent):void {
    sw = false;
}

// ボックスを移動する&速度指定
function moveBox(event:MouseEvent):void {
    if(sw) {
        my_ball.x = stage.mouseX - click_x;
        my_ball.y = stage.mouseY - click_y;
        v_x  = stage.mouseX - mouseX_bef;
        v_y = stage.mouseY - mouseY_bef;
        mouseX_bef = stage.mouseX;
        mouseY_bef = stage.mouseY;
    }
}

// ボールを投げる
function throwBox(event:Event):void {
    if(!sw) { // 捕まえられたら止まる
        // 画面外に行かないように
        if(my_ball.x < 0) my_ball.x = 0;
        if(my_ball.x > limitX) my_ball.x = limitX;
        if(my_ball.y < 0) my_ball.y = 0;
        if(my_ball.y > limitY) my_ball.y = limitY;
       
        // 端っこで跳ね返る
        if((my_ball.x + v_x) < 0 || (my_ball.x + v_x) > limitX) {
            v_x = -v_x/ela;
        }
        if((my_ball.y + v_y) < 0 || (my_ball.y + v_y) > limitY) {
            v_y = -v_y/ela;
        }
           
        // 摩擦
        v_x = v_x/rub;
        v_y = v_y/rub;
       
        // 移動
        my_ball.x += v_x
        my_ball.y += v_y;
       
        // 動き続けないように
        if(Math.abs(v_x) < 0.01) v_x = 0;
        if(Math.abs(v_y) < 0.01) v_y = 0;
    }
}


ちなみに、改良するとこんなんなります。