在React项目里,当两个矩形元素可能发生重叠时,判断鼠标拖拽、地图标记、游戏角色是否接触,不能只依赖DOM的getBoundingClientRect,因为旋转后的元素包围区域不再是轴对齐矩形。物理碰撞检测的常用思路是先建立包围盒,再用数学公式判断重叠。这里重点讨论AABB和OBB两种包围盒,以及如何在React组件中落地。

碰撞检测的核心思路与包围盒选型
碰撞检测的本质是判断两个几何体是否共享任意空间点。直接对复杂图形做像素级检测代价过高,因此通常用包围盒近似物体轮廓。包围盒越大,判断越快,但误报率越高;包围盒越贴合,计算量越大,但结果越精确。在React这类前端框架中,我们往往需要同时兼顾交互实时性和代码可维护性,所以不会直接引入完整物理引擎,而是手动实现轻量级检测函数。
AABB全称Axis-Aligned Bounding Box,也就是轴对齐包围盒。它的四条边分别与坐标系的X轴、Y轴平行,只需要记录x、y、width、height四个属性即可描述。AABB的最大优势是相交判断非常快,只需要比较区间是否重叠。缺点是当物体发生旋转时,AABB必须覆盖旋转后的整个范围,会产生大量空白区域,导致两个明明没有接触的物体被误判为碰撞。
OBB全称Oriented Bounding Box,即有向包围盒。它比AABB多了一个rotation属性,表示矩形可以绕自身中心旋转,因此能够更紧密地贴合物体真实轮廓。OBB的代价是需要使用分离轴定理进行相交测试,计算量明显高于AABB。在实际项目中,合理做法是把两者组合起来:先用AABB做快速剔除,再对可能碰撞的对象使用OBB精确判断。
AABB包围盒算法实现与React集成
AABB的碰撞判断基于一个简单事实:两个区间重叠,当且仅当一个区间的最小值小于等于另一个区间的最大值,并且另一个区间的最小值小于等于当前区间的最大值。把它分别应用到X轴和Y轴两个方向上,就能得到完整的碰撞条件。如果任意一个轴上的区间不重叠,两个矩形就一定不相交。
下面是AABB检测函数的实现。函数接收两个矩形对象,每个对象都包含x、y、width、height四个字段。返回值为true表示发生碰撞,false表示未碰撞。
function aabbCollision(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;
}
在React组件中使用AABB检测时,可以把检测函数放在组件外部,避免每次渲染都重新创建函数。当用户拖拽元素或请求动画帧更新位置时,调用检测函数并把结果写入useState。如果在渲染过程中直接进行大量检测,可能会阻塞主线程,因此更推荐在事件处理器或requestAnimationFrame回调中执行。下面的示例展示了如何在组件中维护两个矩形的位置和碰撞状态。
import React, { useRef, useState } from 'react';
const initialBoxes = [
{ id: 'a', x: 140, y: 100, width: 130, height: 80, rotation: 0 },
{ id: 'b', x: 230, y: 150, width: 110, height: 90, rotation: 0 }
];
export default function AABBPanel() {
const [boxes, setBoxes] = useState(initialBoxes);
const [hitText, setHitText] = useState('未碰撞');
const canvasRef = useRef(null);
function handleCheck() {
const result = aabbCollision(boxes[0], boxes[1]);
setHitText(result ? '发生碰撞' : '未碰撞');
}
return (
<div className="collision-panel">
<canvas ref={canvasRef} width={600} height={400} />
<button onClick={handleCheck}>检测碰撞</button>
<p>{hitText}</p>
</div>
);
}
AABB虽然简单,但在很多场景下已经足够。例如拖拽排序中的占位矩形、非旋转的仪表盘组件、以及只需要快速过滤大量对象的第一阶段碰撞检测。它的时间复杂度是常数级别,即使同时检测数百个对象,对现代浏览器也不会造成明显压力。
OBB包围盒与分离轴定理
当矩形开始旋转后,AABB的空白区域问题就会被放大。例如一个细长的横条旋转到接近垂直角度时,它的轴对齐包围盒几乎变成了一个正方形,面积远大于实际形状。此时继续使用AABB会导致大量误判。OBB通过保留旋转角度来解决这个问题,但它需要更复杂的数学工具:分离轴定理。
分离轴定理指出,如果两个凸多边形不相交,那么一定存在一条轴,使得两个多边形在该轴上的投影区间不重叠。对于矩形来说,只需要检查四条轴即可:每个矩形自身的两条边方向向量。如果一个矩形已经旋转,我们需要先根据中心点、宽高和旋转角度计算出四个角点,再把角点投影到候选轴上。只要四条轴中任意一条轴上的投影区间不重叠,就可以立即判定两个OBB没有碰撞。
下面是OBB碰撞检测的完整实现。getCorners负责计算旋转后的四个顶点坐标,projectOntoAxis负责将顶点投影到指定轴并返回最小值和最大值,obbCollision则枚举所有候选轴并判断投影区间是否重叠。
function getCorners(box) {
const cx = box.x;
const cy = box.y;
const hw = box.width / 2;
const hh = box.height / 2;
const cos = Math.cos(box.rotation);
const sin = Math.sin(box.rotation);
return [
{ x: cx + hw * cos - hh * sin, y: cy + hw * sin + hh * cos },
{ x: cx + hw * cos + hh * sin, y: cy + hw * sin - hh * cos },
{ x: cx - hw * cos + hh * sin, y: cy - hw * sin - hh * cos },
{ x: cx - hw * cos - hh * sin, y: cy - hw * sin + hh * cos }
];
}
function projectOntoAxis(corners, axis) {
let min = Infinity;
let max = -Infinity;
for (const corner of corners) {
const projection = corner.x * axis.x + corner.y * axis.y;
if (projection < min) min = projection;
if (projection > max) max = projection;
}
return { min, max };
}
function isOverlap(a, b) {
return a.min <= b.max && b.min <= a.max;
}
function obbCollision(boxA, boxB) {
const cornersA = getCorners(boxA);
const cornersB = getCorners(boxB);
const angleA = boxA.rotation;
const angleB = boxB.rotation;
const axes = [
{ x: Math.cos(angleA), y: Math.sin(angleA) },
{ x: -Math.sin(angleA), y: Math.cos(angleA) },
{ x: Math.cos(angleB), y: Math.sin(angleB) },
{ x: -Math.sin(angleB), y: Math.cos(angleB) }
];
for (const axis of axes) {
const projectionA = projectOntoAxis(cornersA, axis);
const projectionB = projectOntoAxis(cornersB, axis);
if (!isOverlap(projectionA, projectionB)) {
return false;
}
}
return true;
}
OBB检测的精度更高,但每次调用都需要计算角点坐标、进行多次向量投影,计算量大约是AABB的几倍。在React应用中,如果对象数量不多,可以全部使用OBB;如果对象数量较大,则需要引入分层策略。
在React中组合使用AABB与OBB的优化策略
把AABB和OBB组合起来并不复杂。检测流程可以概括为两步:首先为每个旋转物体生成一个轴对齐包围盒,用AABB快速判断是否可能碰撞;只有当AABB重叠时,才进入OBB精确检测。这种方式能在大量对象场景下显著减少不必要的OBB计算。即使有几百个对象,大部分对象对在第一阶段就会被排除掉。
在React组件中,可以把这套逻辑封装到一个自定义Hook或者一个独立的检测工具模块中。核心原则是不要在render函数的JSX中直接执行OBB检测,因为render可能被频繁调用,任何重计算都会直接影响界面响应速度。更合适的做法是使用useMemo缓存物体的包围盒数据,并在事件回调或动画帧循环中执行检测,最后只把碰撞结果写入状态。
下面是一个组合检测的函数示例。它先把OBB转换为覆盖其旋转范围的最小AABB,再执行AABB粗筛。如果粗筛通过,才调用obbCollision做精确判断。
function getAABBFromOBB(box) {
const hw = box.width / 2;
const hh = box.height / 2;
const cos = Math.abs(Math.cos(box.rotation));
const sin = Math.abs(Math.sin(box.rotation));
const projectedWidth = hw * cos + hh * sin;
const projectedHeight = hw * sin + hh * cos;
return {
x: box.x - projectedWidth,
y: box.y - projectedHeight,
width: projectedWidth * 2,
height: projectedHeight * 2
};
}
function combinedCollision(boxA, boxB) {
const aabbA = getAABBFromOBB(boxA);
const aabbB = getAABBFromOBB(boxB);
if (!aabbCollision(aabbA, aabbB)) {
return false;
}
return obbCollision(boxA, boxB);
}
这种分层检测方案在React交互式编辑器、在线白板、轻量级游戏等场景中非常实用。它既保留了数学判断的精确性,又避免了每一次状态更新都执行高成本计算。你可以根据项目需要把检测结果绑定到className、样式或警告提示上,而检测逻辑本身与React渲染解耦,后续也方便替换为Web Worker或WebAssembly优化版本。
最后需要提醒的是,AABB和OBB都是近似检测。对于不规则形状或需要像素级精度的业务,仍然需要使用更复杂的碰撞算法,例如凸多边形裁剪、GJK算法或基于Canvas像素数据的手工检测。不过对于绝大多数基于矩形元素的React应用来说,AABB负责快速过滤,OBB负责旋转对象的精确判断,已经能够在性能与准确性之间取得很好的平衡。