円と線の衝突判定の実装ログ

一応、完成。一回だけすり抜けたっぽいので、まだ問題はありそうだが、ひとまずここまで。

結果

はてなダイアリーに Flash を埋め込むガジェット - てっく煮ブログ 跡地で提供されているガジェットを使わせてもらうことにした。とりあえず、キーボードの上下左右で移動できる。

コード

「円と線の衝突判定」。円だけが動き、線は固定。反発係数は考慮しているが、摩擦係数や回転は考慮していない。


衝突判定の部分は、図がないとたぶん分からない。


真面目にやると、「平坦な地面の上を横移動」の時に「少しだけ下に速度を持ちながら地面に接触」→「少しだけバウンド」→「重力に引かれてまた接触」→「少しだけバウンド」→...という感じで5回程度の試行回数では止まってしまうので、「重力に引かれてまた接触」を阻止するために「接触したらその試行の中では加速度は働かない」という風にした。1/60秒分の加速度の働きを削るだけなので、そんなにおかしくはならないはず。

//mxmlc Main.as
//author Show=O=Healer

package {
	//Common
	import flash.display.*;
	import flash.events.*;

	//Debug
	import flash.text.*;
	import flash.ui.*;
	import flash.utils.*;

	//Input
	import flash.ui.Keyboard;

	//PV3D
	import org.papervision3d.core.*;

	//Class
	public class Main extends Sprite {
		//Common
		private var m_Container	: Sprite;

		//Player
		private var m_Player:Sprite;

		//Block
		private var m_BlockManager:BlockManager;

		//Input
		private var m_InputL:int = 0;
		private var m_InputR:int = 0;
		private var m_InputU:int = 0;
		private var m_InputD:int = 0;

		//Param
		private const PLAYER_VEL:Number = 160.0;
		private const GRAVITY:Number = 1000.0;

		//Debug
		public static var m_Label:TextField;


		//Constructor
		public function Main():void {
			{//Init Common
				//リサイズ対応
				stage.addEventListener(Event.RESIZE, onStageResize);

				//定期的にupdateを呼ばせる
				addEventListener(Event.ENTER_FRAME, update);
			}

			{//Init Block
				m_BlockManager = new BlockManager();
				addChild(m_BlockManager);

				m_BlockManager.m_PlayerSphere.m_CollisionAcc.y = GRAVITY;
			}

			{//Init Player
				m_Player = new Sprite();
				var playerRad:Number = m_BlockManager.m_PlayerSphere.m_CollisionRad;
				m_Player.graphics.lineStyle(1, 0x221100, 1.0);
				m_Player.graphics.beginFill(0xAA8822);
				m_Player.graphics.drawCircle(0, 0, playerRad);
				m_Player.graphics.endFill();
				addChild(m_Player);
			}

			{//Init Input
				stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
				stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
			}

			{//Init Debug
				m_Label = new TextField();
				m_Label.autoSize = TextFieldAutoSize.LEFT;
				m_Label.selectable=false
				addChild(m_Label);
			}
		}


		//=Common=

		private function update( event:Event ):void {
			//プレイヤーの移動速度をセット
			m_BlockManager.m_PlayerSphere.m_CollisionVel.x = PLAYER_VEL * (m_InputR - m_InputL);
			if(m_InputU > 0){
				m_BlockManager.m_PlayerSphere.m_CollisionVel.y = -PLAYER_VEL;
			}

			//コリジョンを元に位置更新
			m_BlockManager.UpdateCollision();

			//更新した位置を表示に反映
			m_Player.x = m_BlockManager.m_PlayerSphere.m_CollisionPos.x;
			m_Player.y = m_BlockManager.m_PlayerSphere.m_CollisionPos.y;
		}

		private function onStageResize(event:Event):void {
			m_Container.x = stage.stageWidth	/ 2;
			m_Container.y = stage.stageHeight / 2;
		}


		//=Input=
		private function onKeyDown(event:KeyboardEvent):void{
			if(event.keyCode == Keyboard.LEFT){
				m_InputL = 1;
			}
			if(event.keyCode == Keyboard.RIGHT){
				m_InputR = 1;
			}
			if(event.keyCode == Keyboard.UP){
				m_InputU = 1;
			}
			if(event.keyCode == Keyboard.DOWN){
				m_InputD = 1;
			}
		}
		private function onKeyUp(event:KeyboardEvent):void{
			if(event.keyCode == Keyboard.LEFT){
				m_InputL = 0;
			}
			if(event.keyCode == Keyboard.RIGHT){
				m_InputR = 0;
			}
			if(event.keyCode == Keyboard.UP){
				m_InputU = 0;
			}
			if(event.keyCode == Keyboard.DOWN){
				m_InputD = 0;
			}
		}
	}
}


//=Local Class=

import flash.display.*;


//=3次元ベクトル=
class CVector
{
	//メンバ
	public var x:Number = 0;
	public var y:Number = 0;
	public var z:Number = 0;

	//コンストラクタ
	public function CVector(in_x:Number = 0, in_y:Number = 0, in_z:Number = 0):void{
		x = in_x;
		y = in_y;
		z = in_z;
	}


	//長さ
	public function Length():Number{
		return Math.sqrt(x*x + y*y + z*z);
	}


	//=各種オペレータ=
	//本体には影響を与えない
	//vecA + vecB の代わりに vecA.Plus(vecB) とやる感じ
	//あるいは vecA.Dot(vecB) の延長で vecA.Plus(vecB) がある感じ

	//和
	public function Plus(rhs:CVector):CVector{
		return new CVector(x + rhs.x, y + rhs.y, z + rhs.z);
	}

	//差
	public function Minus(rhs:CVector):CVector{
		return new CVector(x - rhs.x, y - rhs.y, z - rhs.z);
	}

	//内積
	public function Dot(rhs:CVector):Number{
		return x * rhs.x + y * rhs.y + z * rhs.z;
	}

	//外積
	public function Cross(rhs:CVector):CVector{
		return new CVector(y * rhs.z - z * rhs.y, z * rhs.x - x * rhs.z, x * rhs.y - y * rhs.x);
	}

	//スケール
	public function Scale(scl:Number):CVector{
		return new CVector(x * scl, y * scl, z * scl);
	}

	//正規化
	public function Normal():CVector{
		var distance:Number = Length();
		if(distance > 0.0){
			return new CVector(x/distance, y/distance, z/distance);
		}
		return new CVector(0, 0, 0);//err
	}

	//線形補間
	public function Lerp(rhs:CVector, ratio:Number):CVector{
		//this * (1 - ratio) + rhs * ratio
		return Scale(1.0 - ratio).Plus(rhs.Scale(ratio));
	}

	//値の比較
	public function Equals(rhs:CVector):Boolean{
		return (x == rhs.x) && (y == rhs.y) && (z == rhs.z);
	}
}


//=球体コリジョン=
class CollisionSphere
{
	//中心位置
	public var m_CollisionPos:CVector;
	//移動速度
	public var m_CollisionVel:CVector = new CVector(0, 0, 0);
	//加速度
	public var m_CollisionAcc:CVector = new CVector(0, 0, 0);

	//半径
	public var m_CollisionRad:Number;

	//コンストラクタ
	public function CollisionSphere(in_Pos:CVector, in_R:Number):void {
		m_CollisionPos = in_Pos;

		m_CollisionRad = in_R;
	}
}


//=線コリジョン=
class CollisionLine
{
	//中心位置
	public var m_CollisionPos:CVector;
//	public var m_CollisionVel:CVector = new CVector(0, 0, 0);
//	public var m_CollisionAcc:CVector = new CVector(0, 0, 0);

	//線の方向
	public var m_AxisA:CVector;
	//線の長さ(端から端まで)
	public var m_WidthA:Number;

	//コンストラクタ
	public function CollisionLine(in_Pos:CVector, in_AxisA:CVector, in_WidthA:Number):void {
		m_CollisionPos = in_Pos;

		m_AxisA = in_AxisA;
		m_WidthA = in_WidthA;
	}
}


//=ブロックの配置=
var BLOCK_DATA:Array = [
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1],
	[0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1],
	[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1],
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];


//=ブロックの管理と衝突管理(できれば分けたい)=
class BlockManager extends Sprite
{
	//=CollisionSphere=

	//プレイヤの球コリジョン(できればもっと自由に登録できるようにしたい)
	public var m_PlayerSphere:CollisionSphere = new CollisionSphere(new CVector(10.0, 10.0), 8.0);


	//=CollisionLine=

	//各ラインコリジョン
	private var m_PlaneCollisionList:Array = new Array();


	//=Param=

	//ブロックの幅
	private const W:int = 20;
	private const H:int = 20;


	//コンストラクタ
	public function BlockManager():void {
		//ブロックの描画の登録と、その境界線上にコリジョン配置
		BLOCK_DATA.forEach(
			function(innerArray:Array, indexY:int, array:Array):void{
				innerArray.forEach(
					function(val:int, indexX:int, array2:Array):void{
						//描画すべきブロックの登録
						if(val == 1){
							CreateBlock(indexX, indexY);
//							m_BlockCollisionList.push(new CollisionBox(0.5*W + W*indexX, 0.5*H + H*indexY, W, H));
						}

						//境界線上にコリジョンを配置(端が抜けているが、今回は気にしない)
						var pos:CVector;
						var axis:CVector;
						if(indexY > 0){
							if(BLOCK_DATA[indexY-1][indexX] != val){
								pos = new CVector(W * (indexX+0.5), H * (indexY));
								axis = new CVector(1, 0);
								m_PlaneCollisionList.push(new CollisionLine(pos, axis, W));
							}
						}
						if(indexX > 0){
							if(BLOCK_DATA[indexY][indexX-1] != val){
								pos = new CVector(W * (indexX), H * (indexY+0.5));
								axis = new CVector(0, 1);
								m_PlaneCollisionList.push(new CollisionLine(pos, axis, H));
							}
						}
					}
				);
			}
		);

		//画面の左右の端にコリジョン追加
		var edgePos:CVector;
		var edgeAxis:CVector;
		var edgeHeight:Number = 1000;//H * BLOCK_DATA.length;
		edgePos = new CVector(0, 0);
		edgeAxis = new CVector(0, 1);
		m_PlaneCollisionList.push(new CollisionLine(edgePos, edgeAxis, edgeHeight));
		edgePos = new CVector(W * BLOCK_DATA[0].length, 0);
		edgeAxis = new CVector(0, 1);
		m_PlaneCollisionList.push(new CollisionLine(edgePos, edgeAxis, edgeHeight));
	}

	//指定箇所にブロックを作成
	public function CreateBlock(x:int, y:int):void{
		var block:Sprite = new Sprite();
		block.graphics.lineStyle(1, 0x4b2503, 1.0);
		block.graphics.beginFill(0x8b4513);
		block.graphics.drawRect(-0.5*W, -0.5*H, W, H);
		block.graphics.endFill();
		block.x = 0.5*W + W * x;
		block.y = 0.5*H + H * y;
		addChild(block);
	}


	//コリジョンの位置更新
	public function UpdateCollision():void{

		//Debug
		Main.m_Label.text = "";

		//この秒数経ったものとして計算
		var deltaTime:Number = 1.0/60.0;

		{//各「球コリジョン」に対して計算(今は一つだけ)
			//球のパラメータ
			var sphere:CollisionSphere = m_PlayerSphere;
			var sphereRad:Number = sphere.m_CollisionRad;
			var sphereBoundRatio:Number = 1.0;//円側の反射率

			//残りの処理時間(衝突のたびに、移動にかかった時間を引いていく)
			var restTime:Number = deltaTime;

			//現時点での位置など(衝突のたびに、その直後のパラメータをセット)
			var nowPos:CVector = sphere.m_CollisionPos;
			var nowVel:CVector = sphere.m_CollisionVel;
			var nowAcc:CVector = sphere.m_CollisionAcc;


			//衝突の計算をcountの許す限り行う
			for(var count:Number = 0; count < 5; count++)//continueでここまで戻ってきてもう一度処理する
			{
				//count以上の計算が必要な場合、すり抜ける可能性がある
				//ただ、これをなくすと無限ループに入る可能性がある

				//計算上の次の位置など
				//nextPos = nowPos + nowVel * restTime + 0.5 * nowAcc * restTime * restTime;
				//nextVel = nowVel + nowAcc * restTime;
				var nextPos:CVector = nowPos.Plus(nowVel.Scale(restTime)).Plus(nowAcc.Scale(0.5 * restTime * restTime));
				var nextVel:CVector = nowVel.Plus(nowAcc.Scale(restTime));

				//移動しようとする量
				var movePos:CVector = nextPos.Minus(nowPos);
				//移動の長さ
				var moveDistance:Number = movePos.Length();

				//ヒットしたら、その情報をこれに入れる
				var hitPlane:CollisionLine= null;
				var hitTime:Number = restTime;
				var hitPos:CVector = nowPos;
				var hitVel:CVector;

				//各ラインに対して衝突判定
				m_PlaneCollisionList.forEach(
					function(plane:CollisionLine, index:int, arr:Array):void{
						//円と線のヒットチェック
						//相対的に考えて、点と「円で引いた線」のヒットで考える
						//円で引いた線の「両端の縁の部分」と「上下の線分」で分けて考える
						var planeBoundRatio:Number = 0.5;//plane側の反射率

						//=両端の円との衝突チェック=
						{
							var hitCheckCircle:Function = function(dir:Number):void{
								//円の中心位置
								var pos:CVector = plane.m_CollisionPos.Plus(plane.m_AxisA.Normal().Scale(dir*0.5*plane.m_WidthA));

								//円の中心と移動開始点の相対位置
								var gap:CVector = nowPos.Minus(pos);

								//垂線の長さ
								var distance:Number;
								if(moveDistance > 0.0){
									//円の中心から移動線に向かって下ろした垂線
									var perpendicular:CVector = movePos.Cross(new CVector(0, 0, 1)).Normal();

									//垂線の長さ
									distance = Math.abs(perpendicular.Dot(gap));
								}else{
									//動いてないなら、中心間の距離にしておく
									//垂線の長さ
									distance = gap.Length();
								}

								if(distance <= sphereRad){
									//そのまま移動し続ければ円の中を通る

									if(movePos.Dot(pos.Minus(nowPos)) >= 0.0){
										//円の内側に移動する場合のみ判定

										//垂線がdistance、斜線がsphereRadとした時の三角形の残りの辺の長さ
										var diff:Number = Math.sqrt(sphereRad*sphereRad - distance*distance);
										//ヒットするのに必要な距離
										var hitMove:Number;
										if(moveDistance > 0.0){
											hitMove = Math.abs(movePos.Normal().Dot(gap)) - diff;
										}else{
											hitMove = 0.0;//gap.Length();
										}
										if(hitMove <= moveDistance){
											//円の中まで届いた=ヒットした
											//加速などを考慮すると、この計算は正しくない(近似)
											var ratio:Number;
											var localHitPos:CVector;
											if(moveDistance > 0){
												ratio = hitMove/moveDistance;
												localHitPos = nowPos.Plus(nextPos.Minus(nowPos).Scale(ratio));
											}else{
												ratio = 0;
												localHitPos = pos.Plus(gap.Scale(sphereRad/gap.Length()));
											}
											var localHitVel:CVector = nowVel.Lerp(nextVel, ratio);//衝突時点の速度
											var nrm:CVector = localHitPos.Minus(pos).Normal();
											//vel -= nrm * (1 + bound)*vel.Dot(nrm)
											localHitVel = localHitVel.Minus(nrm.Scale((1.0 + sphereBoundRatio * planeBoundRatio) * localHitVel.Dot(nrm)));//→反射時の速度
											var localHitTime:Number = restTime * ratio;
											if(localHitTime < hitTime){
												//他のより先に当たっていたら採用
												if(! hitPos.Equals(localHitPos)){
													//前回の衝突位置とは違ったら続ける
													hitPlane = plane;
													hitTime = localHitTime;
													hitPos = localHitPos;
													hitVel = localHitVel;
												}
											}
										}
									}
								}
							};

							hitCheckCircle(1);//片方のチェック
							hitCheckCircle(-1);//もう片方のチェック
						}

						//=上下の線分との衝突チェック=
						{
							var hitCheckPlane:Function = function(dir:Number):void{
								var planeNrm:CVector = plane.m_AxisA.Cross(new CVector(0, 0, 1)).Normal();

								//線分の中心位置
								var pos:CVector = plane.m_CollisionPos.Plus(planeNrm.Scale(dir*sphereRad));

								//線分の中心位置との相対位置
								var nowGap:CVector = nowPos.Minus(pos);
								var nextGap:CVector = nextPos.Minus(pos);

								if(planeNrm.Dot(nowGap) * planeNrm.Dot(nextGap) <= 0.0){
									//線分をまたいで移動している

									if(dir * planeNrm.Dot(movePos) <= 0.0){//こちらの線分に向かってきている

										//線分と交差する場合の位置
										var localHitPos:CVector;
										if(dir * planeNrm.Dot(movePos) < 0.0){
											//hitpos = nowPos + (nextPos - nowPos) * (planeNrm.Dot(nowGap)/moveDistance);
											localHitPos = nowPos.Plus(nextPos.Minus(nowPos).Scale(Math.abs(planeNrm.Dot(nowGap)/moveDistance)));
										}else{
											if(Math.abs(planeNrm.Dot(nowGap)) > sphereRad){
												localHitPos = nowPos;
											}else{
												//nextPos - planeNrm * (sphereRad - Abs(planeNrm.Dot(pos - nextPos)))
												//nextPos + planeNrm * (sphereRad - Abs(planeNrm.Dot(pos - nextPos)))
												localHitPos = nextPos.Plus(planeNrm.Scale(dir * (sphereRad - Math.abs(planeNrm.Dot(nextGap)))));
//												localHitPos = nextPos.Minus(planeNrm.Scale(sphereRad - Math.abs(planeNrm.Dot(nextGap))));
											}
										}

										//交差点と中心からの距離
										var hitDistance:Number = Math.abs(plane.m_AxisA.Dot(localHitPos.Minus(pos)));

										if(hitDistance <= 0.5*plane.m_WidthA){
											//その距離が幅以内ならヒットしている
											var ratio:Number;
											if(moveDistance > 0){
												ratio = nowPos.Minus(localHitPos).Length()/moveDistance;
											}else{
												ratio = 0;
											}
											var localHitVel:CVector = nowVel.Lerp(nextVel, ratio);//衝突時点の速度
											//vel -= nrm * (1 + bound)*vel.Dot(nrm)
											localHitVel = localHitVel.Minus(planeNrm.Scale((1.0 + sphereBoundRatio * planeBoundRatio) * localHitVel.Dot(planeNrm)));//→反射時の速度
											var localHitTime:Number = restTime * ratio;
											if(localHitTime < hitTime){
												//他のより先に当たっていたら採用
												if(! hitPos.Equals(localHitPos)){
													//前回の衝突位置とは違ったら続ける
													hitPlane = plane;
													hitTime = localHitTime;
													hitPos = localHitPos;
													hitVel = localHitVel;
												}
											}
										}
									}
								}
							};

							hitCheckPlane(1);//片方のチェック
							hitCheckPlane(-1);//もう片方のチェック
						}
					}
				);

				//何かとぶつかっていたら、新しい位置と方向を元に再び処理を開始
				if(hitPlane != null){
					restTime -= hitTime;
					nowPos = hitPos;
					nowVel = hitVel;

					nowAcc = new CVector();//微小時間の加速度は無視してみる(地面スレスレを行く場合に止まってしまうので)

					continue;
				}

				//ぶつかっていなければ、求めた移動先に移動して終了
				sphere.m_CollisionPos = nextPos;
				sphere.m_CollisionVel = nextVel;

				break;
			}
		}
	}
}

予定

疲れたので今日はここまで。来週は「円と線」から「球と面」の衝突判定に移る予定。それができたら再びWiiとつないで動かす。その前にPV3Dにつないでグラフィックの傾きに対応する方が先か。