Canvas游戏开发实战:从零实现高性能碰撞检测系统
在H5小游戏和互动可视化项目中,碰撞检测往往是性能瓶颈所在。许多开发者习惯直接引入物理引擎,但当项目只需要基础碰撞功能时,这些"重型武器"反而会成为负担。本文将带你从数学原理出发,构建一套轻量级碰撞检测体系,涵盖圆形、矩形到复杂多边形的精确检测,并分享我在实际项目中积累的优化技巧。
1. 碰撞检测基础:从圆形开始
圆形碰撞是最简单的检测模型,但其中蕴含的优化思想却贯穿整个碰撞系统。核心算法只需比较两圆圆心距离与半径之和:
function circleCollision(c1, c2) { const dx = c1.x - c2.x const dy = c1.y - c2.y const distanceSquared = dx*dx + dy*dy const radiusSum = c1.radius + c2.radius return distanceSquared <= radiusSum * radiusSum }关键优化点:
- 使用距离平方比较避免开方运算
- 对象池化减少GC压力
- 预计算半径平方值
提示:在60FPS的游戏中,即使每次检测节省0.01ms,1000次检测也能节省10ms的帧时间
实际项目中,我常用空间分区来优化大量圆形检测:
- 将画布划分为N×N网格
- 只检测同一网格或相邻网格中的对象
- 动态调整网格大小保持每个网格5-10个对象
2. 矩形碰撞的两种实现范式
2.1 AABB(轴对称包围盒)
AABB是各边与坐标轴平行的矩形,检测逻辑极其高效:
function aabbCollision(box1, box2) { return !( box1.right < box2.left || box1.left > box2.right || box1.bottom < box2.top || box1.top > box2.bottom ) }性能对比表:
| 检测类型 | 平均耗时(ms) | 适用场景 |
|---|---|---|
| AABB | 0.002 | 静态UI元素 |
| OBB | 0.012 | 旋转物体 |
2.2 OBB(定向包围盒)
对于旋转的矩形,需要采用分离轴定理(SAT)实现:
class OBB { constructor(center, width, height, rotation) { this.axes = [ {x: Math.cos(rotation), y: Math.sin(rotation)}, {x: -Math.sin(rotation), y: Math.cos(rotation)} ] this.extents = [width/2, height/2] this.center = center } getProjectionRadius(axis) { return this.extents[0] * Math.abs(this.axes[0].x*axis.x + this.axes[0].y*axis.y) + this.extents[1] * Math.abs(this.axes[1].x*axis.x + this.axes[1].y*axis.y) } } function obbCollision(a, b) { const axes = [...a.axes, ...b.axes] const offset = {x: b.center.x - a.center.x, y: b.center.y - a.center.y} for(let axis of axes) { const projA = a.getProjectionRadius(axis) const projB = b.getProjectionRadius(axis) const projOffset = Math.abs(offset.x*axis.x + offset.y*axis.y) if(projOffset > projA + projB) return false } return true }3. 复杂形状检测策略
3.1 胶囊体检测
胶囊体本质是圆柱体+半球帽的组合,检测时转化为线段到点的距离计算:
function capsulePointDistance(capsule, point) { const segmentVec = {x: capsule.x2 - capsule.x1, y: capsule.y2 - capsule.y1} const pointVec = {x: point.x - capsule.x1, y: point.y - capsule.y1} const t = Math.max(0, Math.min(1, (pointVec.x*segmentVec.x + pointVec.y*segmentVec.y) / (segmentVec.x*segmentVec.x + segmentVec.y*segmentVec.y) )) const projection = { x: capsule.x1 + t*segmentVec.x, y: capsule.y1 + t*segmentVec.y } const dx = point.x - projection.x const dy = point.y - projection.y return Math.sqrt(dx*dx + dy*dy) }3.2 扇形检测
扇形检测需要分三步判断:
- 圆心是否在扇形角范围内
- 圆心到扇形顶点的距离
- 圆心到两条边的距离
function sectorCircleCollision(sector, circle) { // 第一步:距离筛选 const dx = circle.x - sector.x const dy = circle.y - sector.y const distSq = dx*dx + dy*dy const maxDist = sector.radius + circle.radius if(distSq > maxDist*maxDist) return false // 第二步:角度检测 const angle = Math.atan2(dy, dx) const angleDiff = normalizeAngle(angle - sector.startAngle) if(angleDiff > sector.angle) return false // 第三步:边检测 if(distSq < circle.radius*circle.radius) return true const edge1 = getLineProjection(sector.x, sector.y, sector.startAngle) const edge2 = getLineProjection(sector.x, sector.y, sector.startAngle + sector.angle) return pointToLineDistance(...edge1, circle.x, circle.y) <= circle.radius || pointToLineDistance(...edge2, circle.x, circle.y) <= circle.radius }4. 性能优化实战技巧
4.1 空间分割优化
四叉树实现要点:
- 设置节点最大容量(通常4-8个对象)
- 动态分裂与合并节点
- 对象只存储在叶子节点
class Quadtree { constructor(bounds, capacity) { this.bounds = bounds // {x,y,width,height} this.capacity = capacity this.objects = [] this.nodes = [] } insert(obj) { if(!this.bounds.contains(obj)) return false if(this.objects.length < this.capacity || !this.nodes.length) { this.objects.push(obj) return true } this.nodes[0].insert(obj) || this.nodes[1].insert(obj) || this.nodes[2].insert(obj) || this.nodes[3].insert(obj) } query(range, found = []) { if(!this.bounds.intersects(range)) return found found.push(...this.objects.filter(obj => range.contains(obj))) for(let node of this.nodes) { node.query(range, found) } return found } }4.2 混合精度检测策略
我通常采用三级检测体系:
- 粗略检测:基于包围盒快速筛选
- 中等精度:简化几何形状检测
- 精确检测:完整几何计算
function hybridDetection(objA, objB) { // 第一级:AABB快速排除 if(!aabbCollision(objA.getAABB(), objB.getAABB())) return false // 第二级:距离粗略判断 const centerDist = distanceSq(objA.center, objB.center) if(centerDist > (objA.radius + objB.radius)**2) return false // 第三级:精确形状检测 return preciseCollision(objA, objB) }5. 调试与可视化技巧
在开发《太空防御》游戏时,我总结了一套实用的调试方法:
碰撞可视化方案:
function drawCollisionDebug(ctx) { ctx.strokeStyle = '#ff0000' gameObjects.forEach(obj => { if(obj.collider) { ctx.beginPath() if(obj.collider.type === 'circle') { ctx.arc(obj.x, obj.y, obj.collider.radius, 0, Math.PI*2) } else if(obj.collider.type === 'aabb') { ctx.rect(obj.x, obj.y, obj.width, obj.height) } ctx.stroke() } }) }性能监控面板:
class PerformanceMonitor { constructor() { this.collisionTimes = [] this.frameCount = 0 } recordCollision(time) { this.collisionTimes.push(time) if(this.collisionTimes.length > 100) { this.collisionTimes.shift() } } draw(ctx) { const avg = this.collisionTimes.reduce((a,b) => a+b, 0) / this.collisionTimes.length ctx.fillText(`碰撞检测: ${avg.toFixed(3)}ms`, 10, 20) } }在实现《弹球大师》的物理系统时,发现当超过200个物体时,基础检测会导致帧率骤降。通过引入四叉树和混合检测策略,最终在1000个物体时仍保持60FPS流畅运行。关键点在于根据物体运动速度动态调整检测频率——静态物体每3帧检测一次,高速物体每帧检测。