美文网首页
多边形碰撞检测(分离轴算法Lua实现)

多边形碰撞检测(分离轴算法Lua实现)

作者: zzqlb | 来源:发表于2019-12-15 23:47 被阅读0次

在游戏开发中,我们一般常用的碰撞检测算法有AABB,OBB以及分离轴算法。
AABB与OBB一般用于矩形的碰撞检测,而多边形的碰撞检测一般使用分离轴算法,这里说的都是2D图形的碰撞检测

一、什么是分离轴?

将两个多边形投影到任意的一条轴上,如果能找到至少有一个轴上的两个多边形投影没有重叠,那么就说明这两个多边形没有发生碰撞。
在程序实现中,我们不可能去检测所有的轴,这是不现实的,不过好在多边形的特性,我们只需要以多边形的每一条边的法线为轴,分别检测一遍就好了。
分离轴适用的是凸多边形之间检测,不适用于凹多边形,凹多边形的检测,可以通过算法将凹多边形分割成多个凸多边形再进行计算。


分离轴.png

多边形A,B在轴Z''上的投影A''与B''是分离的,所以多边形A,B是分离的。

二、算法实现
2.1、多边形与多边形
(1)辅助方法
---获取两点的距离
function GetDis(vec1,vec2)
    local x1 = vec1.x or vec1[1]
    local y1 = vec1.y or vec1[2]
    local x2 = vec2.x or vec2[1]
    local y2 = vec2.y or vec2[2]

    local disX = x1 - x2
    local disY = y1 - y2
    local dis = math.sqrt(disX * disX + disY * disY)
    return dis
end

---向量归一化
function Normalize(vec)
    local x = vec[1] or vec.x
    local y = vec[2] or vec.y
    local mag = math.sqrt(x*x + y*y)
    if type(vec) == "table" then
        vec[1] = x/mag
        vec[2] = y/mag
    end
    vec.x = x/mag
    vec.y = y/mag
end

---点乘
function Dot(vec1,vec2)
    local x1 = vec1.x or vec1[1]
    local y1 = vec1.y or vec1[2]
    local x2 = vec2.x or vec2[1]
    local y2 = vec2.y or vec2[2]
    return x1*x2 + y1*y2
end

---精确到小数点后n位
---num 浮点数
---n 浮点数精确位数
function FloatAccurateN(num,n)
    if type(num) ~= "number" then
        return num;
    end
    n = n or 0;
    n = math.floor(n)
    if n < 0 then
        n = 0;
    end
    local nDecimal = 10 ^ n
    local nTemp = math.floor(num * nDecimal);
    local nRet = nTemp / nDecimal;
    return nRet;
end

---二维向量的向量积
---大小的绝对值表示两个向量构成的三角形的面积的2倍
---正负表示与两个向量构成的平面的法线的方向
function VectorProduct(vec1,vec2)
    local vec1X = vec1.x or vec1[1]
    local vec1Y = vec1.y or vec1[2]
    local vec2X = vec2.x or vec2[1]
    local vec2Y = vec2.y or vec2[2]
    return vec1X*vec2Y - vec2X*vec1Y
end

function Add(pt1,pt2)
    return {x = pt1.x + pt2.x , y = pt1.y + pt2.y }
end

function Sub(pt1,pt2)
    return {x = pt1.x - pt2.x , y = pt1.y - pt2.y }
end

-- 计算圆周上的点位置
-- 返回的是通过圆心与水平夹角为angleRadians度的直径在圆上的两个交点
function CalCirclePos(centerPos, radius, angleRadians)
    return Add(centerPos, {x=math.cos(angleRadians)*radius, y=math.sin(angleRadians)*radius}),Sub(centerPos, {x=math.cos(angleRadians)*radius, y=math.sin(angleRadians)*radius})
end

---将角度转换为逆时针角度
---rotation (0 ~ 180逆时针,0 ~ -180顺时针)
function changeRotationToInverse(rotation)
    rotation = rotation or 0
    if rotation < 0 then
        rotation = rotation + 360
    end
    return rotation or 0
end
(2)创建多边形
--创建一个多边形
--name:多边形名字
--offset:实际点与多边形的偏移量,offset为多边形的原点,多边形的顶点位置都是相对于这个点的偏移量
--vertices : 多边形的顶点数组,位置相对于offset
--rotation : 旋转角度(角度不是弧度(0~180为逆时针,0~-180为顺时针) 
function Polyon(name,offset,vertices,rotation)
    local polyon = {}
    polyon.name = name or "polyon"
    polyon.offset = {offset.x or offset[1] or 0,offset.y or offset[2] or 0}
    -- 弧度
    polyon.rotation = math.rad(changeRotationToInverse(rotation))
    --- 模板顶点,相对于offset为原点的顶点数组
    polyon._tempVertices = {}
    for i, vertice in ipairs(vertices) do
        local x = vertices[i][1]
        local y = vertices[i][2]
        table.insert(polyon._tempVertices,{x=x,y=y})
    end
    --顶点数组,实际顶点坐标
    polyon._vertices = {}
    -- 平面中,一个点(x,y)绕任意点(dx,dy)逆时针旋转a度后的坐标
    -- xx= (x - dx)*cos(a) - (y - dy)*sin(a) + dx ;
    -- yy= (x - dx)*sin(a) + (y - dy)*cos(a) +dy ;
    for i, vertice in ipairs(vertices or {}) do
        local x = (vertices[i][1]*math.cos(polyon.rotation))-(vertices[i][2]*math.sin(polyon.rotation))
        local y = (vertices[i][1]*math.sin(polyon.rotation))+(vertices[i][2]*math.cos(polyon.rotation))
        table.insert(polyon._vertices,{x=x,y=y})
    end

    ---边
    polyon._edges = {}
    for i=1,#polyon._vertices do
        table.insert(polyon._edges, createSegment(polyon._vertices[i], polyon._vertices[1+i%(#polyon._vertices)]))
    end

    polyon.centerPoint = {x=0,y=0}

    --- 注册点到中心点的距离
    polyon._centerToAnchorDistance = GetDis({0,0},polyon.offset)
    --- 中心点相对于注册点的旋转弧度
    polyon._centerToAnchorRadian = math.atan2(polyon.offset[2], polyon.offset[1])

    return polyon
end

---多边形的边
---@param vertice1 第一个顶点
---@param vertice2 第二个顶点
function createSegment(vertice1,vertice2)
    -- dir为多边形的边的向量
    local segment = {pointA = vertice1,pointB = vertice2,dir = {vertice2.x - vertice1.x,vertice2.y - vertice1.y}}
    return segment
end

-- 打印多边形
function PrintPolygon(polyon)
    print(polyon.name.." offset ",polyon.offset[1],polyon.offset[2])
    print(polyon.name.." centerPoint ",polyon.centerPoint.x,polyon.centerPoint.y)
    print(polyon.name.."模板顶点信息:===========================================")
    for index, value in ipairs(polyon._tempVertices) do
        print(string.format("x = %f, y = %f",polyon._tempVertices[index].x,polyon._tempVertices[index].y))
    end
    print(polyon.name.."模板顶点信息:===========================================")
    print(polyon.name.."实际顶点信息:===========================================")
    for index, value in ipairs(polyon._vertices) do
        print(string.format("x = %f, y = %f",polyon._vertices[index].x,polyon._vertices[index].y))
    end
    print(polyon.name.."实际顶点信息:=======================================")
    print(polyon.name.."边信息:===========================================")
    for index, value in ipairs(polyon._edges) do
        print(string.format("dir = (%f,%f)",polyon._edges[index].dir[1],polyon._edges[index].dir[2]))
    end
    print(polyon.name.."边信息:=======================================")
end
多边形offset.png

上图为两个顶点相同,offset不同的两个多边形在(0,0)点位置。

(3)设置多边形
---设置多边形的实际位置,更新多边形的信息
---polyon 多边形
---x 碰撞体的实际位置X
---y 碰撞体的实际位置y
---rotation 是角度不是弧度
function setPolyon(polyon, x, y, rotation)
    rotation = changeRotationToInverse(rotation) or 0
    local r = math.rad(rotation)
    polyon.rotation = r
    ---相对于世界坐标系旋转的弧度
    local radian = polyon._centerToAnchorRadian + r
    local dx = polyon._centerToAnchorDistance * math.cos(radian)
    local dy = polyon._centerToAnchorDistance * math.sin(radian)
    ---中心点(offset点)的世界坐标
    polyon.centerPoint.x = x + dx
    polyon.centerPoint.y = y + dy

    ---更新多边形顶点位置(相对于世界坐标的)
    for i, vertice in ipairs(polyon._vertices) do
        local x = polyon._tempVertices[i].x
        local y = polyon._tempVertices[i].y
        -- 平面中,一个点(x,y)绕任意点(dx,dy)逆时针旋转a度后的坐标
        -- xx= (x - dx)*cos(a) - (y - dy)*sin(a) + dx ;
        -- yy= (x - dx)*sin(a) + (y - dy)*cos(a) +dy ;
        polyon._vertices[i].x = polyon.centerPoint.x + (x*math.cos(polyon.rotation))-(y*math.sin(polyon.rotation))
        polyon._vertices[i].y = polyon.centerPoint.y + (x*math.sin(polyon.rotation))+(y*math.cos(polyon.rotation))
    end

    ---更新边的信息
    for i=1,#polyon._vertices do
        local pointA = polyon._vertices[i]
        local pointB = polyon._vertices[1+i%(#polyon._vertices)]
        polyon._edges[i].pointA = pointA
        polyon._edges[i].pointB = pointB
        polyon._edges[i].dir[1] = pointB.x - pointA.x
        polyon._edges[i].dir[2] = pointB.y - pointA.y
    end
end

先计算中心点相对于实际坐标(x,y)的位置,计算各个顶点在多边形旋转以后与中心点的相对位置,计算出各个顶点的实际坐标点,通过各个顶点的实际坐标再更新多边形的边的信息

(5)计算多边形碰撞
---polygonA 多边形
---polygonB 多边形
---分离轴,以多边形的每条边的法向量为轴,将多边形投影到轴上
---如有任意一个轴上的两个多边形的投影不相交,那么这两个多边形就是分离的
function detectorPolygonvsPolygon(polygonA, polygonB)
    local aProjection = {}
    local bProjection = {}
    local segmentNormal  = {}
    --遍历多边形的边,以每条边的法线向量作为两个多边形计算投影的投影轴
    for i, segment in ipairs(polygonA._edges) do
        ---边的法线向量
        segmentNormal[1] = segment.dir[2]
        segmentNormal[2] = -segment.dir[1]
        ---两个多边形在当前轴上的投影
        aProjection[1],aProjection[2] = getProjectionWithAxis(polygonA,segmentNormal)
        bProjection[1],bProjection[2] = getProjectionWithAxis(polygonB,segmentNormal)
        
        if not projectionContains(aProjection,bProjection) then
            return false
        end
    end

    for i, segment in ipairs(polygonB._edges) do
        ---边的法线向量
        segmentNormal[1] = segment.dir[2]
        segmentNormal[1] = -segment.dir[1]
        ---两个多边形在当前轴上的投影
        aProjection[1],aProjection[2] = getProjectionWithAxis(polygonA,segmentNormal)
        bProjection[1],bProjection[2] = getProjectionWithAxis(polygonB,segmentNormal)
        if not projectionContains(aProjection,bProjection) then
            return false
        end
    end
    return true
end

---计算多边形在轴上的投影
---polyon 多边形
---axis 投影的轴
---返回值为投影两端的最大值与最小值
function getProjectionWithAxis(polyon,axis)
    --将投影轴归一化
    Normalize(axis)
    ---在轴上面的投影
    local min = Dot(polyon._vertices[1],axis)
    local max = min
    for i,v in ipairs(polyon._vertices) do
        local proj =  Dot(v, axis)
        if proj < min then
             min = proj
        end
        if proj > max then 
            max = proj 
        end
    end
    --返回最小值与最大值,保留小数点后三位
    return FloatAccurateN(min,3),FloatAccurateN(max,3)
end

---判断两条投影轴是否相交
function projectionContains(a,b)
    local aMin = a[1]
    local aMax = a[2]
    local bMin = b[1]
    local bMax = b[2]
    --确保a,b第一个元素小于第二个元素
    if aMax < aMin then 
        aMin = aMax
        aMax = a[1]
    end
    if bMax < bMin then 
        bMin = bMax
        bMax = b[1]
    end
    return not (aMin > bMax or aMax < bMin)
end

两个多边形分别遍历他们的边,取边的法线向量作为投影轴,将两个多边形分别投影到投影轴上,如果两个多边形的投影不相交,那么两个多边形则没有碰撞。如果所有的边的投影都相交的话,那么两个多边形则发生了碰撞。

(6)计算多边形碰撞测试数据
local polyonA = Polyon("polyonA",{1,1},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,0)
local polyonB = Polyon("polyonB",{1,1},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonB,2,0,0)
print("多边形A,B碰撞 : ",detectorPolygonvsPolygon(polyonA,polyonB))

多边形A,B碰撞 :  true
[Finished in 0.1s]
平移碰撞1.png
local polyonA = Polyon("polyonA",{0,0},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,60)
local polyonB = Polyon("polyonB",{0,0},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonB,2.5,0,-30)
print("多边形A,B碰撞 : ",detectorPolygonvsPolygon(polyonA,polyonB))

多边形A,B碰撞 :  false
[Finished in 0.1s]
旋转检测.png
2.2、多边形与圆
(1)创建圆
---offset:圆心相对于实际位置的偏移点
---radius:圆的半径
function Circle(offset,radius)
    local Circle = {}
    Circle.radius = radius or 0
    Circle.offset = {x=offset.x and offset[1] or 0, y=offset.y and offset[2] or 0}
    Circle.centerPoint = {x=0,y=0}
    return Circle
end
(2)设置圆的位置
function CircleSet(circle,x,y,radius)
    ---圆心的实际位置
    circle.centerPoint.x = x + circle.offset.x
    circle.centerPoint.y = y + circle.offset.y
    circle.radius = radius or circle.radius
end
(3)圆与多边形的碰撞
---计算圆形在轴上的投影
---axis 投影的轴
---返回投影两端的最大值与最小值
function getClicleProjectionWithAxis(circle,axis)
    Normalize(axis)
    ---线段的夹角
    local rad = math.atan2(axis.y, axis.x)
    ---经过圆心,线段与圆相交的两个点的位置
    local pointInCircle1,pointInCircle2 = CalCirclePos(circle.centerPoint,circle.radius,rad)
    ---分别两个点在轴上的投影
    local min = Dot(pointInCircle1,axis)
    local max = min
    local min2 = Dot(pointInCircle2,axis)
    if min2 < min then min = min2 end
    if min2 > max then max = min2 end
    return FloatAccurateN(min,3),FloatAccurateN(max,3)
end

---polygon 多边形
---circle 圆形
---原理与多边形判断一样
function detectorPolygonvsCircle(polygon,circle)
    local aProjection = {}
    local bProjection = {}
    local circleCenter = circle.centerPoint
    for i, segment in ipairs(polygon._edges) do
        ---顶点与圆心的连线(轴)
        local axes = {segment.dir[2],-segment.dir[1]}
        ---多边形在当前轴上的投影
        aProjection[1],aProjection[2] = getProjectionWithAxis(polygon,axes)
        ---圆在当前轴上的投影
        bProjection[1],bProjection[2] = getClicleProjectionWithAxis(circle,axes)
        if not projectionContains(aProjection,bProjection) then
            return false
        end
    end
    return true
end
(4)多边形与圆碰撞测试数据
local polyonA = Polyon("polyonA",{1,1},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,0)
local circleA = Circle({0,0},1)
CircleSet(circleA,2,0,1)
print("多边形A与圆B碰撞 : ",detectorPolygonvsCircle(polyonA,circleA))

多边形A与圆B碰撞 :     true
[Finished in 0.0s]
多边形与圆平移.png
local polyonA = Polyon("polyonA",{0,0},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,-45)
local circleA = Circle({0,0},1)
CircleSet(circleA,2,0,1)
print("多边形A与圆B碰撞 : ",detectorPolygonvsCircle(polyonA,circleA))

多边形A与圆B碰撞 :     false
[Finished in 0.1s]
多边形与圆的旋转平移.png
2.3、多边形与点
(1) 实现
---多边形与点的碰撞(判断一个点是在多边形内,还是在多边形上,还是在多边形外)
---polygon 多边形
---point 需要检测的点
function detectorPolygonvsPoint(polygon,point)
    local pointX = point.x or point[1]
    local pointY = point.y or point[2]
    local fristvectorproduct = 0
    for i, edge in ipairs(polygon._edges) do
        local vertice = edge.pointA
        ---边的第一个顶点point的向量
        local vertice2point = {pointX - vertice.x,pointY - vertice.y}
        ---边与vertice2point的向量积
        local vectorproduct = FloatAccurateN(VectorProduct(edge.dir,vertice2point),3)
        ---点在多边形的边上
        if vectorproduct == 0 then
            return true
        end
        if i == 1 then
            fristvectorproduct = vectorproduct
        else
            if fristvectorproduct * vectorproduct < 0 then
                return false
            end
        end
    end
    return true
end

多边形可看做从某点出发的闭合回路,内部的点永远在回路的同一边。通过边与点的连线的向量积(叉积)的正负表示方向,顺时针方向,所有向量积数值均为负,逆时针方向,所有向量积数值均为正
多边形与点参考文章

(2)测试数据
local polyonA = Polyon("polyonA",{0,0},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,0)
local pointB = {x=0,y=1}
print("多边形A与点B碰撞 : ",detectorPolygonvsPoint(polyonA,pointB))

多边形A与点B碰撞 :     true
[Finished in 0.0s]
多边形与点.png
local polyonA = Polyon("polyonA",{0,0},{{0,1},{-1,0},{-1,-1},{1,-1},{1,0}},0)
setPolyon(polyonA,0,0,30)
local pointB = {x=0,y=1}
print("多边形A与点B碰撞 : ",detectorPolygonvsPoint(polyonA,pointB))

多边形A与点B碰撞 :     false
[Finished in 0.0s]
多边形与点旋转.png
三、分离轴优缺点

优点:
速度快,算法简单易懂,过程只需要简单的向量运算,只要有一个轴满足不相交,就可以停止检测。
缺点:
只能用于检测凸多边形。只能知道多边形发生了碰撞,但是并不能知道是碰撞的具体信息(碰撞位置),但是可以间接的知道碰撞的深度。

代码下载
提取码: ffpv

相关文章

网友评论

      本文标题:多边形碰撞检测(分离轴算法Lua实现)

      本文链接:https://www.haomeiwen.com/subject/rmhvgctx.html