用 canvas 做小游戲或者特效,碰撞檢測是少不了的。本文將會涉及普通的碰撞檢測,以及像素級的碰撞檢測。(本文的碰撞檢測均以矩形為例)
普通碰撞檢測#
普通的矩形碰撞檢測比較簡單。即已知兩個矩形的各頂點坐標,判斷是否相交,如相交,則為碰撞。
leetcode 有道題是給出兩個矩形的坐標,求其相交面積(223. Rectangle Area),代碼 可以直接拿過來用,如果面積大於 0,則為碰撞。
如果只需判斷是否相交或者相交面積,非常簡單,可以參考 這里。
為了程序的可擴展性,如果碰撞,最好還能求得相交矩形的坐標信息(為像素級碰撞檢測作准備),完善后的檢測代碼如下:
// 矩形一 top-left 坐標 (A, B), C 為 width, D 為 height
// 矩形二 同上
// 如果沒有相交,返回 [0, 0, 0, 0]
// 如果相交,假設相交矩形對角坐標 (x0, y0) (x1, y1) -- x1 > x0 & y1 > y0
// return [x0, y0, x1, y1]
function check(A, B, C, D, E, F, G, H) {
// 轉為對角線坐標
C += A, D += B, G += E, H += F;
// 沒有相交
if (C <= E || G <= A || D <= F || H <= B)
return [0, 0, 0, 0];
var tmpX, tmpY;
if (E > A) {
tmpX = G < C ? [E, G] : [E, C];
} else {
tmpX = C < G ? [A, C] : [A, G];
}
if (F > B) {
tmpY = H < D ? [F, H] : [F, D];
} else {
tmpY = D < H ? [B, D] : [B, H];
}
return [tmpX[0], tmpY[0], tmpX[1], tmpY[1]];
}
// 相交矩形坐標信息
var rect = check(fish.pos.x, fish.pos.y, fish.size.x, fish.size.y,
cat.pos.x, cat.pos.y, cat.size.x, cat.size.y);
// 相交面積大於 0 即為碰撞
var isHit = (rect[2] - rect[0]) * (rect[3] - rect[1]) > 0;
像素級碰撞檢測#
為什么要有像素級檢測?一圖以蔽之。
一般游戲或者動畫中的精靈都是矩形,僅僅判斷矩形相交是不准確的,比如上圖中,圖片所在矩形已經相交,但是精靈其實並沒有碰撞,所以我們需要進行像素級別的碰撞檢測。
方法一:
同時檢測兩圖在相交矩形內的像素,若存在一點在兩個圖上的 alpha 值不為 0,則發生碰撞。
因為還要對原始的圖像(fish 圖和 cat 圖)分別提取像素點(進行判斷),所以需要一個離屏的 canvas 。這里用了 canvas 的 getImageData
方法提取像素點 rgba 信息。
// a, b 為精靈對象
// a, b 分別擁有鍵值 img(精靈圖像 DOM元素), pos(精靈瞬間位置 top-left 坐標), size(wdith, height 數據)
// rect 參數為 check() 函數返回值
function checkInDetail(a, b, rect) {
// 離屏 canvas
var canvas = document.createElement('canvas');
_ctx = canvas.getContext('2d');
_ctx.drawImage(a.img, 0, 0, a.size.x, a.size.y);
// 相對位置
var data1 = _ctx.getImageData(rect[0] - a.pos.x, rect[1] - a.pos.y, rect[2] - rect[0], rect[3] - rect[1]).data;
_ctx.clearRect(0, 0, b.size.x, b.size.y);
_ctx.drawImage(b.img, 0, 0, b.size.x, b.size.y);
var data2 = _ctx.getImageData(rect[0] - b.pos.x, rect[1] - b.pos.y, rect[2] - rect[0], rect[3] - rect[1]).data;
canvas = null;
for(var i = 3; i < data1.length; i += 4) {
if(data1[i] > 0 && data2[i] > 0)
return true; // 碰撞
}
return false;
}
// 精靈對象實例
var fish = {
img: document.getElementById('fish')
, pos: new Vector2()
, size: new Vector2()
// ...
};
方法二:
先畫一張圖,然后將混合模式改為 source-in,這時再畫圖,新圖片會僅僅出現與原有內容重疊的地方,其他地方透明度變為 0,這時就可以通過判斷是否所有像素都透明來判斷碰撞了。
// a, b 為精靈對象
// a, b 分別擁有鍵值 img(精靈圖像 DOM元素), pos(精靈瞬間位置 top-left 坐標), size(wdith, height 數據)
// rect 參數為 check() 函數返回值
function _checkInDetail(a, b, rect) {
// 離屏 canvas
var canvas = document.createElement('canvas');
_ctx = canvas.getContext('2d');
// 將 (0, 0) 作為基准點,將 a 放入 (0, 0) 位置
_ctx.drawImage(a.img, 0, 0, a.size.x, a.size.y);
_ctx.globalCompositeOperation = 'source-in';
_ctx.drawImage(b.img, b.pos.x - a.pos.x, b.pos.y - a.pos.y, b.size.x, b.size.y);
var data = _ctx.getImageData(rect[0] - a.pos.x, rect[1] - a.pos.y, rect[2] - rect[0], rect[3] - rect[1]).data;
canvas = null;
// 改回來(雖然並沒有什么卵用)
_ctx.globalCompositeOperation = 'source-over';
for(var i = 3; i < data.length; i += 4) {
if (data[i])
return true; // 碰撞
}
return false;
}
我測試了幾次,把相交的像素點都取了出來求得相交像素點總數,兩種方法有時會相差一兩個像素點。對於像素級碰撞檢測來說,兩種方法任取其一就可。