– 参考链接
https://www.cnblogs.com/vana/p/10948789.html
https://blog.csdn.net/qq276592716/article/details/45999831
https://blog.csdn.net/x2345com/article/details/103091899
需求前提
当游戏运行中会不断进行大量的碰撞检测,需要适合的碰撞检测方案,所以在网上搜索了一下资料。下面是找到的几种方案。
方案一:循环遍历:
● 在游戏场景运行期间,会每帧调用update(),在update函数中循环遍历场景中所有的物体,通过计算物体中心点的距离来判断物体是否碰撞。
● 试想如果场景中有N个物体,对每两个物体都进行碰撞检测,那时间复杂度就有N^2,效率低。而且实际上一个位于场景左下角的物体与一个位于场景右上角的物体明显不可能发生碰撞,
方案二:使用cocos2dx自带的chipmunk物理引擎:
● 可以在scene添加物理世界,将所有的物体都设置成物理刚体,通过chipmunk的碰撞回调实现碰撞检测
● 之前做游戏遇到过刚体速度太快出现碰撞穿透现象,所以此次不采用此方案。
方案三:四叉树优化碰撞检测:
● 使用四叉树空间索引,减少需要遍历的物体数量,大大减少了计算量。
四叉树原理
● 四叉树是一个每个父节点都具有四个子节点的树状数据结构。将屏幕划分为四个区域,用于区分处于不同位置的物体,四叉树的四个节点正合适表示这四个区域。方便起见将四块区域命名为象限一、二、三、四。
● 将完全处于某一个象限的物体存储在该象限对应的子节点下,当然,也存在跨越多个象限的物体,可以将它们存在父节点中,如下图所示:
● 如果某个象限内的物体的数量过多,它会同样会分裂成四个子象限,以此类推:
四叉树的实现(lua)
● 定义四叉树的结构:
--常量:
--MAX_OBJECTS: 每个节点(象限)所能包含物体的最大数量
--MAX_LEVELS: 四叉树的最大深度
QuadTree.MAX_OBJECTS = 10
QuadTree.MAX_LEVELS = 5
--四叉树节点包含:
--objects: 用于存储物体对象
--nodes: 存储四个子节点
--level: 该节点的深度,根节点的默认深度为0
--bounds: 该节点对应的象限在屏幕上的范围,bounds是一个矩形
function QuadTree:new(bounds, level)
local o = {}
o = setmetatable(o,QuadTree)
o.objects = {}
o.nodes = {}
o.level = level and level or 0
o.bounds = bounds
return o
end```
● 接下来,需要判断屏幕上的物体属于哪个象限:
-- 获取物体对应的象限序号,以屏幕中心为界限,切割屏幕:
-- - 右上:象限一
-- - 左上:象限二
-- - 左下:象限三
-- - 右下:象限四
function QuadTree:getIndex(node)
local rect = node:getBoundingBox()
if not checkbounds(self.bounds, rect) then
return nil
end
local x = self.bounds.x
local y = self.bounds.y
local width = self.bounds.width / 2
local height = self.bounds.height / 2
local quadrant1 = cc.rect(x + width, y + height, width, height)
local quadrant2 = cc.rect(x, y + height, width, height)
local quadrant3 = cc.rect(x, y, width, height)
local quadrant4 = cc.rect(x + width, y, width, height)
if checkbounds(quadrant1, rect) then
return 1
elseif checkbounds(quadrant2, rect) then
return 2
elseif checkbounds(quadrant3, rect) then
return 3
elseif checkbounds(quadrant4, rect) then
return 4
end
--如果物体跨越多个象限,则放回-1
return - 1
end
● 如果某一个象限(节点)内存储的物体数量超过了MAX_OBJECTS最大数量,则需要对这个节点进行划分,所以需要一个划分函数,它的工作就是将一个象限看作一个屏幕,将其划分为四个子象限:
-- 划分
function QuadTree:split()
if #self.nodes > 0 then
return
end
local x = self.bounds.x
local y = self.bounds.y
local width = self.bounds.width / 2
local height = self.bounds.height / 2
local tree1 = QuadTree:new(cc.rect(x + width, y + height, width, height), self.level + 1)
local tree2 = QuadTree:new(cc.rect(x, y + height, width, height), self.level + 1)
local tree3 = QuadTree:new(cc.rect(x, y, width, height), self.level + 1)
local tree4 = QuadTree:new(cc.rect(x + width, y, width, height), self.level + 1)
table.insert(self.nodes, tree1)
table.insert(self.nodes, tree2)
table.insert(self.nodes, tree3)
table.insert(self.nodes, tree4)
end
● 为了初始化四叉树,需要实现四叉树的插入功能,用于将物体插入到四叉树中:
-- 插入功能:
-- - 如果当前节点[ 存在 ]子节点,则检查物体到底属于哪个子节点,如果能匹配到子节点,则将该物体插入到该子节点中
-- - 如果当前节点[ 不存在 ]子节点,将该物体存储在当前节点。随后,检查当前节点的存储数量,如果超过了最大存储数量,则对当前节点进行划分,划分完成后,将当前节点存储的物体重新分配到四个子节点中。
function QuadTree:insert(node)
--如果该节点下存在子节点
print("!!!!!!!!!!!!!!!!!!!!!!!!",tolua.type(node))
if #self.nodes > 0 then
local index = self:getIndex(node)
if index and index ~= - 1 then
self.nodes[index]:insert(node)
return
end
end
--否则存储在当前节点下
table.insert(self.objects, node)
--如果当前节点存储的数量超过了MAX_OBJECTS
if #self.nodes <= 0 and #self.objects > QuadTree.MAX_OBJECTS
and self.level < QuadTree.MAX_LEVELS then
self:split()
for i = #self.objects, 1, - 1 do
local index = self:getIndex(self.objects[i])
if index and index ~= - 1 then
self.nodes[index]:insert(self.objects[i])
table.remove(self.objects, i)
end
end
end
end
● 检索功能,重头戏来了!现在已经能够初始化一个四叉树了,接下来要解决——如何将可能发生碰撞的物体集合选取出来。需要注意的一点:并不是所有物体都恰好完全属于某一个象限的,比如有个物体跨越了象限一和象限二:
● 为了让跨越多个象限的物体也能递归地执行retrive函数,从而找到所有可能碰撞的物体集合,需要让这个物体同时属于这些象限。
● 以矩形6为例,如何让矩形6同时属于象限二和象限三呢?做法是:以象限的边界为切割线,将矩形6切割为两个子矩形。能够确定的是:这两个子矩形分别属于象限二和象限三,所以能用这两个子矩形递归的调用retrive函数,从而找到所有可能碰撞的物体集合。
-- 检索功能:
-- 给出一个物体对象,该函数负责将该物体可能发生碰撞的所有物体选取出来。该函数先查找物体所属的象限,该象限下的物体都是有可能发生碰撞的,然后再递归地查找子象限...
function QuadTree:retrieve(node)
local result = {}
if #self.nodes > 0 then
local index = self:getIndex(node)
if index and index ~= - 1 then
local list = self.nodes[index]:retrieve(node)
for _,value in pairs(list) do
table.insert(result, value)
end
elseif index and index == - 1 then
local x = self.bounds.x
local y = self.bounds.y
local width = self.bounds.width / 2
local height = self.bounds.height / 2
local quadrant1 = cc.rect(x + width, y + height, width, height)
local quadrant2 = cc.rect(x, y + height, width, height)
local quadrant3 = cc.rect(x, y, width, height)
local quadrant4 = cc.rect(x + width, y, width, height)
local rect = node:getBoundingBox()
if checkbounds(quadrant1, rect) then
local list = self.nodes[1]:retrieve(node)
for _,value in pairs(list) do
table.insert(result, value)
end
end
if checkbounds(quadrant2, rect) then
local list = self.nodes[2]:retrieve(node)
for _,value in pairs(list) do
table.insert(result, value)
end
end
if checkbounds(quadrant3, rect) then
local list = self.nodes[3]:retrieve(node)
for _,value in pairs(list) do
table.insert(result, value)
end
end
if checkbounds(quadrant4, rect) then
local list = self.nodes[4]:retrieve(node)
for _,value in pairs(list) do
table.insert(result, value)
end
end
end
end
for _,value in pairs(self.objects) do
table.insert(result, value)
end
return result
end
● 由于屏幕的物体是运行的,前一秒在象限一的物体可能下一秒就跑到象限二了,所以每一帧都需要重新初始化四叉树。这意味着,每16ms就要初始化一次四叉树,这个代价太大,太得不偿失了。实际上,只是部分物体从一个象限跑到另一个象限,而其他物体都是保持在原先象限中,所以只需要重新插入这部分物体即可,从而避免了对所有物体进行插入操作。我们为四叉树增添这部分的功能,其名为动态更新:
--判断矩形是否在象限范围内
function QuadTree:isInner(node, bounds)
local rect = node:getBoundingBox()
return rect.x >= bounds.x and rect.x + rect.width <= bounds.x + bounds.width
and rect.y >= bounds.y and rect.y + rect.height <= bounds.y + bounds.height
end
-- 动态更新:
-- 从根节点深入四叉树,检查四叉树各个节点存储的物体是否依旧属于该节点(象限)的范围之内,如果不属于,则重新插入该物体。
function QuadTree:refresh(root)
root = root or self
for i = #self.objects, 1, - 1 do
local node = self.objects[i]
local index = self:getIndex(node)
if index then
--如果矩形不属于该象限,则将该矩形重新插入
if not self:isInner(node, self.bounds) then
if self ~= root then
root:insert(self.objects[i])
table.remove(self.objects, i)
end
-- 如果矩形属于该象限 且 该象限具有子象限,则
-- 将该矩形安插到子象限中
elseif #self.nodes > 0 then
self.nodes[index]:insert(self.objects[i])
table.remove(self.objects, i)
end
end
end
for i = 1, #self.nodes do
self.nodes[i]:refresh(root)
end
end
四叉树的用法
首先创建一个四叉树:
local quad_tree = QuadTree:new(cc.rect(MAP_LEFT, MAP_BOTTOM, MAP_WIDTH, MAP_HEIGHT))
接下来,需要初始化四叉树,将屏幕上的所有物体都插入到这个四叉树中:
for _, item in ipairs({...}) do
-- 四叉树点击
qutree:insert({...})
end
一棵四叉树已经初始化完成,调用retrive找出每个物体对应的碰撞物体集合,并进行下一步的碰撞检测:
-- 从四叉树中获取筛选的数据
local items = qutree:retrieve(cc.rect(location.x, location.y, 1, 1))
接下来就可以在筛选的items中进行碰撞检测了:
for _, item in ipairs(items) do
.........
.........
.........
end
总结
四叉树的应用前提是游戏内场景有大量节点需要检测碰撞,例如游戏开发中的游戏内大地图检测,子弹碰撞检测等方面。简单来说,四叉树就是一种优化方法,能够帮助我们对元素按区域进行划分,减少检测数量。需要说明的是,四叉树只是一种减少碰撞候选者的算法,利用四叉树得到这些候选元素之后,还需要检测这些元素跟目标元素是否发生碰撞,碰撞算法还需另外实现。
使用四叉树检测碰撞的主要流程为:
1.创建四叉树,区域为整个屏幕,并将树保存为全局变量;
2.插入需要做碰撞检测的目标元素;
3.检索目标元素的待检测碰撞对象,返回碰撞候选元素组;
4.编写碰撞算法检测候选元素跟目标元素是否发生碰撞;
5.清除四叉树中的元素,方便下次继续检测。