## 碰撞檢測
碰撞檢測是物體與物體之間的交互,其實在前面的邊界檢測也是一種碰撞檢測,只不過檢測的對象是物體與邊界之間。在本章中,我們將介紹更多的碰撞檢測,比如:兩個物體間的碰撞檢測、一個物體與一個點的碰撞檢測、基于距離的碰撞檢測等等碰撞檢測方法。
**什么是碰撞檢測呢?**
簡單來說,碰撞檢測就是判定兩個物體是否在同一時間內占用一塊空間,用數學的角度來看,就是兩個物體有沒有交集。
檢測碰撞的方法有很多,一般我們使用如下兩種:
從幾何圖形的角度來檢測,就是判斷一個物體是否與另一個有重疊,我們可以用物體的矩形邊界來判斷。
檢測距離,就是判斷兩個物體是否足夠近到發生碰撞,需要計算距離和判斷兩個物體是否足夠近。
**1、基于幾何圖形的碰撞檢測**
基于幾何圖形的碰撞檢測,一般情況下是檢查一個矩形是否與其他矩形相交,或者某一個坐標點是否落在矩形內。
**1.1 兩個物體間的碰撞檢測(矩形邊界檢測法)**
在上一章中,我們介紹了一個 getBound() 方法,參數為球對象,返回矩形對象。
```
function getBound(body){
return {
x: (body.x - body.radius),
y: (body.y - body.radius),
width: body.radius * 2,
height: body.radius * 2
};
}
```
現在我們已經知道如何獲取物體的矩形邊界,那么只需檢測兩個對象的邊界框是否相交,就可以判斷兩個物體是否碰撞了。我們在 tool.js 工具類中添加一個工具函數 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);
};
```
這個函數傳入兩個矩形對象,如果返回true,表示兩個矩形相交了;否則,返回false。(如果你看不明白這段代碼,請看下圖,讓一個矩形分別位于另一個矩形的上下左右位置):

檢測函數已經知道了,當要檢測兩個物體是否相交時,就可以做如下判斷:
```
if (tool.intersects(objectA,objectB)) {
console.log('撞上了');
}
```
注意:這里傳入的必須是矩形對象。如果是球,可調用getBound()方法返回矩形對象。如果已經是矩形對象,就直接傳入。
這里有一個需要注意的問題,有些時候,我們的物體是不規則的,如果我們采取矩形邊界檢測,有時候會不精確(只有真正的矩形才是精確的):

在上面的圖中,有矩形、圓形和五角形,我們都可以采取矩形邊界檢測法,不過,你會發現,當物體是不規則的形狀時,雖然通過上面的 tool.intesects() 方法判斷兩個物體已經碰撞,但實際上并沒有,所以矩形邊界檢測法對不規則的圖形來說,這只是一種不精確的檢測方法,如果你要精確檢測,那就要做更多的檢測了。當然,矩形邊界檢測法對于大多數情況下已經足夠了。
實例又來了(用iframe插入會導致頁面卡,所以放在單獨頁面中,點擊可看):http://ghmagical.com/Iframe/show/code/intersect
```
if(activeRect !== rect && tool.intersects(activeRect, rect)) {
activeRect.y = rect.y - activeRect.height;
activeRect = createRect();
};
```
這個例子是不是有點像俄羅斯方塊呢,每一次只有一個活動物體,然后循環檢測它是否與已經存在的物體碰撞,如果碰撞,則將活動物體放在與它碰撞物體的上面,然后創建一個新的方塊。
**1.2 物體與點的碰撞檢測**
在前面我們在 tool工具類中添加了一個工具函數 tool.containsPoint,它接受三個參數,第一個是矩形對象,后面兩個是一個點的x和y的坐標,返回值是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()函數就是在檢測點與矩形是否碰撞。
比如,要檢測點(50,50)是否在一個矩形內:
```
if(tool.containsPoint(body,50,50)){
console.log('在矩形內');
}
```
tool.intesects()和tool.containsPoint()方法都會遇到精確問題,對矩形最精確,越不規則,精確率就越小。大多數情況下,都會采取這兩種方法。當然,如果你要對不規則圖形采取更精確的方法,那你就要寫更多的代碼去執行精確的檢測了。
**2、基于距離的碰撞檢測**
距離就是指兩個物體間的距離,當然,物體總是有高寬的,這就還要考慮高寬。一般我們會先確定兩個物體的最小距離,然后計算當前距離,最后進行比較,如果當前距離比最小距離小,那肯定發生了碰撞。
這種距離檢測法,對圓來說是最精確的,而對于其他圖形,或多或少會有一些精確問題。
**2.1 基于距離的簡單碰撞檢測**
基于距離的碰撞檢測的最理想的情況是:有兩個正圓形要進行碰撞檢測,從圓的中心點開始計算。
要檢測兩個圓是否碰撞,其實就是比較兩個圓的中心點的距離與兩個圓的半徑和的大小關系。
```
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('碰撞了');
}
```
實例:canvas-demo/distanceIntersect.html
在上面的例子中,碰撞距離就是一個球的半徑加上另一個球的半徑,也是碰撞的最小距離,而兩者真正的距離就是圓心與圓心的距離。
```
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節里的例子一樣,當兩個球碰撞時,我們加入了文字提示,當然,我們還可以做更多操作,比如這節要講的彈性碰撞。
實例:canvas-demo/springIntersect.html
首先我們加入一個放在canvas中心的圓球ballA,然后加入多個隨機大小和隨機速度的圓球,讓它們做勻速運動,遇到墻就反彈,最后在每一幀使用基于距離的方法檢測小球是否與中央的圓球ballA發生了碰撞,如果發生了碰撞,則計算彈動目標點和兩球間的最小距離來避免小球完全撞上圓球ballA。
對于小球和圓球ballA的碰撞,我們可以這樣理解,我們在ballA外設置了目標點,然后讓小球向目標點彈動,一旦小球到達目標點,就不再繼續碰撞,彈性運動就結束了,繼續做勻速運動。
下面的效果就像一群小氣泡在大氣泡上反彈,小氣泡撞入大氣泡一點距離,這個距離取決于小氣泡的速度,然后被彈出來。
如果你看不懂它如何反彈的,那你就要回到上一章看看《緩動和彈動》是如何實現的了。
**3、多物體的碰撞檢測策略**
這一節并不會介紹新的碰撞檢測方法,而是介紹如何優化多物體碰撞代碼。
如果你用過二維數組,那么你肯定知道如何去遍歷數組元素,通常的方法是使用兩個循環函數,而多物體的碰撞檢測,也類似二維數組:
```
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],在第二次循環中,第一個物體(j=0)也是objects[0],是不是完全沒必要的檢測,我們可以這樣避免:
```
if(i != j && tool.intersects(objectA,objectB){}
```
這樣會節省了i次碰撞檢測
**(2)重復碰撞檢測**
第一次(i=0)循環時,我們檢測了objects[0](i=0)和objects[1](j=1)的碰撞;第二次(i=1)循環時,代碼似乎又檢測了objects[1](i=1)和objects[0](j=0)的碰撞,這豈不是多余的嗎?
我們應該做如下的避免:
```
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){}
}
};
```
這樣處理后,不僅避免了自身碰撞檢測,而且減少了重復碰撞檢測。
實例:canvas-demo/collision.html
在上面的例子中,兩個球在碰撞后的彈動代碼并沒有太大的區別,只不過這里將ballB當成了中央位置的圓球而已:
```
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,這是因為當ballA移動ax時,ballB也反向移動ax,那么就造成了 ax 變成 2ax ,所以要乘以0.5,才是真正的加速度。當然,你也可以將spring減小成原來的一半。
**總結**
碰撞檢測是很多動畫中必不可少的,你必須掌握基于幾何圖形的碰撞檢測、基于距離的碰撞檢測方法,以及如何更有效的的檢測多物體間的碰撞。
**附錄**
**重要公式:**
(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){}
}
};
```