碰撞检测方案(四叉树)
2018-08-04 本文已影响268人
LofterAltair
最近公司要做一个类似于球球大作战的项目,游戏运行中会不断进行大量的碰撞检测,需要适合的碰撞检测方案,所以在网上搜索了一下资料。下面是找到的几种方案。
方案一:循环遍历:
- 在游戏场景运行期间,会每帧调用update(),在update函数中循环遍历场景中所有的物体,通过计算物体中心点的距离来判断物体是否碰撞。
- 试想如果场景中有N个物体,对每两个物体都进行碰撞检测,那时间复杂度就有N^2,效率低。而且实际上一个位于场景左下角的物体与一个位于场景右上角的物体明显不可能发生碰撞,
方案二:使用cocos2dx自带的chipmunk物理引擎:
- 可以在scene添加物理世界,将所有的物体都设置成物理刚体,通过chipmunk的碰撞回调实现碰撞检测
- 之前做游戏遇到过刚体速度太快出现碰撞穿透现象,所以此次不采用此方案。
方案三:四叉树优化碰撞检测:
- 使用四叉树空间索引,减少需要遍历的物体数量,大大减少了计算量。
说得太多主要还是因为我没有使用过四叉树~ 想尝试一下~
四叉树原理
网上可以查到许多关于四叉树的资料。四叉树是一个每个父节点都具有四个子节点的树状数据结构。我们将屏幕划分为四个区域,用于区分处于不同位置的物体,四叉树的四个节点正合适表示这四个区域。
方便起见将四块区域命名为象限一、二、三、四。
我们将完全处于某一个象限的物体存储在该象限对应的子节点下,当然,也存在跨越多个象限的物体,我们将它们存在父节点中,如下图所示:
image.png如果某个象限内的物体的数量过多,它会同样会分裂成四个子象限,以此类推:
image.pnglua实现四叉树
local QuadTree = {}
QuadTree.__index = QuadTree
QuadTree.MAX_OBJECTS = 10
QuadTree.MAX_LEVELS = 5
local function checkbounds(quadrant, box)
local list = {
cc.p(box.x + box.width, box.y + box.height),
cc.p(box.x + box.width, box.y + box.height),
cc.p(box.x + box.width, box.y + box.height),
cc.p(box.x + box.width, box.y + box.height),
}
for _, pos in pairs(list) do
if not (pos.x >= quadrant.x and pos.x <= quadrant.x + quadrant.width and pos.y >= quadrant.y and pos.y <= quadrant.y + quadrant.height) then
return false
end
end
return true
end
--bounds 屏幕范围
--level 四叉树层级
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
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
-- 检索功能:
-- 给出一个物体对象,该函数负责将该物体可能发生碰撞的所有物体选取出来。该函数先查找物体所属的象限,该象限下的物体都是有可能发生碰撞的,然后再递归地查找子象限...
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
--判断矩形是否在象限范围内
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
return QuadTree
后期实际运用到项目里面的时候,发现还缺少了移除节点,以及清除整个四叉树的接口
四叉树中object的结构我存的是CCNODE
--移除四叉树节点中的object
function QuadTree:remove(removeNode)
for i = #self.objects, 1, - 1 do
local node = self.objects[i]
if node and node:getTag() == removeNode:getTag() then
table.remove(self.objects, i)
return true
end
end
for i = 1, #self.nodes do
if self.nodes[i]:remove(removeNode) then
return true
end
end
return false
end
--清理四叉树
function QuadTree:clear()
for i = #self.objects, 1, - 1 do
table.remove(self.objects,i)
end
self.objects = {}
for i = 1, #self.nodes do
self.nodes[i]:clear()
end
end