本文详解JavaScript中基于分离轴定理(SAT)的碰撞检测算法。通过电筒阴影类比解释原理,涵盖凸多边形与圆的检测逻辑,提供从法向量投影到重叠判断的完整步骤,并附带HTML5 Canvas实现代码与在线测试示例。
JS/HTML5游戏开发:分离轴定理(SAT)碰撞检测详解
本文详解JavaScript与HTML5游戏开发中常用的碰撞检测算法,重点介绍包围盒检测技术,供开发者参考实现。
概述
概述
分离轴定理(Separating Axis Theorem, SAT)是一种广泛适用的碰撞检测算法,支持圆与多边形、多边形与多边形之间的碰撞判断,但其局限性在于无法直接处理凹多边形的碰撞检测。本示例采用JavaScript编写算法逻辑,并通过HTML5 Canvas进行图形渲染。
详细原理
详细原理
一、理解分离轴定理算法原理
一、理解分离轴定理算法原理
从核心目的来看,分离轴定理及其他碰撞算法旨在检测并判断两个图形之间是否存在间隙。该算法采用的方法使其在实现上具有独特性。
理解分离轴定理的一个直观类比如下:
想象你使用手电筒从不同角度照射两个图形,观察它们投射到后方墙壁上的阴影形态。

若从所有角度照射后均未发现任何间隙,则判定两个图形必然发生接触;反之,若找到任一存在间隙的角度,则说明两者未接触。
从编程实现角度,遍历所有可能的角度进行检测计算量过大。然而,利用多边形的几何特性,只需检测少数关键角度即可得出结论。
需要检测的角度数量等于多边形的边数。具体而言,最大检测角度数为参与碰撞检测的两个多边形边数之和。例如,检测两个五边形的碰撞需检查10个角度。

以下是一种基础且逻辑清晰的实现步骤:
步骤一:从待检测的多边形中选取一条边,计算其法向量(即垂直于该边的向量),该向量将作为我们的“投影轴”。

步骤二:遍历第一个多边形的所有顶点,将它们投影至该轴上,并记录该多边形在轴上的最高点和最低点。

步骤三:对第二个多边形执行相同的投影处理。

步骤四:获取两个多边形在轴上的投影区间,并判断这两个区间是否发生重叠。

若在任一轴上发现投影“阴影”存在间隙,则判定两个图形未相交;若所有轴上均无间隙,则需继续检测直至遍历完两个多边形的所有边。若遍历所有边后仍未发现间隙,则判定两者发生碰撞。
上述即为该算法的基本流程。
此外,若记录投影重叠值最小的轴及其重叠量,可利用该信息计算将两个图形分离所需的位移向量。
那么,如何处理圆的碰撞检测?
在分离轴定理框架下,圆的检测与多边形略有不同,但同样可行。
圆没有边,因此不存在明显的投影轴。但存在一条“隐含”的投影轴,即穿过圆心且垂直于多边形上离圆心最近顶点的直线。

确定该轴后,按常规流程遍历另一个多边形的所有投影轴,并检测投影是否重叠。
关于将圆投影至轴上的方法:只需将圆心坐标投影至轴上,然后分别加上和减去圆的半径,即可得到圆在轴上的投影区间。
二、完整代码实例
二、完整代码实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta charset="UTF-8">
<title>盒包围碰撞算法-凸多边形分离轴检测算法</title>
<style>
#stage {
border: 1px solid lightgray;
}
</style>
</head>
<body>
<h1>是否碰撞:<span class="hitTest">否</span></h1>
<canvas id="stage"></canvas>
</body>
<script>
window.onload = function () {
var stage = document.querySelector('#stage'),
ctx = stage.getContext('2d');
stage.width = 400;
stage.height = 400;
var starPointArr = [];
// 绘制五角星
function drawStar(ctx,r,R,x,y,rot,c){
ctx.beginPath();
for(var i =0;i<5;i++){
var startPosX = Math.cos((18 + i*72 - rot)/180 * Math.PI )*R + x,
startPosY = - Math.sin((18 + i*72 - rot)/180 * Math.PI)*R + y,
endPosX = Math.cos((54 + i*72 - rot)/180 * Math.PI )*r + x,
endPosY = - Math.sin((54 + i*72 - rot)/180 * Math.PI)*r + y;
ctx.lineTo(startPosX,startPosY);
ctx.lineTo(endPosX, endPosY);
starPointArr.push(startPosX,startPosY,endPosX,endPosY);
}
ctx.closePath();
ctx.fillStyle = c;
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.fill();
ctx.stroke();
}
var polygonPointArr = [];
// 绘制多边形
function drawpolygon(numSides,radius,x,y){
ctx.beginPath();
for(i = 1;i<=numSides; i++){
var xPos = x+radius*Math.cos(2*Math.PI*i/numSides);
var yPos = x+radius*Math.sin(2*Math.PI*i/numSides);
polygonPointArr.push(xPos,yPos);
ctx.lineTo(xPos,yPos);
}
//创建完成 闭合路径
ctx.closePath();
ctx.lineWidth = 3; //线宽
ctx.lineJoin = "round";
ctx.fillStyle = '#00f';
ctx.fill();
ctx.stroke();
}
//两个向量的点积
function dotV2(v1,v2) {
return v1.x*v2.x+v1.y*v2.y;
}
//计算polyArr在轴线axis上的投影,polyArr是一系列点坐标的集合,数组表示
function calcProj(axis,polyArr) {
var v = {"x":polyArr[0],"y":polyArr[1]};
var d,min,max;
min = max = dotV2(axis,v);//计算投影轴与第一个坐标点的点积
for(var i=2;i<polyArr.length-1;i+=2) {
v.x=polyArr[i];
v.y=polyArr[i+1];
d = dotV2(axis,v);//计算v到投影轴的距离,遍历出最小和最大区间
min = (d<min)?d:min;
max = (d>max)?d:max;
}
return [min,max];
}
//计算同一个轴上线段的距离s1(min1,max1),s2(min2,max2),如果距离小于0则表示两线段有相交;
function segDist(min1,max1,min2,max2) {
if(min1<min2)
{
return min2-max1;
}
else
{
return min1-max2;
}
}
//判断两个多边形是否相交碰撞,p1,p2用于保存多边形点的数组
function isCollide(p1,p2) {
//定义法向量
var e = {"x":0,"y":0};
var p = p1,idx=0,len1=p1.length,len2=p2.length,px,py;//p缓存形状p1的数据
for(var i=0,len = len1+len2;i<len-1;i+=2)//遍历所有坐标点,i+=2代表xy轴两个坐标点
{
idx = i;
//计算两个多边形每条边
if(i>len1) {//当p1遍历完毕后,p缓存形状p2的数据,从新遍历
p=p2;
idx=(i-len1);//len2
}
if(i===p.length-2) {//p包含的点数据组成的最后一个坐标点
px=p[0]-p[idx];//首尾的x轴相连
py=p[1]-p[idx+1];//首尾的y轴相连
} else {
px = p[idx+2]-p[idx];//递增的x轴相连
py = p[idx+3]-p[idx+1];//递减的y轴相连
}
//得到边的法向量【垂直相交】,即投影轴
e.x = -py;
e.y = px;
//计算两个多边形在法向量上的投影
var pp1 = calcProj(e,p1);//涵盖到投影轴的最小值与最大值
var pp2 = calcProj(e,p2);
//计算两个线段在法向量上距离,如果大于0则可以退出,表示无相交
if(segDist(pp1[0],pp1[1],pp2[0],pp2[1])>0) {
return false;
}
}
return true;
}
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
//根据地图数组碰撞将测
switch (e.keyCode) {
case 37:
console.log("Left");
if (starPosX > 0) {
starPosX -= 2;
}
break;
case 38:
console.log("Top");
if (starPosY > 0) {
starPosY -= 2;
}
break;
case 39:
console.log("Right");
if (starPosX < stage.width) {
starPosX += 2;
}
break;
case 40:
console.log("Bottom");
if (starPosY < stage.height) {
starPosY += 2;
}
break;
default:
return false;
}
};
var starPosX = stage.width/2,starPosY = stage.height/2;
stage.addEventListener('click', function (event) {
var x = event.clientX - stage.getBoundingClientRect().left;
var y = event.clientY - stage.getBoundingClientRect().top;
starPosX = x;
starPosY = y;
});
function update() {
ctx.clearRect(0, 0, 400, 400);
starPointArr = [];
polygonPointArr = [];
drawpolygon(7,50,300,300);
drawStar(ctx,30,50,starPosX,starPosY,30,"yellow");
document.querySelector('.hitTest').innerHTML = "否";
var flag = isCollide(starPointArr, polygonPointArr);
if (flag) {
document.querySelector('.hitTest').innerHTML = "是";
}
requestAnimationFrame(update);
}
update();
};
</script>
</html>
可使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun 测试上述代码,运行结果如下:

GitHub源码地址:https://github.com/krapnikkk/JS-gameMathematics
参考文章:http://www.demodashi.com/demo/10423.html
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》
希望本文所述的算法实现与解析对JavaScript程序设计有所帮助。