2D图形碰撞检测

2,272 阅读6分钟

概括

主要阐述2d图形中碰撞检测原理及关键代码实现。主要分为:规则的几何图形碰撞判断,不规则多边形碰撞判断,像素检测。里面涉及一些数学几何原理:分离轴定理、向量叉乘的几何意义。


规则的几何图形碰撞判断

矩形与矩形

两个矩形,我们只需根据矩形所在的坐标位置,以及自己的宽高来比较,得出两者是否相交,相交则发生碰撞。

1.png

圆形与圆形

只需要判断两个圆形的圆心之间的距离,是否小于两者的半径之和。

圆形与矩形

在矩形上找到离圆心最近的点,比较这个点到圆心的距离是否小于圆的半径。

if(this._circle.x < this._rect.x){
    closestPoint.x = this._rect.x
} else if(this._circle.x > this._rect.x + this._rect.width){
    closestPoint.x = this._rect.x + this.rect.width;
} else {
    closestPoint.x = this._circle.x;
}

if(this._circle.y < this.rect.y){
    closestPoint.y = this._rect.y
} else if(this._circle.y > this._rect.y + this._rect.height){
    closestPoint.y = this._rect.y + this.rect.height;
} else {
    closestPoint.y = this._circle.y;
}

不规则多边形碰撞判断

实际开发时,大多数2d图形,都是使用的不规则形状,通过对图形的外围进行近似包围处理后,图形就可以变成多边形,用多边形碰撞检测原理来实现图形的碰撞检测。

凸多边形与凸多边形

可以使用分离轴定理来判断两个图形是否重叠。

对分离轴定理做一个类比:

假如你用一个手电筒从不同的角度照射到两个图形上,从每一个角度都找不到两者之间存在缝隙,那么这两个图形一定有接触,如果有一个缝隙,那两者一定没有接触。

2.png

在实际开发中,针对多边形,我们只需要正对每一条边进行判断,不需要从所有的角度进行运算。

3.png

如上图,也就是沿着每条边向量方向进行投影,即边缘法向量为“投影轴”,对两个物体投射在轴上的阴影进行判断。如果每个方向的两个投影线段都有重叠,则两物体发生碰撞;如果有一个方向出现不重叠,则两者没发生碰撞。

  • 点乘公式 a * b = x1 * x2 + y1 * y2 代表的含义就是一个向量在另一向量方向中的投影长度

  • 法向量就是点乘为0

4.png

//获取多边形需要计算的分离轴(与传入的不重复)
public static getUniqueAxis(p: Array<Vector>, curaxis: Array<Vector> = []): Array<Vector> {
    var i, j: number = 0;
    var b: boolean = false;
    var nor: Vector = new Vector(0, 0);
    var segment: Vector = new Vector(0, 0);

    for (i = 0; i < p.length; i++) {
        if (i >= p.length - 1) {
            segment.x = p[0].x - p[i].x;
            segment.y = p[0].y - p[i].y;
        } else {
            segment.x = p[i + 1].x - p[i].x;
            segment.y = p[i + 1].y - p[i].y;
        }
        // 获取单位向量(即向量大小为1,用于表示向量方向),一个非零向量除以它的模即可得到单位向量
        nor = VectorUtil.perp(VectorUtil.normalize(segment));
        if (nor.x <= 0) {
               if (nor.x == 0) {
                if (nor.y < 0) nor.y *= -1;
            } else {
                nor.x *= -1;
                nor.y *= -1;
            }
        }
        b = true;
        //如果存在相同方向的分离轴,则不插入
        for (j = 0; j < curaxis.length; j++) {
            if (curaxis[j].x != nor.x) continue;
            if (curaxis[j].y != nor.y) continue;
            b = false;
            break;
        }
        if (!b) continue;
        curaxis.push(nor);
    }
    return curaxis;
}

分离轴定理优点及不足:

优点:

  • 分离轴算法较快,使用数学向量进行运算,当检测出有间隙时,则能得出结论,可减少其余不必要的运算。
  • 分离轴算法十分准确。

缺点:

  • 算法只适用于凸多边形,对凹边形不适用
  • 没法知道到底是那条边发生了碰撞,只知道重叠了多少,以及分开需要移动的最短距离

凹多边形碰撞

根据以上分析,凹多边形显然没法使用分离轴定理进行判断,那凹多边形要如何进行碰撞检测呢?

我们引用一个几何知识点,向量的叉乘:向量a与向量b进行叉乘,若小于0,表示向量b在向量a的顺时针方向,若大于0,表示向量b在向量a的逆时针方向,若等于0,向量a与向量b平行。

用行列式计算:a(x1, y1), b(x2, y2):

5.png

我们可以从判断组成两个图形的线段是否相交,来得出两个多边形是否发送碰撞。

如何判断两条线段相交呢?

假设有线段AB, CD, 若两者相交,则情况如下:

1、线段AB的两端点分布在线段CD所在直线两侧;

2、线段CD的两端点分布在线段AB所在的直线两侧;

或者:

一条线段的端点在另一条线段上;

6.png

7.png

具体实现:

线段a: A1(x1,y1), A2(x2,y2) 线段b: B1(x11,y11),B2(x22,y22)

对两个线段进行叉乘运算:

r1 = (A2 - A1) * (B1 - A1) r2 = (A2 - A1) * (B2 - A1) r3 = (B2 - B1) * (A1 - B1) r4 = (B2 - B1) * (A2 - B1)

(上面都是进行叉乘操作)

如果r1 * r2 < 0 ,并且r3 * r4 < 0 则可判断图形相交;

8.png

如果r1 * r2 < 0 ,r3 * r4 = 0, 则表示一个线段的端点在另外一条直线上;

9.png

如果r1 * r2 = 0 ,r3 * r4 = 0,则表示两个线段平行,或者两线段共线,需要根据两个线段是否有重合点来判断是否碰撞;

10.png

11.png

12.png

// 多边形的线段是否相交
private static segment(polygon1: Polygon, polygon2: Polygon): boolean {
    // 线段相交
    for (let i = 1; i < polygon1.points.length; ++i) {

        var seg1pt1 = new egret.Point(polygon1.points[i - 1].x, polygon1.points[i - 1].y);
        var seg1pt2 = new egret.Point(polygon1.points[i].x, polygon1.points[i].y);
        var b = this.d1(seg1pt1, seg1pt2, polygon2);
        if (b == true) {
            return true;
        }
    } // end for

    var seg1pt11 = new egret.Point(polygon1.points[polygon1.points.length - 1].x, polygon1.points[polygon1.points.length - 1].y);
    var seg1pt22 = new egret.Point(polygon1.points[0].x, polygon1.points[0].y);
    var b = this.d1(seg1pt11, seg1pt22, polygon2);
    if (b == true) {
        return true;
    }

    return false;
}

像素检测

如果功能需要很精确的碰撞检测,上面的多边形包围盒方式可能会导致碰撞的不准确,此时可以使用像素检测来达到精准碰撞判断。

一般分两步,先将一个图形外围加一个圆形或长方形的包围盒进行判断,如果包围盒还未发生碰撞,则直接判断为未碰撞,如果进入包围盒,则开始进行像素检测。

像素检测的原理,由于所有的精灵都是由像素点组成,每个像素数据 都是由RGBA四个数据组成,像素检测就以像素点的透明度A,来进行像素检测。当两个精灵在同一位置的透明度都不为0时,则判断为已碰撞,停止其它像素点的判断。

像素检测虽然能精准检测碰撞,但是由于要遍历所有的像素点,运算量大,对程序运行性能影响较大,一般实际应用中使用很少,特别是对较大较多的精灵进行检测。


总结

本文主要讲解了多边形的碰撞检测算法,及部分代码实现,代码为白鹭引擎下的实现。实际使用中,大多游戏引擎有封装一些外围包围盒的api,可以对图形进行外围盒近似实现,实际检测时,也是多种方式结合来检测。会先用一些规则的长方形或者圆形包围盒做一个最外围的包围,当判断该包围盒发生碰撞时,再进行内部近似包围盒检测,以此来减少碰撞检测的运算量