## 碰撞检测 碰撞检测是物体与物体之间的交互,其实在前面的边界检测也是一种碰撞检测,只不过检测的对象是物体与边界之间。在本章中,我们将介绍更多的碰撞检测,比如:两个物体间的碰撞检测、一个物体与一个点的碰撞检测、基于距离的碰撞检测等等碰撞检测方法。 **什么是碰撞检测呢?** 简单来说,碰撞检测就是判定两个物体是否在同一时间内占用一块空间,用数学的角度来看,就是两个物体有没有交集。 检测碰撞的方法有很多,一般我们使用如下两种: 从几何图形的角度来检测,就是判断一个物体是否与另一个有重叠,我们可以用物体的矩形边界来判断。 检测距离,就是判断两个物体是否足够近到发生碰撞,需要计算距离和判断两个物体是否足够近。 **1、基于几何图形的碰撞检测** 基于几何图形的碰撞检测,一般情况下是检查一个矩形是否与其他矩形相交,或者某一个坐标点是否落在矩形内。 **1.1 两个物体间的碰撞检测(矩形边界检测法)** 在上一章中,我们介绍了一个 getBound() 方法,参数为球对象,返回矩形对象。 ``` function getBound(body){ return { x: (body.x - body.radius), y: (body.y - body.radius), width: body.radius * 2, height: body.radius * 2 }; } ``` 现在我们已经知道如何获取物体的矩形边界,那么只需检测两个对象的边界框是否相交,就可以判断两个物体是否碰撞了。我们在 tool.js 工具类中添加一个工具函数 tool.intersects : ``` tool.intersects = function(bodyA,bodyB){ return !(bodyA.x + bodyA.width < bodyB.x || bodyB.x + bodyB.width < bodyA.x || bodyA.y + bodyA.height < bodyB.y || bodyB.y + bodyB.height < bodyA.y); }; ``` 这个函数传入两个矩形对象,如果返回true,表示两个矩形相交了;否则,返回false。(如果你看不明白这段代码,请看下图,让一个矩形分别位于另一个矩形的上下左右位置): ![](https://box.kancloud.cn/fbb5ac590132efc02e78399a8d96fdad_388x314.jpg) 检测函数已经知道了,当要检测两个物体是否相交时,就可以做如下判断: ``` if (tool.intersects(objectA,objectB)) { console.log('撞上了'); } ``` 注意:这里传入的必须是矩形对象。如果是球,可调用getBound()方法返回矩形对象。如果已经是矩形对象,就直接传入。 这里有一个需要注意的问题,有些时候,我们的物体是不规则的,如果我们采取矩形边界检测,有时候会不精确(只有真正的矩形才是精确的): ![](https://box.kancloud.cn/598a7c9e6e01e4a95419490a72412494_600x195.jpg) 在上面的图中,有矩形、圆形和五角形,我们都可以采取矩形边界检测法,不过,你会发现,当物体是不规则的形状时,虽然通过上面的 tool.intesects() 方法判断两个物体已经碰撞,但实际上并没有,所以矩形边界检测法对不规则的图形来说,这只是一种不精确的检测方法,如果你要精确检测,那就要做更多的检测了。当然,矩形边界检测法对于大多数情况下已经足够了。 实例又来了(用iframe插入会导致页面卡,所以放在单独页面中,点击可看):http://ghmagical.com/Iframe/show/code/intersect ``` if(activeRect !== rect && tool.intersects(activeRect, rect)) { activeRect.y = rect.y - activeRect.height; activeRect = createRect(); }; ``` 这个例子是不是有点像俄罗斯方块呢,每一次只有一个活动物体,然后循环检测它是否与已经存在的物体碰撞,如果碰撞,则将活动物体放在与它碰撞物体的上面,然后创建一个新的方块。 **1.2 物体与点的碰撞检测** 在前面我们在 tool工具类中添加了一个工具函数 tool.containsPoint,它接受三个参数,第一个是矩形对象,后面两个是一个点的x和y的坐标,返回值是true或false: ``` tool.containsPoint = function(body, x, y){ return !(x < body.x || x > (body.x + body.width) || y < body.y || y > (body.y + body.height)); }; ``` 其实,tool.containsPoint()函数就是在检测点与矩形是否碰撞。 比如,要检测点(50,50)是否在一个矩形内: ``` if(tool.containsPoint(body,50,50)){ console.log('在矩形内'); } ``` tool.intesects()和tool.containsPoint()方法都会遇到精确问题,对矩形最精确,越不规则,精确率就越小。大多数情况下,都会采取这两种方法。当然,如果你要对不规则图形采取更精确的方法,那你就要写更多的代码去执行精确的检测了。 **2、基于距离的碰撞检测** 距离就是指两个物体间的距离,当然,物体总是有高宽的,这就还要考虑高宽。一般我们会先确定两个物体的最小距离,然后计算当前距离,最后进行比较,如果当前距离比最小距离小,那肯定发生了碰撞。 这种距离检测法,对圆来说是最精确的,而对于其他图形,或多或少会有一些精确问题。 **2.1 基于距离的简单碰撞检测** 基于距离的碰撞检测的最理想的情况是:有两个正圆形要进行碰撞检测,从圆的中心点开始计算。 要检测两个圆是否碰撞,其实就是比较两个圆的中心点的距离与两个圆的半径和的大小关系。 ``` dx = ballB.x - ballA.x; dy = ballB.y - ballA.y; dist = Math.sqrt(dx * dx + dy * dy); if(dist < ballA.radius + ballB.radius){ console.log('碰撞了'); } ``` 实例:canvas-demo/distanceIntersect.html 在上面的例子中,碰撞距离就是一个球的半径加上另一个球的半径,也是碰撞的最小距离,而两者真正的距离就是圆心与圆心的距离。 ``` var dx = ballB.x - ballA.x; var dy = ballB.y - ballA.y; var dist = Math.sqrt(dx * dx + dy * dy); if(ball != ballB && dist < ballA.radius + ballB.radius){ ctx.strokeStyle = 'red'; var txt = '你压着我了'; var tx = ballA.x - ctx.measureText(txt).width / 2; ctx.font = '30px Arial' ctx.strokeText(txt,tx,ballA.y); }; ``` **2.2 弹性碰撞** 就像2.1节里的例子一样,当两个球碰撞时,我们加入了文字提示,当然,我们还可以做更多操作,比如这节要讲的弹性碰撞。 实例:canvas-demo/springIntersect.html 首先我们加入一个放在canvas中心的圆球ballA,然后加入多个随机大小和随机速度的圆球,让它们做匀速运动,遇到墙就反弹,最后在每一帧使用基于距离的方法检测小球是否与中央的圆球ballA发生了碰撞,如果发生了碰撞,则计算弹动目标点和两球间的最小距离来避免小球完全撞上圆球ballA。 对于小球和圆球ballA的碰撞,我们可以这样理解,我们在ballA外设置了目标点,然后让小球向目标点弹动,一旦小球到达目标点,就不再继续碰撞,弹性运动就结束了,继续做匀速运动。 下面的效果就像一群小气泡在大气泡上反弹,小气泡撞入大气泡一点距离,这个距离取决于小气泡的速度,然后被弹出来。 如果你看不懂它如何反弹的,那你就要回到上一章看看《缓动和弹动》是如何实现的了。 **3、多物体的碰撞检测策略** 这一节并不会介绍新的碰撞检测方法,而是介绍如何优化多物体碰撞代码。 如果你用过二维数组,那么你肯定知道如何去遍历数组元素,通常的方法是使用两个循环函数,而多物体的碰撞检测,也类似二维数组: ``` for(var i = 0; i < objects.length; i++){ var objectA = objects[i]; for(var j = 0; j < objects.length; j++){ var objectB = objects[j]; if(tool.intersects(objectA,objectB){} } }; ``` 上面的方法的语法是没错的,不过这段代码有两个效率问题: **(1)多余的自身碰撞检测** 它检测了同一个物体是否自身碰撞,比如:第一个物体(i=0)是objects[0],在第二次循环中,第一个物体(j=0)也是objects[0],是不是完全没必要的检测,我们可以这样避免: ``` if(i != j && tool.intersects(objectA,objectB){} ``` 这样会节省了i次碰撞检测 **(2)重复碰撞检测** 第一次(i=0)循环时,我们检测了objects[0](i=0)和objects[1](j=1)的碰撞;第二次(i=1)循环时,代码似乎又检测了objects[1](i=1)和objects[0](j=0)的碰撞,这岂不是多余的吗? 我们应该做如下的避免: ``` for(var i = 0; i < objects.length; i++){ var objectA = objects[i]; for(var j = i + 1; j < objects.length; j++){ var objectB = objects[j]; if(tool.intersects(objectA,objectB){} } }; ``` 这样处理后,不仅避免了自身碰撞检测,而且减少了重复碰撞检测。 实例:canvas-demo/collision.html 在上面的例子中,两个球在碰撞后的弹动代码并没有太大的区别,只不过这里将ballB当成了中央位置的圆球而已: ``` function checkCollision(ballA, ballB) { var dx = ballA.x - ballB.x; var dy = ballA.y - ballB.y; var dist = Math.sqrt(dx * dx + dy * dy); var min_dist = ballB.radius + ballA.radius; if(dist < min_dist) { var angle = Math.atan2(dy, dx); var tx = ballB.x + Math.cos(angle) * min_dist; var ty = ballB.y + Math.sin(angle) * min_dist; var ax = (tx - ballA.x) * spring * 0.5; var ay = (ty - ballA.y) * spring * 0.5; ballA.vx += ax; ballA.vy += ay; ballB.vx += (-ax); ballB.vy += (-ay); }; }; ``` 上面代码最后四行的意思是:不仅ballB要从ballA弹开,而且ballA要从ballB弹出,它们的加速度的绝对值是相同的,方向相反。 不知道你有没有注意到,ax和ay的计算都乘以0.5,这是因为当ballA移动ax时,ballB也反向移动ax,那么就造成了 ax 变成 2ax ,所以要乘以0.5,才是真正的加速度。当然,你也可以将spring减小成原来的一半。 **总结** 碰撞检测是很多动画中必不可少的,你必须掌握基于几何图形的碰撞检测、基于距离的碰撞检测方法,以及如何更有效的的检测多物体间的碰撞。 **附录** **重要公式:** (1)矩形边界碰撞检测 ``` tool.intersects = function(bodyA,bodyB){ return !(bodyA.x + bodyA.width < bodyB.x || bodyB.x + bodyB.width < bodyA.x || bodyA.y + bodyA.height < bodyB.y || bodyB.y + bodyB.height < bodyA.y); }; ``` (2)基于距离的碰撞检测 ``` dx = objectB.x - objectA.x; dy = objectB.y - objectA.y; dist = Math.sqrt(dx * dx + dy * dy); if(dist < objectA.radius + objectB.radius){} ``` (3)多物体碰撞检测 ``` for(var i = 0; i < objects.length; i++){ var objectA = objects[i]; for(var j = i + 1; j < objects.length; j++){ var objectB = objects[j]; if(tool.intersects(objectA,objectB){} } }; ```