2003/12/8

FLASH的碰撞偵測

最近讀完FLASH MX Game Design Demystified這本書的Chapter 5. Collision Detection,這章節主要是介紹一些hitTest()所做不到,或有限制的碰撞。以下是我的筆記,有興趣的朋友可以參考看看。
.所有範例檔下載

前言
經常使用hitTest()的人應該都知道,hitTest有著一些限制,例如矩形範圍限制,即使hitTest(x,y,true)也只是解決一半的問題;還有frame by frame的限制,例如當位移量大於被偵測物時,那碰撞事件就會被忽略了。
總之這個章節是讓我們練習不用hitTest,自己手工打造碰撞偵測的方法。

第一節 點對圓的碰撞
範例檔:collision.fla

點對圓的碰撞是相當簡單的,因為你只要知道點跟圓的距離,及圓本身的半徑就能測到兩者是否碰撞了。
只要 點跟圓的距離 小於 圓的半徑 那麼就表示有碰到,反之則否。
這個範例中,如上圖的描述,我們放了兩個點,一個在圓內,一個在圓外,各別計算他們離圓心的距離,再跟圓的半徑比大小,就能知道是否碰撞了。

//碰撞函式
function collision(pointMc, cycleMc) {
var dx = cycleMc.x-pointMc.x;
var dy = cycleMc.y-pointMc.y;
var distance = Math.sqrt(dx*dx+dy*dy);
trace(pointMc.name+"離圓心的距離為:"+distance);
if (distance>cyc.radius) {
trace(pointMc.name+"沒碰到圓。");
} else {
trace(pointMc.name+"碰到圓了!");
}
}

第二節 圓對圓的碰撞
範例檔:collision2.fla

這節除了判斷碰撞外,還加上了圓的運動狀態。
圓跟圓的碰撞原理也是相當容易的。
同樣先取得兩圓心間的距離,再來只要比對兩圓半徑的和。
當 兩圓心間距離 小於 兩圓半徑的和 則表示有碰到。

//碰撞函式
function collision(obj1, obj2) {
var dx = obj1.x-obj2.x;
var dy = obj1.y-obj2.y;
var distance = Math.sqrt(dx*dx+dy*dy);
if (distance>(obj1.radius+obj2.radius)) {
trace("沒事");
//cFlag = false;
} else {
trace("啊!!!!!!!!!!!撞到了!");
//cFlag = true;
}
}

第三節 圓對圓-不受frame限制
範例檔:collision3.fla

我所謂的frame限制,是指在flash中的移動,是一個個frame來動的,例如你設的位移量可能是10,那麼在flash中,就是一次10px地跳躍式前進。但真實世界中可不是這樣的,真實世界中的移動是連續性的,而不是一秒跳多少公分這樣算的。但這種限制是無可避免的,因為flash就是這樣一套建立於frame基礎上的軟體。我們只能儘量地擺脫並模擬真實世界,但其最根源還是frame by frame的。

前兩節都有受限於這樣的frame限制,下面這個範例就是要來嘗試擺脫它了
它大概的原理是,在畫面上的圓要移到下一個位置時,事先算好是否會碰撞
也因此程式會比上述的麻煩些。它把時間因素加進去算,然後用一元二次方程式的公式去解。

//預先測碰撞
function collision(obj1, obj2) {
//設解答變數,若無解則為1
collisionTime = 1;
//設定位移變數
var xmov1 = obj1.xmov;
var ymov1 = obj1.ymov;
var xmov2 = obj2.xmov;
var ymov2 = obj2.ymov;
//設舊的位置變數
var xold1 = obj1.xpos;
var yold1 = obj1.ypos;
var xold2 = obj2.xpos;
var yold2 = obj2.ypos;
//定義公式所需之各小變數
var R = obj1.r+obj2.r;
var a = -2*xmov1*xmov2+xmov1*xmov1+xmov2*xmov2;
var b =-2*xold1*xmov2-2*xold2*xmov1+2*xold1*xmov1+2*xold2*xmov2;
var c =-2*xold1*xold2+xold1*xold1+xold2*xold2;
var d =-2*ymov1*ymov2+ymov1*ymov1+ymov2*ymov2;
var e =-2*yold1*ymov2-2*yold2*ymov1+2*yold1*ymov1+2*yold2*ymov2;
var f =-2*yold1*yold2+yold1*yold1+yold2*yold2;
var g = a+d;
var h = b+e;
var k = c+f-R*R;
//計算公式
var sqRoot = Math.sqrt(h*h-4*g*k);
var t1 = (-h+sqRoot)/(2*g);
var t2 = (-h-sqRoot)/(2*g);
//若第一個根小於1且大於0
if (t1>0 && t1< =1) {
collisionTime = t1;
cycleCollided = true;
}
//若第二根小於1且大於0
if (t2>0 && t2< =1) {
//且第二根小於第一根或是無第一根,則以第二根為解
if (collisionTime == null || t2<=t1) {
collisionTime = t2;
cycleCollided = true;
}
}
if (cycleCollided) {
//發出碰撞訊號
trace("碰到了!!");
}
}
二元二次就是像: aX平方+bX+c=0這種的 而解法是: 2a分之 -b 加減 根號 b平方-4ac (因為不會打數學符號,其實看圖比較清楚) 前面AS碼中,t1及t2代表兩個根 若這個根大於0且小於1的話,就代表他們有碰到了。 若兩個根都符合條件,那就取小的為答案。 照書中的範例弄出來的結果會像下圖 但我個人認為有個小缺失,就是它雖然脫離了frame的限制,抓到了碰撞訊號,但畫面中的球仍是疊在一起。也就是雖然抓到了訊號,也抓到了正確的碰撞時間,但最後沒有畫出正確的碰撞位置。 因此我加了小小的改進。把算出的碰撞時間,乘以其位移量,畫出正確的碰撞位置。 結果變成下圖 我想該書作者不是不知道啦… 也許是有別的考量,因此沒有在這?討論這個問題。 第四節 線與斜率 範例檔:collision4_drawLine.fla 這一節也是相當簡單,就是從已知的y軸截點b,及斜率m,畫出一條線。 斜率的公式我想大概是高中的吧 (沒讀過高中…這部份如果講錯,請指正) 斜率公式:斜率(m)=直邊(y)/橫邊(x) 斜率公式2:y=m*x+b (b為線與y軸之交界點y值) 然後用
_root.createEmptyMovieClip("clip", 1);
clip.lineStyle(0, 0x000000, 100);
line1 = {};
line1.m = 1;
line1.b = 100;
function findY(line, x) {
var y = line.m*x+line.b;
return y;
}
function drawLine(line) {
//決定一個 x
var x = 200;
//求得y
var y = findY(line, x);
//線的起點
clip.moveTo(x, y);
//決定另一個x
var x = 100;
//求另一個y
var y = findY(line, x);
//畫出線
clip.lineTo(x, y);
}
drawLine(line1);
可畫出來 相當容易的一個練習 第五節 線與線的交叉 範例檔:collision5_drawLine2.fla 當兩條的斜率不同,則此兩線將會交叉 這一節就是要做這個練習,先在場景中畫出兩斜率不同的線, 並求出其交叉點的位置 畫線的部份就如同上一節 求交叉點的部份原理如下 已知:y=m1*x+b1 y=m2*x+b2 所以:m1*x+b1=m2*x+b2 m1*x-m2*x=b2-b1 x=(b2-b1)/(m1-m2) 改寫成as code就是:
function findIntersection(line_a, line_b) {
var x = (line_b.b-line_a.b)/(line_a.m-line_b.m);
var y = line_a.m*x+line_a.b;
dot._x = x;
dot._y = y;
}
第六節 判斷碰撞是否發生 範例檔:collision6_drawLine3.fla 這一節仍是上一節的延伸, 上一次知道當斜率不同時,此兩線將會交叉,但不能得知這個交叉是否發生了。 一切也是延用上一節的做法,只是最後再判斷一下交叉點有沒有在線上。 而這個判斷法也只是用x、y值比比大小而已,所以不會太複雜。 所謂 兩線將會交叉,但目前未發生,就像 而目前已交叉會像 這樣應該很清楚了 重點如下
//判斷目前是否發生交叉
function collision(lineA, lineB) {
if ((dot._x>=lineA.x1 and dot._x< =lineA.x2) ||
(dot._x<=lineA.x1 and dot._x>=lineA.x2) || 
(dot._y>=lineA.y1 and dot._y< =lineA.y2) ||
(dot._y<=lineA.y1 and dot._y>=lineA.y2)) {
lineA.collision = true;
}
if ((dot._x>=lineB.x1 and dot._x< =lineB.x2) ||
(dot._x<=lineB.x1 and dot._x>=lineB.x2) ||
(dot._y>=lineB.y1 and dot._y< =lineB.y2) ||
(dot._y<=lineB.y1 and dot._y>=lineB.y2)) {
lineB.collision = true;
}
if (lineA.collision and lineB.collision) {
trace("目前發生碰撞了!!!!");
} else {
trace("目前沒事。");
}
}
第七節 球與線的碰撞 範例檔:collision7_cycleLine.fla 這節的主旨是偵測移動中的球與線之間的碰撞,書中提出四個步驟: step1.找出球的運動路徑與線的交叉點 step2.運用三角函數,找出球碰到線時的位置 step3.同上,找出球跟線碰到的接觸點 step4.計算從目前位置到碰觸位置所需的frame數(時間),若大於0小於等於1,則表示碰撞發生。 配合下圖解釋: 重點如下:
function getFrames(tempLine, point) {
//========Step 1============
//球路徑斜率
var slope2 = point.ymov/point.xmov;
//預防斜率無限大(垂直線)或無限小(水平線)狀況
if (slope2 == Number.POSITIVE_INFINITY) {
var slope2 = 1000000;
} else if (slope2 == Number.NEGATIVE_INFINITY) {
var slope2 = -1000000;
}
//球路徑的y軸截點b2
var b2 = point.y-slope2*point.x;
//求出球路徑與線的交叉點
var x = (b2-tempLine.b)/(tempLine.slope-slope2);
var y = tempLine.slope*x+tempLine.b;
//===========Step 2===============
//球路徑的角度
var theta = Math.atan2(point.ymov, point.xmov);
//球路徑與線之夾角
var gamma = theta-tempLine.angle;
//碰觸時,"球圓心"與"球路徑及線交叉點"之距離
var sinGamma = Math.sin(gamma);
var r = point.radius/sinGamma;
//求出碰觸時圓心座標 x, y
var x = x-r*Math.cos(theta);
var y = y-r*Math.sin(theta);
//==================Step 4================
//求出碰觸所需之frame數
var dis =Math.sqrt((x-point.x)*(x-point.x)+(y-point.y)*(y-point.y));
var vel =Math.sqrt(point.xmov*point.xmov+point.ymov*point.ymov);
var frames = dis/vel;
//======================Step 3=================
//找出碰觸點
//求tempLine.slope的垂直線=slope2a
var slope2a = -1/tempLine.slope;
var b2a = y-slope2a*x;
//碰觸點座標xa,ya
var xa = (tempLine.b-b2a)/(slope2a-tempLine.slope);
var ya = slope2a*xa+b2a;
//檢查碰觸點是否在線段上
if ((xa>tempLine.x1 && xa< templine .x2) ||(xa< tempLine.x1 && xa>tempLine.x2) ||((ya>tempLine.y1 && ya<templine .y2) ||(ya<tempLine.y1 && ya>tempLine.y2))) {
//是
} else {
//否
//讓frame數到1000
var frames = 1000;
}
return frames;
}
function getTempPositions() {
ball.tempx = ball.x+ball.xmov;
ball.tempy = ball.y+ball.ymov;
}
function bankCollisionDetect() {
for (var i = 0; i< linearray .length; ++i) {
var frame = getFrames(lineArray[i], ball);
if (frame<=1 && frame>0) {
//碰到了
trace("撞到了!!");
}
}
}
Step1就像之前介紹過的求兩線交叉點的方法。 Step2之所以會寫成這樣,可能就要看一下圖了
var theta = Math.atan2(point.ymov, point.xmov); //先求得theta角
var gamma = theta-tempLine.angle; //進而求得gamma角
var sinGamma = Math.sin(gamma);
var r = point.radius/sinGamma; //利用gamma角及圓半徑,取得r之長度
var x = x-r*Math.cos(theta); 
//利用r之長度,及之前找到的球路徑與線交叉點座標,來取得球碰撞時的圓心座標
var y = y-r*Math.sin(theta);
Step4 很簡單,只要知道目前離碰撞點的距離,除以每frame位移距離,就能得知目前到碰撞所需的frame數 Step3 要先知道,球跟線碰撞時,碰撞點跟圓心連成的那條半徑,必定是跟碰撞線形成垂直關係的。 所以我們把那條半徑算出來,再用之前介紹的線與線碰撞偵測,就能方便算出碰撞點的座標了。 最後再檢查這個點有沒有在碰撞線上,就大功告成了。 第八節 點跟矩形的碰撞偵測 範例檔:collision8_pointRectangle.fla 這節比上述幾節簡單許多,只用單純的比較x y大小而已 重點函式如下:
function pointRectangleDetection(point, rectangle) {
//點的位置
var x = point.x;
var y = point.y;
//矩形的左右邊界
var x1 = rectangle.x;
var x2 = x1+rectangle.width;
//上下邊界
var y1 = rectangle.y;
var y2 = y1+rectangle.height;
//以比大小方式,判斷點是否在矩形?
if (x>x1 && x< x2 && y>y1 && y< y2) {
trace("碰到了!!");
}
}
有一點值得討論一下 書中範例在最後執行的部份是這樣
EnterFrame = function() {
getTempPositions();
pointRectangleDetection(point1, rectangle1);
render();
};
所以當第一次發出碰撞訊號時,黑點其實已經畫到碰撞的下一格位置了。 要修正這點,我想可能要在render()之前做個判斷,或是乾脆把碰撞偵測跟render寫成一個function吧 不過這只是個範例,所以也不是那麼重要吧… 第九節 矩形跟矩形的碰撞偵測 範例檔:collision9_2rectangle.fla 矩形之所以簡單,是因為都是直角,也就是能夠直接用x y 值比一比就行了 (去年做的橫向捲軸demo,就是用這種方式) 這節也是用類似上一節的方式。比較兩矩形邊界的X Y值,來判斷是否碰撞。 後續討論 此章節主要內容大概就到這邊了,後面加了一些討論,順便記在這裏: 關於多邊形的碰撞呢?例如一個八角形跟一個球的碰撞該如何做? 用上述討論的球與線的碰撞,做八次就可以了 同理我們也可以做六角形、五角形、星形等複雜圖形的碰撞。 ch5總算k完了,接著ch6才是碰撞反應,趁這幾天有點空,一次把這兩章k完才算完整。

沒有留言: