多边形碰撞检测在游戏开发中是非常常用的算法,最直接的算法是检测两个多边形的每个点是否被包含,但是由于多边形的数量和多边形点的数量导致这种最直接的算法的效率非常之低。本文将介绍一个非常简单并且效率极高的算法——“分离轴算法”,并用C语言和Lua语言分别实现该算法,可以分别用于Cocos2d和Corona开发。
分离轴算法
上图就是分离轴算法的图示,首先需要知道分离轴算法只适用于“凸多边形”,但是由于“凹多边形”可以分成多个凸多边形组成,所以该算法可以用于所有多边形碰撞检测。不知道凹多边形是什么的看下图:
凹多边形就是含有顶点的内角度超过180°的多边形,反之就是凸多边形。
简单的说,分离轴算法就是指两个多边形能有一条直线将彼此分开,如图中黑线“Seperating line”,而与之垂直的绿线就是分离轴“Separating axis”。图中虚线表示的是多边形在分离轴上的投影(Projection)。详细的数学理论请查看wiki,我这里只讲该算法的实现方式。如果用伪代码来表示就是:
bool sat(polygon a, polygon b){
for (int i = 0; i < a.edges.length; i++){
vector axis = a.edges[i].direction; // Get the direction vector of the edge
axis = vec_normal(axis); // We need to find the normal of the axis vector.
axis = vec_unit(axis); // We also need to "normalize" this vector, or make its length/magnitude equal to 1
// Find the projection of the two polygons onto the axis
segment proj_a = project(a, axis), proj_b = project(b, axis);
if(!seg_overlap(proj_a, proj_b)) return false; // If they do not overlap, then return false
}
... // Same thing for polygon b
// At this point, we know that there were always intersections, hence the two polygons must be colliding
return true;
}
首先取多边形a的一边,得出该边的法线(即分离轴)。然后算出两个多边形在该法线上的投影,如果两个投影没有重叠则说明两个多边形不相交。遍历多边形a所有的边,如果所有法线都不满足条件,则说明两多边形相交。
算法实现
首先我们需要定义几个数据类型和函数。
Lua:
function vec(x, y)
return {x, y}
end
v = vec -- shortcut
function dot(v1, v2)
return v1[1]*v2[1] + v1[2]*v2[2]
end
function normalize(v)
local mag = math.sqrt(v[1]^2 + v[2]^2)
return vec(v[1]/mag, v[2]/mag)
end
function perp(v)
return {v[2],-v[1]}
end
function segment(a, b)
local obj = {a=a, b=b, dir={b[1] - a[1], b[2] - a[2]}}
obj[1] = obj.dir[1]; obj[2] = obj.dir[2]
return obj
end
function polygon(vertices)
local obj = {}
obj.vertices = vertices
obj.edges = {}
for i=1,#vertices do
table.insert(obj.edges, segment(vertices[i], vertices[1+i%(#vertices)]))
end
return obj
end
vec为矢量或者向量,也可表示点;dot为矢量点投影运算;normalize为求模运算;perp计算法线向量;segment表示线段;polygon为多边形,包括顶点vertices和边edges,所有点的顺序必须按顺时针或者逆时针。如:
a = polygon{v(0,0),v(0,1),v(1,1),v(1,0)}
下面是C语言版的:
typedef struct {float x, y;} vec;
vec v(float x, float y){
vec a = {x, y}; // shorthand for declaration
return a;
}
float dot(vec a, vec b){
return a.x*b.x+a.y*b.y;
}
#include
vec normalize(vec v){
float mag = sqrt(v.x*v.x + v.y*v.y);
vec b = {v.x/mag, v.y/mag}; // vector b is only of distance 1 from the origin
return b;
}
vec perp(vec v){
vec b = {v.y, -v.x};
return b;
}
typedef struct {vec p0, p1, dir;} seg;
seg segment(vec p0, vec p1){
vec dir = {p1.x-p0.x, p1.y-p0.y};
seg s = {p0, p1, dir};
return s;
}
typedef struct {int n; vec *vertices; seg *edges;} polygon; // Assumption: Simply connected => chain vertices together
polygon new_polygon(int nvertices, vec *vertices){
seg *edges = (seg*)malloc(sizeof(seg)*(nvertices));
int i;
for (i = 0; i < nvertices-1; i++){
vec dir = {vertices[i+1].x-vertices[i].x, vertices[i+1].y-vertices[i].y};seg cur = {vertices[i], vertices[i+1], dir}; // We can also use the segment method here, but this is more explicit
edges[i] = cur;
}
vec dir = {vertices[0].x-vertices[nvertices-1].x, vertices[0].y-vertices[nvertices-1].y};seg cur = {vertices[nvertices-1], vertices[0], dir};
edges[nvertices-1] = cur; // The last edge is between the first vertex and the last vertex
polygon shape = {nvertices, vertices, edges};
return shape;
}
polygon Polygon(int nvertices, ...){
va_list args;
va_start(args, nvertices);
vec *vertices = (vec*)malloc(sizeof(vec)*nvertices);
int i;
for (i = 0; i < nvertices; i++){
vertices[i] = va_arg(args, vec);
}
va_end(args);
return new_polygon(nvertices, vertices);
}
有了数据类型然后就是算法的判断函数。
Lua:
-- We keep a running range (min and max) values of the projection, and then use that as our shadow
function project(a, axis)
axis = normalize(axis)
local min = dot(a.vertices[1],axis)
local max = min
for i,v in ipairs(a.vertices) do
local proj = dot(v, axis) -- projection
if proj < min then min = proj end
if proj > max then max = proj end
end
return {min, max}
end
function contains(n, range)
local a, b = range[1], range[2]
if b < a then a = b; b = range[1] end
return n >= a and n <= b
end
function overlap(a_, b_)
if contains(a_[1], b_) then return true
elseif contains(a_[2], b_) then return true
elseif contains(b_[1], a_) then return true
elseif contains(b_[2], a_) then return true
end
return false
end
project为计算投影函数,先计算所有边长的投影,然后算出投影的最大和最小点即起始点;overlap函数判断两条线段是否重合。
C:
float* project(polygon a, vec axis){
axis = normalize(axis);
int i;
float min = dot(a.vertices[0],axis); float max = min; // min and max are the start and finish points
for (i=0;i max) max = proj;
}
float* arr = (float*)malloc(2*sizeof(float));
arr[0] = min; arr[1] = max;
return arr;
}
int contains(float n, float* range){
float a = range[0], b = range[1];
if (b= a && n <= b);
}
int overlap(float* a_, float* b_){
if (contains(a_[0],b_)) return 1;
if (contains(a_[1],b_)) return 1;
if (contains(b_[0],a_)) return 1;
if (contains(b_[1],a_)) return 1;
return 0;
}
最后是算法实现函数,使用到上面的数据和函数。
Lua:
function sat(a, b)
for i,v in ipairs(a.edges) do
local axis = perp(v)
local a_, b_ = project(a, axis), project(b, axis)
if not overlap(a_, b_) then return false end
end
for i,v in ipairs(b.edges) do
local axis = perp(v)
local a_, b_ = project(a, axis), project(b, axis)
if not overlap(a_, b_) then return false end
end
return true
end
遍历a和b两个多边形的所有边长,判断投影是否重合。
C:
int sat(polygon a, polygon b){
int i;
for (i=0;i
两个函数的使用方法很简单,只要定义好了多边形就行了。
Lua:
a = polygon{v(0,0),v(0,5),v(5,4),v(3,0)}
b = polygon{v(4,4),v(4,6),v(6,6),v(6,4)}
print(sat(a,b)) -- true
C:
polygon a = Polygon(4, v(0,0),v(0,3),v(3,3),v(3,0)), b = Polygon(4, v(4,4),v(4,6),v(6,6),v(6,4));
printf("%d\n", sat(a,b)); // false
a = Polygon(4, v(0,0),v(0,5),v(5,4),v(3,0)); b = Polygon(4, v(4,4),v(4,6),v(6,6),v(6,4));
printf("%d\n", sat(a,b)); // true
完整的函数下载:Lua、C