canvas動畫包教不包會:碰撞檢測

2020-06-18 09:31 更新
碰撞檢測是物體與物體之間的交互,其實在前面的邊界檢測也是一種碰撞檢測,只不過檢測的對象是物體與邊界之間。在本章中,我們將介紹更多的碰撞檢測,比如:兩個物體間的碰撞檢測、一個物體與一個點的碰撞檢測、基于距離的碰撞檢測等等碰撞檢測方法。

什么是碰撞檢測呢?

簡單來說,碰撞檢測就是判定兩個物體是否在同一時間內(nèi)占用一塊空間,用數(shù)學(xué)的角度來看,就是兩個物體有沒有交集。

檢測碰撞的方法有很多,一般我們使用如下兩種:
  • 從幾何圖形的角度來檢測,就是判斷一個物體是否與另一個有重疊,我們可以用物體的矩形邊界來判斷。
  • 檢測距離,就是判斷兩個物體是否足夠近到發(fā)生碰撞,需要計算距離和判斷兩個物體是否足夠近。

1、基于幾何圖形的碰撞檢測
基于幾何圖形的碰撞檢測,一般情況下是檢查一個矩形是否與其他矩形相交,或者某一個坐標(biāo)點是否落在矩形內(nèi)。

1.1 兩個物體間的碰撞檢測(矩形邊界檢測法)
在上一章中,我們介紹了一個 getBound() 方法,參數(shù)為球?qū)ο?,返回矩形對象?/div>

function getBound(body){   

  return {   

    x: (body.x - body.radius),   

    y: (body.y - body.radius),   

    width: body.radius * 2,   

    height: body.radius * 2   

  };   

}

現(xiàn)在我們已經(jīng)知道如何獲取物體的矩形邊界,那么只需檢測兩個對象的邊界框是否相交,就可以判斷兩個物體是否碰撞了。我們在 tool.js 工具類中添加一個工具函數(shù) tool.intersects :

tool.intersects = function(bodyA,bodyB){

  return !(bodyA.x + bodyA.width < bodyB.x ||

          bodyB.x + bodyB.width < bodyA.x ||

          bodyA.y + bodyA.height < bodyB.y ||

          bodyB.y + bodyB.height < bodyA.y);

};

這個函數(shù)傳入兩個矩形對象,如果返回true,表示兩個矩形相交了;否則,返回false。(如果你看不明白這段代碼,請看下圖,讓一個矩形分別位于另一個矩形的上下左右位置):


檢測函數(shù)已經(jīng)知道了,當(dāng)要檢測兩個物體是否相交時,就可以做如下判斷:

if(tool.intersects(objectA,objectB)){

  console.log('撞上了');

}

注意:這里傳入的必須是矩形對象。如果是球,可調(diào)用getBound()方法返回矩形對象。如果已經(jīng)是矩形對象,就直接傳入。

這里有一個需要注意的問題,有些時候,我們的物體是不規(guī)則的,如果我們采取矩形邊界檢測,有時候會不精確(只有真正的矩形才是精確的):

在上面的圖中,有矩形、圓形和五角形,我們都可以采取矩形邊界檢測法,不過,你會發(fā)現(xiàn),當(dāng)物體是不規(guī)則的形狀時,雖然通過上面的 tool.intesects() 方法判斷兩個物體已經(jīng)碰撞,但實際上并沒有,所以矩形邊界檢測法對不規(guī)則的圖形來說,這只是一種不精確的檢測方法,如果你要精確檢測,那就要做更多的檢測了。當(dāng)然,矩形邊界檢測法對于大多數(shù)情況下已經(jīng)足夠了。

實例又來了(用iframe插入會導(dǎo)致頁面卡,所以放在單獨頁面中,點擊可看):http://ghmagical.com/Iframe/show/code/intersect

if(activeRect !== rect && tool.intersects(activeRect, rect)) {   

  activeRect.y = rect.y - activeRect.height;   

  activeRect = createRect();   

};

這個例子是不是有點像俄羅斯方塊呢,每一次只有一個活動物體,然后循環(huán)檢測它是否與已經(jīng)存在的物體碰撞,如果碰撞,則將活動物體放在與它碰撞物體的上面,然后創(chuàng)建一個新的方塊。

1.2 物體與點的碰撞檢測
在前面我們在 tool工具類中添加了一個工具函數(shù) tool.containsPoint,它接受三個參數(shù),第一個是矩形對象,后面兩個是一個點的x和y的坐標(biāo),返回值是true或false:

tool.containsPoint = function(body, x, y){   

  return !(x < body.x || x > (body.x + body.width)    

        || y < body.y || y > (body.y + body.height));  

};

其實,tool.containsPoint()函數(shù)就是在檢測點與矩形是否碰撞。


比如,要檢測點(50,50)是否在一個矩形內(nèi):

if(tool.containsPoint(body,50,50)){

  console.log('在矩形內(nèi)');

}


tool.intesects()和tool.containsPoint()方法都會遇到精確問題,對矩形最精確,越不規(guī)則,精確率就越小。大多數(shù)情況下,都會采取這兩種方法。當(dāng)然,如果你要對不規(guī)則圖形采取更精確的方法,那你就要寫更多的代碼去執(zhí)行精確的檢測了。


2、基于距離的碰撞檢測

距離就是指兩個物體間的距離,當(dāng)然,物體總是有高寬的,這就還要考慮高寬。一般我們會先確定兩個物體的最小距離,然后計算當(dāng)前距離,最后進(jìn)行比較,如果當(dāng)前距離比最小距離小,那肯定發(fā)生了碰撞。


這種距離檢測法,對圓來說是最精確的,而對于其他圖形,或多或少會有一些精確問題。


2.1 基于距離的簡單碰撞檢測

基于距離的碰撞檢測的最理想的情況是:有兩個正圓形要進(jìn)行碰撞檢測,從圓的中心點開始計算。


要檢測兩個圓是否碰撞,其實就是比較兩個圓的中心點的距離與兩個圓的半徑和的大小關(guān)系。

dx = ballB.x - ballA.x;

dy = ballB.y - ballA.y;


dist = Math.sqrt(dx * dx + dy * dy);


if(dist < ballA.radius + ballB.radius){

  console.log('碰撞了');

}


實例:


在上面的例子中,碰撞距離就是一個球的半徑加上另一個球的半徑,也是碰撞的最小距離,而兩者真正的距離就是圓心與圓心的距離。

var dx = ballB.x - ballA.x;   

var dy = ballB.y - ballA.y;   

var dist = Math.sqrt(dx * dx + dy * dy);   

if(ball != ballB && dist < ballA.radius + ballB.radius){   

  ctx.strokeStyle = 'red';   

  var txt = '你壓著我了';   

  var tx = ballA.x - ctx.measureText(txt).width / 2;   

  ctx.font = '30px Arial'   

  ctx.strokeText(txt,tx,ballA.y);   

};


2.2 彈性碰撞

就像2.1節(jié)里的例子一樣,當(dāng)兩個球碰撞時,我們加入了文字提示,當(dāng)然,我們還可以做更多操作,比如這節(jié)要講的彈性碰撞。


實例:

首先我們加入一個放在canvas中心的圓球ballA,然后加入多個隨機大小和隨機速度的圓球,讓它們做勻速運動,遇到墻就反彈,最后在每一幀使用基于距離的方法檢測小球是否與中央的圓球ballA發(fā)生了碰撞,如果發(fā)生了碰撞,則計算彈動目標(biāo)點和兩球間的最小距離來避免小球完全撞上圓球ballA。


對于小球和圓球ballA的碰撞,我們可以這樣理解,我們在ballA外設(shè)置了目標(biāo)點,然后讓小球向目標(biāo)點彈動,一旦小球到達(dá)目標(biāo)點,就不再繼續(xù)碰撞,彈性運動就結(jié)束了,繼續(xù)做勻速運動。


下面的效果就像一群小氣泡在大氣泡上反彈,小氣泡撞入大氣泡一點距離,這個距離取決于小氣泡的速度,然后被彈出來。



如果你看不懂它如何反彈的,那你就要回到上一章看看《緩動和彈動》是如何實現(xiàn)的了。


3、多物體的碰撞檢測策略

這一節(jié)并不會介紹新的碰撞檢測方法,而是介紹如何優(yōu)化多物體碰撞代碼。


如果你用過二維數(shù)組,那么你肯定知道如何去遍歷數(shù)組元素,通常的方法是使用兩個循環(huán)函數(shù),而多物體的碰撞檢測,也類似二維數(shù)組:

for(var i = 0; i < objects.length; i++){

  var objectA = objects[i];

  for(var j = 0; j < objects.length; j++){

    var objectB = objects[j];

    if(tool.intersects(objectA,objectB){}

  }

};

上面的方法的語法是沒錯的,不過這段代碼有兩個效率問題:

(1)多余的自身碰撞檢測

它檢測了同一個物體是否自身碰撞,比如:第一個物體(i=0)是objects[0],在第二次循環(huán)中,第一個物體(j=0)也是objects[0],是不是完全沒必要的檢測,我們可以這樣避免:

if(i != j && tool.intersects(objectA,objectB){}

這樣會節(jié)省了i次碰撞檢測


(2)重復(fù)碰撞檢測

第一次(i=0)循環(huán)時,我們檢測了objects[0](i=0)和objects[1](j=1)的碰撞;第二次(i=1)循環(huán)時,代碼似乎又檢測了objects[1](i=1)和objects[0](j=0)的碰撞,這豈不是多余的嗎?

我們應(yīng)該做如下的避免:

for(var i = 0; i < objects.length; i++){

  var objectA = objects[i];

  for(var j = i + 1; j < objects.length; j++){

    var objectB = objects[j];

    if(tool.intersects(objectA,objectB){}

  }

};

這樣處理后,不僅避免了自身碰撞檢測,而且減少了重復(fù)碰撞檢測。


實例:


在上面的例子中,兩個球在碰撞后的彈動代碼并沒有太大的區(qū)別,只不過這里將ballB當(dāng)成了中央位置的圓球而已:

function checkCollision(ballA, ballB) {   

  var dx = ballA.x - ballB.x;   

  var dy = ballA.y - ballB.y;   

  var dist = Math.sqrt(dx * dx + dy * dy);   

  var min_dist = ballB.radius + ballA.radius;   

  if(dist < min_dist) {   

    var angle = Math.atan2(dy, dx);   

    var tx = ballB.x + Math.cos(angle) * min_dist;   

    var ty = ballB.y + Math.sin(angle) * min_dist;   

    var ax = (tx - ballA.x) * spring * 0.5;   

    var ay = (ty - ballA.y) * spring * 0.5;   

    ballA.vx += ax;   

    ballA.vy += ay;   

    ballB.vx += (-ax);   

    ballB.vy += (-ay);   

  };   

};

上面代碼最后四行的意思是:不僅ballB要從ballA彈開,而且ballA要從ballB彈出,它們的加速度的絕對值是相同的,方向相反。


不知道你有沒有注意到,ax和ay的計算都乘以0.5,這是因為當(dāng)ballA移動ax時,ballB也反向移動ax,那么就造成了 ax 變成 2ax ,所以要乘以0.5,才是真正的加速度。當(dāng)然,你也可以將spring減小成原來的一半。


總結(jié)

碰撞檢測是很多動畫中必不可少的,你必須掌握基于幾何圖形的碰撞檢測、基于距離的碰撞檢測方法,以及如何更有效的的檢測多物體間的碰撞。


下一章:坐標(biāo)旋轉(zhuǎn)和斜面反彈


附錄

重要公式:

(1)矩形邊界碰撞檢測

tool.intersects = function(bodyA,bodyB){

  return !(bodyA.x + bodyA.width < bodyB.x ||

          bodyB.x + bodyB.width < bodyA.x ||

          bodyA.y + bodyA.height < bodyB.y ||

          bodyB.y + bodyB.height < bodyA.y);

};


(2)基于距離的碰撞檢測

dx = objectB.x - objectA.x;

dy = objectB.y - objectA.y;

dist = Math.sqrt(dx * dx + dy * dy);

if(dist < objectA.radius + objectB.radius){}


(3)多物體碰撞檢測

for(var i = 0; i < objects.length; i++){

  var objectA = objects[i];

  for(var j = i + 1; j < objects.length; j++){

    var objectB = objects[j];

    if(tool.intersects(objectA,objectB){}

  }

};


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號