牛骨文教育服务平台(让学习变的简单)

碰撞检测

碰撞检测是物体与物体之间的交互,其实在前面的边界检测也是一种碰撞检测,只不过检测的对象是物体与边界之间。在本章中,我们将介绍更多的碰撞检测,比如:两个物体间的碰撞检测、一个物体与一个点的碰撞检测、基于距离的碰撞检测等等碰撞检测方法。

什么是碰撞检测呢?

简单来说,碰撞检测就是判定两个物体是否在同一时间内占用一块空间,用数学的角度来看,就是两个物体有没有交集。

检测碰撞的方法有很多,一般我们使用如下两种:
从几何图形的角度来检测,就是判断一个物体是否与另一个有重叠,我们可以用物体的矩形边界来判断。
检测距离,就是判断两个物体是否足够近到发生碰撞,需要计算距离和判断两个物体是否足够近。

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。(如果你看不明白这段代码,请看下图,让一个矩形分别位于另一个矩形的上下左右位置):

检测函数已经知道了,当要检测两个物体是否相交时,就可以做如下判断:

if (tool.intersects(objectA,objectB)) {
  console.log("撞上了");
}

注意:这里传入的必须是矩形对象。如果是球,可调用getBound()方法返回矩形对象。如果已经是矩形对象,就直接传入。

这里有一个需要注意的问题,有些时候,我们的物体是不规则的,如果我们采取矩形边界检测,有时候会不精确(只有真正的矩形才是精确的):

在上面的图中,有矩形、圆形和五角形,我们都可以采取矩形边界检测法,不过,你会发现,当物体是不规则的形状时,虽然通过上面的 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){}
  }
};