本文详解HTML5游戏中的像素级碰撞检测算法。该算法通过对比精灵重叠区域的RGBA透明度数据实现高精度判定,虽精确但效率较低。文中介绍了先利用AABB包围盒进行初步筛选以优化性能的策略,并分步解析了利用后台缓冲区获取像素数据、对比Alpha值判断相交的具体实现流程及注意事项。
像素碰撞检测算法概述
本文详细介绍一种在JS/HTML5游戏开发中常用的碰撞检测技术——像素检测算法,旨在为开发者提供参考与实现思路。
像素碰撞检测法能提供极高的精度,但其显著缺点在于执行效率较低。除非游戏场景对碰撞判定有极严苛的精度要求,否则在追求运行效率的HTML5游戏中,通常不建议采用此算法。
AABB包围盒与像素检测的协同策略
在实际应用中,像素检测前通常会先进行AABB(轴对齐包围盒)矩形检测。若AABB检测判定无碰撞,则确认为无碰撞;若判定有碰撞,则需进一步进行像素检测以确认。如图所示,尽管两个精灵的包围盒发生重叠,但精灵本体并未接触。这种情况下,除非使用与精灵形状高度贴合的多边形包围盒(且仅适用于凸多边形),否则难以获得理想效果。

像素检测原理与实现步骤
为了实现精确检测,必须采用像素检测算法。其核心原理在于:所有精灵均由像素点构成,而Canvas中的像素数据由RGBA四个通道值组成。若某点的Alpha(透明度)值为0,则该点为透明像素,如图6-19中精灵空白区域的Alpha值即为0。当两个精灵发生碰撞时,意味着它们在像素坐标上存在重叠,即两个精灵的像素点坐标相同,如下图所示。

基于上述原理,检测流程大致可分为以下步骤:
(1)选定需要检测的两个精灵对象。
(2)首先判断两个精灵是否发生包围盒碰撞。若未碰撞,则直接退出检测;若已碰撞,则计算并获取两个矩形的相交区域,继续后续步骤。
(3)将其中一个精灵绘制到一个与游戏屏幕尺寸相同的空白后台缓冲区中,并提取该缓冲区中相交区域的像素数据。
(4)清空后台缓冲区,为下一步操作做准备。
(5)对另一个精灵执行与步骤3相同的操作,将其绘制到后台缓冲区并获取相交区域的像素数据。
(6)在获得两个精灵在同一相交矩形区域内的像素数据后,遍历比较每一个像素点。若两个精灵在同一位置的透明度(Alpha值)均不为0,则判定为两个精灵发生相交,此时退出循环并返回真值。
需特别注意,在步骤3中获取相交区域像素数据时,必须在后台另开辟一个与游戏屏幕等大的空白区域进行绘制,而不能直接获取游戏画面中已重叠的相交区域。这是因为两个精灵在相交区域内的像素数据已经发生了视觉上的重叠(基于包围盒),直接读取会导致数据混乱。
从上述算法逻辑可见,像素检测需要额外的后台缓冲区支持:需绘制精灵、清空缓冲区,最后还需通过for循环逐像素检测。若相交区域大小为100×100像素,最坏情况下需循环10,000次,由此可见该算法并非节省时间的方案。
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta charset="UTF-8">
<title>盒包围碰撞算法-像素检测算法</title>
<style>
#stage {
border: 1px solid lightgray;
}
</style>
</head>
<body>
<h1>包围盒是否碰撞:<span class="hitTestBox">否</span></h1>
<h1>像素检测是否碰撞:<span class="hitTestPixel">否</span></h1>
<canvas id="stage"></canvas>
<img src="./images/penguin.png" alt="penguinImg" id="penguinImg" style="display:none" />
<img src="./images/giraffe.png" alt="giraffeImg" id="giraffeImg" style="display:none" />
</body>
<script>
window.onload = function () {
var stage = document.querySelector('#stage'),
ctx = stage.getContext('2d');
stage.width = 600;
stage.height = 600;
//创建后台canvas
var bC = document.createElement("canvas");
bC.width = stage.width;
bC.height = stage.height;
var backBuf = bC.getContext("2d");
var penguin = document.querySelector('#penguinImg'),giraffe = document.querySelector('#giraffeImg');
var penguinImg = {
x:stage.width/2,
y:stage.height/2,
r:128,
data:null
},giraffeImg = {
x:100,
y:100,
r:128,
data:null
};
function drawImageBox(img,x,y,width,height){
ctx.beginPath();
ctx.rect(x,y,width,height);
ctx.stroke();
ctx.drawImage(img,x,y,width,height);
}
//缓存画布绘制方法
function drawImageBC(img,x,y,width,height){
backBuf.clearRect(0,0,stage.width,stage.height);
backBuf.save();
backBuf.drawImage(img,x,y,width,height);
backBuf.restore();
}
//获取两个矩形相交区域
function getInRect(x1,y1,x2,y2,x3,y3,x4,y4) {
return [Math.max(x1,x3),Math.max(y1,y3),Math.min(x2,x4),Math.min(y2,y4)];
}
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
//根据地图数组碰撞将测
switch (e.keyCode) {
case 37:
console.log("Left");
if (penguinImg.x > 0) {
penguinImg.x -= 2;
}
break;
case 38:
console.log("Top");
if (penguinImg.y > 0) {
penguinImg.y -= 2;
}
break;
case 39:
console.log("Right");
if (penguinImg.x < stage.width) {
penguinImg.x += 2;
}
break;
case 40:
console.log("Bottom");
if (penguinImg.y < stage.height) {
penguinImg.y += 2;
}
break;
default:
return false;
}
};
stage.addEventListener('click', function (event) {
var x = event.clientX - stage.getBoundingClientRect().left;
var y = event.clientY - stage.getBoundingClientRect().top;
penguinImg.x = x;
penguinImg.y = y;
});
function update() {
ctx.clearRect(0, 0, 600, 600);
drawImageBox(giraffe,giraffeImg.x,giraffeImg.y,giraffeImg.r,giraffeImg.r);
drawImageBox(penguin,penguinImg.x,penguinImg.y,penguinImg.r,penguinImg.r);
document.querySelector('.hitTestBox').innerHTML = "否";
document.querySelector('.hitTestPixel').innerHTML = "否";
var rect = getInRect(giraffeImg.x,giraffeImg.y,giraffeImg.x+giraffeImg.r,giraffeImg.y+giraffeImg.r,penguinImg.x,penguinImg.y,penguinImg.x+penguinImg.r,penguinImg.y+penguinImg.r)
//如果没有相交则退出
if(rect[0]>=rect[2]||rect[1]>=rect[3]) {
} else{
document.querySelector('.hitTestBox').innerHTML = "是";
giraffeImg.data = null;
penguinImg.data = null;
//获取精灵在相交矩形像素数据
drawImageBC(giraffe,giraffeImg.x,giraffeImg.y,giraffeImg.r,giraffeImg.r);
giraffeImg.data = backBuf.getImageData(rect[0],rect[1],rect[2],rect[3]).data;
drawImageBC(penguin,penguinImg.x,penguinImg.y,penguinImg.r,penguinImg.r);
penguinImg.data = backBuf.getImageData(rect[0],rect[1],rect[2],rect[3]).data;
for(var i=3;i<giraffeImg.data.length;i+=4)
{
if(giraffeImg.data[i]>0&&penguinImg.data[i]>0){
document.querySelector('.hitTestPixel').innerHTML = "是";
}
}
}
requestAnimationFrame(update);
}
update();
};
</script>
</html>
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试运行上述代码,观察运行效果。
github地址:https://github.com/krapnikkk/JS-gameMathematics
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》
希望本文所述对大家JavaScript程序设计有所帮助。