JavaScript实现碰撞检测(分离轴定理)

发布时间:2017-06-30
技术:JavaScript + HTML5

概述

分离轴定理是一项用于检测碰撞的算法。其适用范围较广,涵盖检测圆与多边形,多边形与多边形的碰撞;缺点在于无法检测凹多边形的碰撞。本demo使用Js进行算法实现,HTML5 canvas进行渲染。

详细

一、准备工作,熟悉分离轴定理 算法原理


(翻译至http://www.sevenson.com.au/actionscript/sat/)


从根本上来讲,分离轴定理(以及其他碰撞算法)的用途就是去检测并判断两个图形之间是否有间隙。分离轴定理中用到的方法使算法本身显得十分独特。

我所听到过分离轴定理的最好类比方式是这样的:

假想你拿一个电筒从不同的角度照射到两个图形上,那么会有怎样的一系列的阴影投射到它们之后的墙壁上呢?

sat_2.jpg

如果你用这个方式从每一个角度上对这两个图形进行处理,并都找不到任何的间隙,那么这两个图形就一定接触。如果你找到了一个间隙,那么这两个图形就显而易见地没有接触。

从编程的角度来讲,从每个可能的角度上去检测会使处理变得十分密集。不过幸运的是,由于多边形的性质,你只需要检测其中几个关键的角度。

你需要检测的角度数量就正是这个多边形的边数。也就是说,你所需检测的角度最大数量就是你要检测碰撞的两个多边形边数之和。举个例子,两个五边形就需要检测10个角度。

sat_3.jpg

这是一个简易但比较啰嗦的方法,以下是基本的步骤:

步骤一:从需要检测的多边形中取出一条边,并找出它的法向量(垂直于它的向量),这个向量将会是我们的一个“投影轴”。

sat_4.jpg

步骤二:循环获取第一个多边形的每个点,并将它们投影到这个轴上。(记录这个多边形投影到轴上的最高和最低点)

sat_5.jpg


步骤三:对第二个多边形做同样的处理。

sat_6.jpg


步骤四:分别得到这两个多边形的投影,并检测这两段投影是否重叠。

sat_7.jpg



如果你发现了这两个投影到轴上的“阴影”有间隙,那么这两个图形一定没有相交。但如果没有间隙,那么它们则可能接触,你需要继续检测直到把两个多边形的每条边都检测完。如果你检测完每条边后,都没有发现任何间隙,那么它们是相互碰撞的。

这个算法基本就是如此的。

顺带提一下,如果你记录了哪个轴上的投影重叠值最小(以及重叠了多少),那么你就能用这个值来分开这两个图形。

那么如何处理圆呢?


在分离轴定理中,检测圆与检测多边形相比,会有点点奇异,但仍然是可以实现的。

最值得注意的是,圆是没有任何的边,所以是没有明显的用于投影的轴。但它有一条“不是很明显的”的投影轴。这条轴就是途经圆心和多边形上离圆心最近的顶点的直线。

sat_8.jpg


在这以后就是按套路遍历另一个多边形的每条投影轴,并检测是否有投影重叠。

噢,对了,万一你想知道如何把圆投影到轴上,那你只用简单地把圆心投影上去,然后加上和减去半径就能得到投影长度了。

二、代码解析

1、html代码如下:

<canvas width="800" height="500" id="mycanvas">Loading...</canvas>
<div id="select-box"></div>

2、main.js主要是控制位移以及圆圈大小,代码如下:

var SHAPE_SIZE = 80, SHAPE_HANDLE_SIZE = 10;
var CANVAS_WIDTH, CANVAS_HEIGHT;

var renderer;
var shA = null, shB = null;

window.onload = function () {
	var canvasTag = document.getElementById("mycanvas");
	var canvas = canvasTag.getContext("2d");

	CANVAS_WIDTH = canvasTag.width;
	CANVAS_HEIGHT = canvasTag.height;

	renderer = new Renderer(canvas);

	setInterval(function () {
		renderer.loopDraw();
	}, 30);

	MouseEvent.addEvents(canvasTag);

	main();
};

function main () {
	UIUtils.createSelect();

	shA = UIUtils.createShape(150, 250, "shA-select");
	renderer.add(shA);

	shB = UIUtils.createShape(540, 250, "shB-select");
	renderer.add(shB);
}

function getPolygonVertices (edges, r) {  
	var ca = 0, aiv = 360 / edges, ata = Math.PI / 180, list = new Array();

	for (var k = 0; k < edges; k++) {
		var x = Math.cos(ca * ata) * r,
			y = Math.sin(ca * ata) * r;

		list.push(new Vec2(x, y));

		ca += aiv;
	}

	return list;
}

SHAPE_SIZE = 80, SHAPE_HANDLE_SIZE = 10 (SHAPE_SIZE设置外圆环大小,SHAPE_HANDLE_SIZE设置内圆大小),UIUtils.createShape(150, 250, "shA-select")设置第一个圆的x轴、y轴位移,还有外圆环选择的形状是什么,UIUtils.createShape(540, 250, "shB-select")设置第二个圆的x轴、y轴位移,还有外圆环选择的形状是什么。

3、SAT.js主要是控制拖动圆点时,外框的颜色等,部分代码如下:

var SAT = (function () {
	function testCollision (A, B) {
		var res, color = "#333333";

		if (A.type == "polygon" && B.type == "polygon") {
			res = polygonsCollisionTest(A, B);
		} else if (A.type == "circle" && B.type == "circle") {
			res = circlesCollisionTest(A, B);
		} else {
			var c, p;
			if (A.type == "circle") {
				c = A;
				p = B;
			} else {
				c = B;
				p = A;
			}

			res = circlePolygonCollisionTest(c, p);
		}

		if (res) {
			color = "#FF0000";
		}

		A.color = B.color = color;
	}

4、Circle.js是控制第二个圆的外框颜色等,代码如下:

function Circle (r) {
	this.objectIndex = Renderer.objectIndex++;
	this.type = "circle";
	this.r = r;
	this.x = 0;
	this.y = 0;
	this.color = "#333333";
}

Circle.prototype = {
	draw : function (c) {
		c.arc(0, 0, this.r, 0, Math.PI * 2);
	},

	getProjection : function (axis) {
		var pro = Vec2.dot(new Vec2(this.x, this.y), axis) / axis.length();

		return {min : pro - this.r, max : pro + this.r};
	}
};


5、Polygon.js是控制第一个圆的外框颜色等,代码如下:

function Polygon (list) {
	this.objectIndex = Renderer.objectIndex++;
	this.type = "polygon";
	this.vertices = list;
	this.x = 0;
	this.y = 0;
	this.color = "#333333";
}

Polygon.prototype = {
	getRootCoordinate : function () {
		var list = this.vertices, res = new Array();

		for (var i = 0, l = list.length; i < l; i++) {
			var coord = list[i];

			res.push(new Vec2(coord.x + this.x, coord.y + this.y));
		}

		return res;
	},

	draw : function (c) {
		var list = this.vertices;

		if (list.length <= 1) {
			return;
		}

		c.moveTo(list[0].x, list[0].y);

		for (var i = 1, l = list.length; i < l; i++) {
			var coord = list[i];

			c.lineTo(coord.x, coord.y);
		}

		c.closePath();
	},

	getSides : function () {
		var list = this.vertices,
			l = list.length,
			res = new Array();

		if (l >= 3) {
			for (var j = 1, pre = list[0]; j < l; j++) {
				var p = list[j];

				res.push(Vec2.substract(p, pre));

				pre = p;
			}

			res.push(Vec2.substract(list[0], list[l - 1]));
		}

		return res;
	},

	getProjection : function (axis) {
		var list = this.getRootCoordinate(), min = null, max = null;

		for (var i = 0, l = list.length; i < l; i++) {
			var p = list[i];

			var pro = Vec2.dot(p, axis) / axis.length();

			if (min === null || pro < min) {
				min = pro;
			}

			if (max === null || pro > max) {
				max = pro;
			}
		}

		return {min : min, max : max};
	},

	getNearestPoint : function (p1) {
		var list = this.getRootCoordinate(), rP = list[0], minDis = Vec2.distance(p1, rP);

		for (var i = 1, l = list.length; i < l; i++) {
			var p2 = list[i], d = Vec2.distance(p1, p2);

			if (d < minDis) {
				minDis = d;

				rP = p2;
			}
		}

		return rP;
	}
};


6、Renderer.js是控制整体外框的属性,比如颜色边框等,代码如下:

Renderer.prototype = {
	loopDraw : function () {
		var c = this.canvas;

		c.fillStyle = "#ff0000";
		c.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		
		for (var i = 0, l = this.displayList.length; i < l; i++) {
			var o = this.displayList[i];

			c.save();
			c.translate(o.x, o.y);

			c.beginPath();
			c.globalAlpha = 0.6;
			c.arc(0, 0, SHAPE_HANDLE_SIZE, 0, Math.PI * 2);
			c.fillStyle = "#0000FF";
			c.fill();

			c.beginPath();
			c.globalAlpha = 1;

			o.draw(c);

			c.strokeStyle = o.color;
			c.lineWidth = 2;
			c.stroke();
			c.restore();
		}
	},

	add : function (o) {
		this.displayList.push(o);
	},

	remove : function (o) {
		for (var i = 0, l = this.displayList.length; i < l; i++) {
			var child = this.displayList[i];

			if (child.objectIndex == o.objectIndex) {
				this.displayList.splice(i, 1);

				break;
			}
		}
	}
};


三、文件以及演示截图

1、文件截图

222.gif

2、演示截图

blob.png

3、双击index.html文件即可运行看效果

四、兼容性

兼容主流浏览器

本实例支付的费用只是购买源码的费用,如有疑问欢迎在文末留言交流,如需作者在线代码指导、定制等,在作者开启付费服务后,可以点击“购买服务”进行实时联系,请知悉,谢谢
手机上随时阅读、收藏该文章 ?请扫下方二维码