多邊形碰撞檢測在游戲開發(fā)中是非常常用的算法倦西,最直接的算法是檢測兩個多邊形的每個點是否被包含翎冲,但是由于多邊形的數(shù)量和多邊形點的數(shù)量導致這種最直接的算法的效率非常之低迈套。本文將介紹一個非常簡單并且效率極高的算法——“分離軸算法”,并用C語言和Lua語言分別實現(xiàn)該算法亥至,可以分別用于Cocos2d和Corona開發(fā)兆览。
分離軸算法
![圖片](http://boboboa32.weebly.com/uploads/6/4/0/3/6403660/2328142.png?310)
上圖就是分離軸算法的圖示屈溉,首先需要知道分離軸算法只適用于“凸多邊形”,但是由于“凹多邊形”可以分成多個凸多邊形組成抬探,所以該算法可以用于所有多邊形碰撞檢測子巾。不知道凹多邊形是什么的看下圖:
![圖片](http://boboboa32.weebly.com/uploads/6/4/0/3/6403660/5809243.gif)
凹多邊形就是含有頂點的內(nèi)角度超過180°的多邊形,反之就是凸多邊形。
簡單的說线梗,分離軸算法就是指兩個多邊形能有一條直線將彼此分開椰于,如圖中黑線“Seperating line”,而與之垂直的綠線就是分離軸“Separating axis”仪搔。圖中虛線表示的是多邊形在分離軸上的投影(Projection)瘾婿。詳細的數(shù)學理論請查看wiki,我這里只講該算法的實現(xiàn)方式僻造。如果用偽代碼來表示就是:
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所有的邊镀娶,如果所有法線都不滿足條件立膛,則說明兩多邊形相交。
算法實現(xiàn)
首先我們需要定義幾個數(shù)據(jù)類型和函數(shù)梯码。
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 <math.h>
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);
}
有了數(shù)據(jù)類型然后就是算法的判斷函數(shù)瓤鼻。
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為計算投影函數(shù),先計算所有邊長的投影贤重,然后算出投影的最大和最小點即起始點茬祷;overlap函數(shù)判斷兩條線段是否重合。
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<a.n;i++){
float proj = dot(a.vertices[i],axis); // find the projection of every point on the polygon onto the line.
if (proj < min) min = proj; if (proj > 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) {a = b; b = range[0];}
return (n >= 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;
}
最后是算法實現(xiàn)函數(shù)并蝗,使用到上面的數(shù)據(jù)和函數(shù)祭犯。
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<a.n;i++){
vec axis = a.edges[i].dir; // Get the direction vector
axis = perp(axis); // Get the normal of the vector (90 degrees)
float *a_ = project(a,axis), *b_ = project(b,axis); // Find the projection of a and b onto axis
if (!overlap(a_,b_)) return 0; // If they do not overlap, then no collision
}
for (i=0;i<b.n;i++){ // repeat for b
vec axis = b.edges[i].dir;
axis = perp(axis);
float *a_ = project(a,axis), *b_ = project(b,axis);
if (!overlap(a_,b_)) return 0;
}
return 1;
}
兩個函數(shù)的使用方法很簡單沃粗,只要定義好了多邊形就行了。
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