在Canvas游戏或者DOM动画交互中,碰撞检测是最基础也最容易出问题的环节。不少项目上线前测试正常,到了真实设备或者复杂场景下就出现“穿模”“漏判”的现象。要彻底解决这类问题,不能只靠调大判定范围,而要从坐标系统、包围盒模型、运动离散化三个层面逐一拆解。

坐标系与包围盒模型理解偏差
最常见的失效原因是把元素的渲染坐标和逻辑坐标混为一谈。在JavaScript里,无论是DOM的getBoundingClientRect还是Canvas的绘制参数,默认都以左上角为原点。但很多开发者写碰撞函数时,习惯把对象的x和y当成中心点,于是计算两个矩形是否相交时就产生了整体偏移。比如一个方块画在(100,100),宽高50,若逻辑里认为这是中心,实际判定框就会跑到(75,75)到(125,125),和视觉位置差了四分之一,自然容易漏检。
另一个容易被忽视的点是旋转后的包围盒。使用轴对齐包围盒(AABB)做检测时,只要元素没有旋转,用最小最大坐标比较即可。可一旦用ctx.rotate转了画布,视觉上的方块变成了菱形,但代码仍用未旋转的矩形去比,就会在边角处失效。此时应该改用多边形碰撞或者计算旋转后的外接AABB。下面是一段纠正坐标模型的AABB检测代码:
// 假设obj.x和obj.y是左上角坐标,不是中心
function isAABBCollide(a, b) {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
// 若之前误把x,y当中心,先转换
function toTopLeft(obj) {
return {
x: obj.x - obj.width / 2,
y: obj.y - obj.height / 2,
width: obj.width,
height: obj.height
};
}
上面的写法明确区分了坐标含义,调用前先把中心坐标转成左上角,就能消除大部分静态物体的误判。如果项目里物体会旋转,建议直接引入分离轴定理(SAT)处理多边形,而不是勉强用矩形近似。
帧率波动导致的高速穿透
当物体移动速度较快、而动画帧率不稳定时,单帧位移可能超过对方厚度。举例来说,一个子弹每帧移动20像素,墙只有10像素宽,若只在每帧结束时做一次点或不重叠判断,子弹就会从墙的一侧直接跳到另一侧,中间没有任何一帧显示交叠,这就是经典的隧穿效应。很多初学者以为调小移动步长就行,但帧率掉到30以下时,步长再小也扛不住时间片拉长。
精准修复的思路是做连续碰撞检测(CCD),也就是把这一帧的运动轨迹当成线段,判断线段是否和障碍物相交。对于轴对齐的薄矩形,可以用扫掠AABB算法:比较上一帧和这一帧的包围盒,取并集后再和障碍比。下面给出简化的扫掠示例:
// prev和now是物体两帧的左上角坐标与尺寸
function sweptAABB(prev, now, box) {
var minX = Math.min(prev.x, now.x);
var maxX = Math.max(prev.x + prev.width, now.x + now.width);
var minY = Math.min(prev.y, now.y);
var maxY = Math.max(prev.y + prev.height, now.y + now.height);
var sweep = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
return isAABBCollide(sweep, box);
}
这种方式把“瞬间位置”扩展成了“本帧经过的区域”,即便帧率骤降也不会漏掉薄墙。在复杂场景里,还可以配合固定时间步长更新逻辑,把渲染和物理分开,进一步压制穿透概率。
事件驱动与检测时机错位
除了数学层面的问题,很多DOM层面的碰撞失效来自检测时机不对。有人喜欢在mousemove里做元素拖拽碰撞,却只在鼠标事件触发时算一次,而动画物体还在用requestAnimationFrame自己动。两边时钟不同步,就会出现拖拽时明明压上去了却没反应,因为检测函数没在动画帧里跑。
正确的做法是将碰撞检测统一放进主循环,或者至少在被拖拽元素和动态元素都更新位置之后调用。如果是用DOM的<div>做碰撞,记得getBoundingClientRect返回的是视口坐标,若页面有滚动需要加上scrollX、scrollY换算成文档坐标再比。下面展示一个主循环内统一检测的结构:
function loop() {
updatePositions(); // 更新所有物体坐标
var rects = items.map(function(el) {
var r = el.getBoundingClientRect();
return { x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height };
});
for (var i = 0; i < rects.length; i++) {
for (var j = i + 1; j < rects.length; j++) {
if (isAABBCollide(rects[i], rects[j])) {
handleHit(i, j);
}
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
把检测收敛到同一时刻,并处理好坐标空间,就能解决绝大多数“事件里看着没碰、循环里才算准”的诡异现象。综合前面三块内容,写好JavaScript碰撞检测的关键就是:坐标语义统一、运动做区间扫描、检测时机集中。
JavaScript碰撞检测边界计算修改时间:2026-08-13 23:54:33