## 碰撞检测
碰撞检测是物体与物体之间的交互,其实在前面的边界检测也是一种碰撞检测,只不过检测的对象是物体与边界之间。在本章中,我们将介绍更多的碰撞检测,比如:两个物体间的碰撞检测、一个物体与一个点的碰撞检测、基于距离的碰撞检测等等碰撞检测方法。
**什么是碰撞检测呢?**
简单来说,碰撞检测就是判定两个物体是否在同一时间内占用一块空间,用数学的角度来看,就是两个物体有没有交集。
检测碰撞的方法有很多,一般我们使用如下两种:
从几何图形的角度来检测,就是判断一个物体是否与另一个有重叠,我们可以用物体的矩形边界来判断。
检测距离,就是判断两个物体是否足够近到发生碰撞,需要计算距离和判断两个物体是否足够近。
**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){}
}
};
```