概括
主要阐述2d图形中碰撞检测原理及关键代码实现。主要分为:规则的几何图形碰撞判断,不规则多边形碰撞判断,像素检测。里面涉及一些数学几何原理:分离轴定理、向量叉乘的几何意义。
规则的几何图形碰撞判断
矩形与矩形
两个矩形,我们只需根据矩形所在的坐标位置,以及自己的宽高来比较,得出两者是否相交,相交则发生碰撞。
圆形与圆形
只需要判断两个圆形的圆心之间的距离,是否小于两者的半径之和。
圆形与矩形
在矩形上找到离圆心最近的点,比较这个点到圆心的距离是否小于圆的半径。
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图形,都是使用的不规则形状,通过对图形的外围进行近似包围处理后,图形就可以变成多边形,用多边形碰撞检测原理来实现图形的碰撞检测。
凸多边形与凸多边形
可以使用分离轴定理来判断两个图形是否重叠。
对分离轴定理做一个类比:
假如你用一个手电筒从不同的角度照射到两个图形上,从每一个角度都找不到两者之间存在缝隙,那么这两个图形一定有接触,如果有一个缝隙,那两者一定没有接触。
在实际开发中,针对多边形,我们只需要正对每一条边进行判断,不需要从所有的角度进行运算。
如上图,也就是沿着每条边向量方向进行投影,即边缘法向量为“投影轴”,对两个物体投射在轴上的阴影进行判断。如果每个方向的两个投影线段都有重叠,则两物体发生碰撞;如果有一个方向出现不重叠,则两者没发生碰撞。
-
点乘公式 a * b = x1 * x2 + y1 * y2 代表的含义就是一个向量在另一向量方向中的投影长度
-
法向量就是点乘为0
//获取多边形需要计算的分离轴(与传入的不重复)
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):
我们可以从判断组成两个图形的线段是否相交,来得出两个多边形是否发送碰撞。
如何判断两条线段相交呢?
假设有线段AB, CD, 若两者相交,则情况如下:
1、线段AB的两端点分布在线段CD所在直线两侧;
2、线段CD的两端点分布在线段AB所在的直线两侧;
或者:
一条线段的端点在另一条线段上;
具体实现:
线段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 则可判断图形相交;
如果r1 * r2 < 0 ,r3 * r4 = 0, 则表示一个线段的端点在另外一条直线上;
如果r1 * r2 = 0 ,r3 * r4 = 0,则表示两个线段平行,或者两线段共线,需要根据两个线段是否有重合点来判断是否碰撞;
// 多边形的线段是否相交
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,可以对图形进行外围盒近似实现,实际检测时,也是多种方式结合来检测。会先用一些规则的长方形或者圆形包围盒做一个最外围的包围,当判断该包围盒发生碰撞时,再进行内部近似包围盒检测,以此来减少碰撞检测的运算量