Murayama blog.

AIの民主化。

衝突判定 by 距離


本日も続いて衝突判定を試してみます。


今回はhitTestObjectメソッド、hitTestPointメソッドから離れて、距離にもとづく衝突判定を考えてみます。


ここで取り扱うのは2つの円の衝突です。正確に言うと真円(縦横の幅が等しい円)を扱います。
2つの真円が衝突するということはこういうことです。


で、衝突判定をするには、2つの円の半径から衝突するために必要な距離を算出できます。


つまり、

2つの円の距離 < 円Aの半径 + 円Bの半径

の条件を満たすと衝突しているということになります。


この例では、円が真横に並んでるのでわかりやすいけど、実際は縦の位置が違うこともあります。
そんなときに2つの円の距離を算出するには、ピタゴラスの定理を使えばOKです。

// dxは横軸の距離、dyは縦軸の距離
var dist = Math.sqrt(dx * dx + dy * dy);


この仕組みを使ってのサンプルがこちら。


コードはこちら。

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	
	[SWF(width=600, height=600)]
	public class Bubble2 extends Sprite
	{
		private var balls:Array;
		private var ballCount:Number = 30;
		private var bounce:Number = -0.5;
		private var spring:Number = -0.05;
		private var gravity:Number = 0.1;
		
		public function Bubble2()
		{
			init();
		}
		
		private function init():void{
			balls = new Array;
			for(var i:uint; i < ballCount; i++){
				var ball:Ball = new Ball(Math.random() * 30 + 20, Math.random() * 0xFFFFFF);
				ball.x = Math.random() * 600;
				ball.y = Math.random() * 600;
				ball.vx = Math.random() * 6 - 3;
				ball.vy = Math.random() * 6 - 3;
				addChild(ball);
				balls.push(ball);
			}
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}


		private function onEnterFrame(evt:Event):void
		{
			// 最後の1回は不要
			for(var i:uint = 0; i < ballCount - 1; i++){
				var ball0:Ball = balls[i];
				// 次のボールからチェック
				for(var j:uint = i + 1; j < ballCount; j++){
					var ball1:Ball = balls[j];
					// 距離を算出
					var dx:Number = ball1.x - ball0.x;
					var dy:Number = ball1.y - ball0.y;
					var dist:Number = Math.sqrt(dx * dx + dy * dy);
					// 衝突判定
					var minDist:Number = ball0.radious + ball1.radious;
					if(dist < minDist){
						// 角度
						var angle:Number = Math.atan2(dy, dx);
						// 対象(ball0のオフセットを考慮)
						var tx:Number = ball0.x + Math.cos(angle) * minDist;
						var ty:Number = ball0.y + Math.sin(angle) * minDist;
						// 対象とballの距離にバネ係数を掛ける
						var ax:Number = (tx - ball1.x) * spring;
						var ay:Number = (ty - ball1.y) * spring;
						// 加速(本と+ーを反対にしたけどあってる?)
						ball0.vx += ax;
						ball0.vy += ay;
						ball1.vx -= ax;
						ball1.vy -= ay;
					}
				}
			}
			for(i = 0; i < ballCount; i++){
				var ball:Ball = balls[i];
				move(ball);
			}
		}
		
		private function move(ball:Ball):void{
			ball.vy += gravity;
			ball.x += ball.vx;
			ball.y += ball.vy;
			
			if(ball.x + ball.radious > stage.stageWidth){
				ball.x = stage.stageWidth - ball.radious;
				ball.vx *= bounce;
			}else if(ball.x - ball.radious < 0){
				ball.x = ball.radious;
				ball.vx *= bounce;
			}

			if(ball.y + ball.radious > stage.stageHeight){
				ball.y = stage.stageHeight - ball.radious;
				ball.vy *= bounce;
			}else if(ball.y - ball.radious < 0){
				ball.y = ball.radious;
				ball.vy *= bounce;
			}
		}
	}
}


import flash.display.Sprite;

class Ball extends Sprite
{
	public var radious:Number;
	private var color:uint;
	public var vx:Number = 0;
	public var vy:Number = 0;

	public function Ball(radius:Number = 40, color:uint = 0xff0000)
	{
		this.radious = radius;
		this.color = color;
		init();
	}

	private function init():void{
		graphics.clear();
		graphics.beginFill(color);
		graphics.drawCircle(0, 0, radious);
		graphics.endFill();
	}
	
	public function setColor(color:uint):void
	{
		this.color = color;
		init();
	}
}


コードの要点を抜粋します。

			// 最後の1回は不要
			for(var i:uint = 0; i < ballCount - 1; i++){
				var ball0:Ball = balls[i];
				// 次のボールからチェック
				for(var j:uint = i + 1; j < ballCount; j++){
					var ball1:Ball = balls[j];
					// 距離を算出
					var dx:Number = ball1.x - ball0.x;
					var dy:Number = ball1.y - ball0.y;
					var dist:Number = Math.sqrt(dx * dx + dy * dy);
					// 衝突判定
					var minDist:Number = ball0.radious + ball1.radious;
					if(dist < minDist){
						// 角度
						var angle:Number = Math.atan2(dy, dx);
						// 対象(ball0のオフセットを考慮)
						var tx:Number = ball0.x + Math.cos(angle) * minDist;
						var ty:Number = ball0.y + Math.sin(angle) * minDist;
						// 対象とballの距離にバネ係数を掛ける
						var ax:Number = (tx - ball1.x) * spring;
						var ay:Number = (ty - ball1.y) * spring;
						// 加速(本と+ーを反対にしたけどあってる?)
						ball0.vx += ax;
						ball0.vy += ay;
						ball1.vx -= ax;
						ball1.vy -= ay;
					}
				}
			}


ここでは不要な判定をなくすため、iとjの初期値、ループ条件をいじっています。
2つのボールの衝突を検査するために必要なball0とball1の組み合わせを最低限にしてるかんじです。


あと、このコードではball0を衝突対象(壁のようなイメージ)のオブジェクトとしています。
ball1が対象に衝突するオブジェクト(車のようなイメージ)です。
そのため、衝突したオブジェクトball1に対して速度をマイナス処理している、
言い換えるなら、衝突した車はスピードが落ちる、ようにball1.vx -= axとしています。


あと、

		// 角度
		var angle:Number = Math.atan2(dy, dx);
		// 対象(ball0のオフセットを考慮)
		var tx:Number = ball0.x + Math.cos(angle) * minDist;
		var ty:Number = ball0.y + Math.sin(angle) * minDist;


の処理は、こんなふうに変換できます。
cosは隣辺/斜辺なので、dx / dist
sinは対辺/斜辺なので、dy / dist
てことで、

		// 対象(ball0のオフセットを考慮)
		var tx:Number = ball0.x + dx / dist * minDist;
		var ty:Number = ball0.y + dy / dist * minDist;

みたいに書けます。
言われてみたら納得だけど、自分で気づくかと言われたら微妙。


以上、第9章おしまい。