这篇是学习和回顾canvas系列笔记的第六篇,完整笔记详见:canvas核心技术。javascript
在上一篇canvas核心技术-如何实现复杂的动画笔记中,咱们详细讨论了在制做复杂动画时,须要考虑时间因素,物理因素等,同时还回顾了如何使用缓动函数来扭曲时间轴实现非线性运动,好比常见的缓入,缓出,缓入缓出等。在游戏或者动画中,运动的物体在变化的过程当中,它们是有可能碰撞在一块儿的,那么这一篇咱们就来详细学习下如何进行碰撞检测。html
最简单的检测手段就是边界值检测了,就是对一个运动的物体的某些属性进行条件判断,若是达到了这个条件,则说明发生了碰撞。例如在上一篇中的示例,小球自由下落,当在检测小球是否与地面发生碰撞时,咱们是检测小球下落的高度fh是否达到了小球自己距离地面的高度dh,若是fh>dh,则说明小球与地面发生了碰撞。java
let distance = ball.currentSpeed * t;
if (ball.offset + distance > ball.verticalHeight) {
//落到地面了,发生了碰撞
// ...
} else {
// 尚未落到地面,没有发生碰撞
ball.offset += distance;
}
复制代码
这种检测方式很是的简单且准确,在针对相似业务开发时,咱们能够简化成边界值检测。可是当咱们开发较为复杂游戏时,边界值检测一般不能很好的实现,为了更加真实,它一般与其余检测方法一块儿使用。github
在canvas游戏中,对于不规则的物体,好比运动的小人等,咱们能够经过抽象成一个矩形,使得这个矩形刚好能够包裹这个物体,在进行碰撞检测时,就可使用这个矩形来代替实际的物体。这种方法,实际上就是经过抽象,将复杂简单化,对于精确度不是那么高的动画或者游戏,咱们直接使用这种外接图形来检测就能够了。在抽象图形的时候,咱们要根据具体的物体,好比小人能够抽象成矩形,太阳就要抽象成圆了,把具体的物体抽象的跟它类似的形状,这样在检测时就会更加准确。typescript
进行了图形抽象以后,咱们在检测就只需对图形进行检测了。对于两个图形是否发生碰撞,咱们只须要判断它们是否存在相交的部分,若是存在相交的部分,那么则能够认为是发生了碰撞,不然就没有。下面,咱们分别来学习矩形和矩形的碰撞检测,圆和圆的碰撞检测,矩形和圆的碰撞检测。canvas
矩形与矩形碰撞状况,ide
这里列举两个矩形发生碰撞的全部状况,在canvas中具体代码实现以下,函数
/* 判断是否两个矩形发生碰撞 */
private didRectCollide(sprite: RectSprite, otherSprite: RectSprite) {
let horizontal = sprite.left + sprite.width > otherSprite.left && sprite.left < otherSprite.left + otherSprite.width;
let vertical = sprite.top < otherSprite.top + otherSprite.height && sprite.top + sprite.height > otherSprite.top;
return horizontal && vertical;
}
复制代码
其实就是分别在水平方向和垂直方向判断这两个矩形是否发生重叠。post
圆和圆碰撞状况,
判断两个圆是否发生碰撞,就是判断两个圆的圆心之间的距离是否小于它们的半径之和,若是小于半径之和,则发生碰撞,不然就没有发生碰撞。主要就是计算两个圆心之间的距离,能够根据坐标系中两点之间距离公式获得,
在canvas中具体代码实现以下,
/* 判断是否两个圆发生碰撞 */
private didCircleCollide(sprite: CircleSprite, otherSprite: CircleSprite) {
return distance(sprite.x, sprite.y, otherSprite.x, otherSprite.y) < sprite.radius + otherSprite.radius;
}
复制代码
矩形和圆碰撞状况,
这种状况,就是判断圆形到矩形上最近的一点的距离是否小于圆的半径,若是小于圆的半径,则发生碰撞,不然就没有发生碰撞。咱们首先要找到圆距离矩形上最近的点的坐标,这种就要考虑圆心在矩形左侧,圆心在矩形上面,圆心在矩形右侧,圆心在矩形下面,圆心在矩形里面这五种状况。若是圆心在矩形里面,那么必定是碰撞的。其余四种状况根据每一种状况来计算获得矩形上离圆心最近的一点,下面举例其中一种状况,其余状况原理相似,好比圆心在矩形左侧,
这种状况下,最近一点的X轴坐标跟矩形左上角坐标的X轴坐标相等,跟圆心Y轴坐标相等,这样就能够得出来了,。在canvas中具体代码实现以下,
/* 判断是否矩形和圆形发生碰撞 */
private didRectWidthCircleCollide(rectSprite: RectSprite, circleSprite: CircleSprite) {
let closePoint = { x: undefined, y: undefined };
if (circleSprite.x < rectSprite.left) {
closePoint.x = rectSprite.left;
} else if (circleSprite.x < rectSprite.left + rectSprite.width) {
closePoint.x = circleSprite.x;
} else {
closePoint.x = rectSprite.left + rectSprite.width;
}
if (circleSprite.y < rectSprite.top) {
closePoint.y = rectSprite.top;
} else if (circleSprite.y < rectSprite.top + rectSprite.height) {
closePoint.y = circleSprite.y;
} else {
closePoint.y = rectSprite.top + rectSprite.height;
}
return distance(circleSprite.x, circleSprite.y, closePoint.x, closePoint.y) < circleSprite.radius;
}
复制代码
光线投射法:画一条与物体的速度向量相重合的线,而后再从另一个待检测物体出发,绘制第二条线,根据两条线的交点位置来断定是否发生碰撞。
光线投射法通常还会结合边界值检测来进行严格准确的判断,这种方法要求咱们在动画更新中,不断计算出两个速度向量的交点坐标,根据交点坐标判断是否知足碰撞条件,交点知足了条件,咱们还要运用边界值检测方法来检测运动物体是否知足边界值条件,只有同时知足才判断为发生碰撞。这种检测,准确度通常比较高,特别是适用于运动速度快的物体。以小球投桶示例,检测代码以下,
/* 是否发生碰撞 */
public didCollide(ball: CircleSprite, bucket: ImageSprite) {
let k1 = ball.verticalVelocity / ball.horizontalVelocity;
let b1 = ball.y - k1 * ball.x;
let inertSectionY = bucket.mockTop; //计算交点Y坐标
let insertSectionX = (inertSectionY - b1) / k1; //计算交点X坐标
return (
insertSectionX > bucket.mockLeft &&
insertSectionX < bucket.mockLeft + bucket.mockWidth &&
ball.x > bucket.mockLeft &&
ball.x < bucket.mockLeft + bucket.mockWidth &&
ball.y > bucket.mockTop &&
ball.y < bucket.mockTop + bucket.mockHeight
);
}
}
复制代码
在判断凸多边形的碰撞检测时,咱们可使用分离轴方法。在学习分离轴检测以前,咱们须要先熟悉向量的一些基础知识。
向量基础知识:
图中能够看到,,
。多余凸多边形的每一个顶点,咱们能够用向量来表示。
分离轴检测思路,
/* 判断是否发生碰撞 */
public didCollide(sprite: Sprite, otherSprite: Sprite) {
let axes1 = sprite.type === 'circle' ? (sprite as Circle).getAxes(otherSprite as Polygon) : (sprite as Polygon).getAxes();
let axes2 = otherSprite.type === 'circle' ? (otherSprite as Circle).getAxes(sprite as Polygon) : (otherSprite as Polygon).getAxes();
// 第一步:获取全部的投影轴
// 第二步:获取多边形在各个投影轴的投影
// 第三步:判断是否存在一条投影轴上,多边形的投影不相交,若是存在不相交的投影则直接返回false,若是有所的投影轴上的投影都存在相交,则说明相碰了。
let axes = [...axes1, ...axes2];
for (let axis of axes) {
let projections1 = sprite.getProjection(axis);
let projections2 = otherSprite.getProjection(axis);
if (!projections1.overlaps(projections2)) {
return false;
}
}
return true;
}
}
复制代码
下面咱们就按照这三个步骤来,一步一步实现分离轴检测方法。
获取投影轴
在多边形中,咱们是以边来创建边向量的,边向量的法向量,就是这条边的投影轴了。对于投影轴,咱们只需它的方向,因此通常会把它格式化为单位向量。
//获取凸多边形的投影轴
public getAxes() {
let points = this.points;
let axes = [];
for (let i = 0, j = points.length - 1; i < j; i++) {
let v1 = new Vector(points[i].x, points[i].y);
let v2 = new Vector(points[i + 1].x, points[i + 1].y);
axes.push(
v1
.subtract(v2)
.perpendicular()
.normalize(),
);
}
let firstPoint = points[0];
let lastPoint = points[points.length - 1];
let v1 = new Vector(lastPoint.x, lastPoint.y);
let v2 = new Vector(firstPoint.x, firstPoint.y);
axes.push(
v1
.subtract(v2)
.perpendicular()
.normalize(),
);
return axes;
}
复制代码
获取了待检测图形的投影轴以后,咱们就须要计算图形在每条投影轴上的投影
public getProjection(v: Vector) {
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
for (let point of this.points) {
let p = new Vector(point.x, point.y);
let dotProduct = p.dotProduct(v);
min = Math.min(min, dotProduct);
max = Math.max(max, dotProduct);
}
return new Projection(min, max);
}
复制代码
最后判断投影是否重叠
/* 投影是否重叠 */
overlaps(p: Projection) {
return this.max > p.min && p.max > this.min;
}
复制代码
其中,若是是一个圆形与一个凸多边形的检测时,在计算圆对应的投影轴时比较特殊,圆只有一条投影轴,就是圆心与它距离多边形最近顶点的向量,
//获取圆的投影轴
public getAxes(polygon: Polygon) {
// 对于圆来讲,获取其投影轴就是将圆心与他距离多边形最近顶点的连线
let { x, y } = this;
let nearestPoint = null;
let nearestDistance = Number.MAX_SAFE_INTEGER;
for (let [index, point] of polygon.points.entries()) {
let d = distance(x, y, point.x, point.y);
if (d < nearestDistance) {
nearestDistance = d;
nearestPoint = point;
}
}
let v1 = new Vector(x, y);
let v2 = new Vector(nearestPoint.x, nearestPoint.y);
return [v1.subtract(v2).normalize()];
}
复制代码
这篇笔记详细记录了2d图形中碰撞检测的方法,比较简单的方法是外接图形法和边界值检测法,它们相对不是那么精确,比较复杂和精确的方法有光线投射法和分离轴法。根据不一样的场景和精确度要求,咱们选择不一样的方法。其余,除了上面几种,还有像素检测等方法也能够实现碰撞检测,像素检测是以像素为单位来检测,若是存在不透明的像素在同一个坐标上重叠,则说明发生了碰撞,具体实现能够查看Pixel accurate collision detection with Javascript and Canvas。
对于这几种检测方法,强力建议熟悉掌握分离轴法,由于它使用的范围最为普遍,对于任意的凸多边形,它均可以较精确的检测出来。因为分离轴检测法计算量通常比较大,因此在检测以前,咱们先过滤掉那些根本不可能发生碰撞的图形,通常方法是空间分隔法,或者过滤可视区间不可见的图形等,而后再对较小的一部分可能发生碰撞的图形来进行计算检测,这样能够提高检测的速度。