円と線の衝突判定の実装ログ
一応、完成。一回だけすり抜けたっぽいので、まだ問題はありそうだが、ひとまずここまで。
結果
はてなダイアリーに 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; } } } }