QuadTree四叉樹顧名思義就是樹狀的資料結構,其每個節點有四個孩子節點,可將二維平面遞迴分割子區域。QuadTree常用於空間資料庫索引,3D的椎體可見區域裁剪,甚至圖片分析處理,我們今天介紹的是QuadTree最常被遊戲領域使用到的碰撞檢測。採用QuadTree演算法將大大減少需要測試碰撞的次數,從而提高遊戲重新整理效能,本文例子基於HT for Web的圖形引擎,通過GraphView和Graph3dView共享同一資料模型DataModel,同時呈現QuadTree演算法下的2D和3D碰撞檢視效果:http://v.youku.com/v_show/id_XODQyNTA1NjY0.html
QuadTree的實現有很多成熟的版本,我選擇的是 https://github.com/timohausmann/quadtree-js/ 四叉樹的演算法很簡單,因此這個開源庫也就兩百來行程式碼。使用也非常簡單,構建一個Quadtree物件,第一個引數傳入rect資訊制定遊戲空間範圍,在每次requestAnimationFrame重新整理幀時,先通過quadtree.clear()清除老資料,通過quadtree.insert(rect)插入新的節點矩形區域,這樣quadtree就初始化好了,剩下就是根據需要呼叫quadtree.retrieve(rect)獲取指定矩形區域下,與其可能相交需要檢測的矩形物件陣列。
我構建了HT的GraphView和Graph3dView兩個元件,通過ht.widget.SplitView左右分割,由於兩個檢視都共享同一DataModel,因此我們剩下的關注點僅是對DataModel的資料操作,構建了200個ht.Node物件,每個物件的attr屬性上儲存了隨機的運動方向vx和vy,同時儲存了將要反覆插入quadtree的矩形物件,這樣避免每幀更新時反覆建立物件,同時矩形物件也引用了ht.Node物件,用來當通過quadtree.retrieve(rect)獲取需要檢測的矩形物件時,我們能指定其所關聯的ht.Node物件,因為我們需要對最終檢測為碰撞的圖元設定上紅顏色的效果,也就是ht.Node平時顯示預設的藍色,當互相碰撞時將改變為紅色。
需要注意從quadtree.retrieve(rect)獲取需要檢測的矩形物件陣列中會包含自身圖元,同時這些僅僅是可能會碰撞的圖元,並不意味著已經碰撞了,由於我們例子是矩形,因此採用ht.Default.intersectsRect(r1, r2)最終判斷是否相交,如果你的例子是圓形則可以採用計算兩個圓心距離是否小於兩個半徑來決定是否相交,因此最終判斷的標準根據遊戲型別會有差異。
採用了QuadTree還是極大了提高了運算效能,否則100個圖元就需要100*100次的監測,我這個例子場景下一般也就100*(10~30)的量:
除了碰撞檢測外QuadTree演算法還有很多有趣的應用領域,有興趣可以玩玩這個 https://github.com/fogleman/Quads
所有程式碼如下供參考:
function init(){ d = 200; speed = 8; dataModel = new ht.DataModel(); g3d = new ht.graph3d.Graph3dView(dataModel); g2d = new ht.graph.GraphView(dataModel); mainSplit = new ht.widget.SplitView(g3d, g2d); mainSplit.addToDOM(); g2d.translate(300, 220); g2d.setZoom(0.8, true); for(var i=0; i<100; i++) { var node = new ht.Node(); node.s3(randMinMax(5, 30), 10, randMinMax(5, 30)); node.p3(randMinMax(-d/2, d/2), 0, randMinMax(-d/2, d/2)); node.s({ 'batch': 'group', 'shape': 'rect', 'shape.border.width': 1, 'shape.border.color': 'white', 'wf.visible': true, 'wf.color': 'white' }); node.a({ vx: randMinMax(-speed, speed), vy: randMinMax(-speed, speed), obj: { width: node.getWidth(), height: node.getHeight(), data: node } }); dataModel.add(node); } createShape([ {x: -d, y: d}, {x: d, y: d}, {x: d, y: -d}, {x: -d, y: -d}, {x: -d, y: d} ]); quadtree = new Quadtree({ x: -d, y: -d, width: d, height: d }); requestAnimationFrame(update); } function update() { quadtree.clear(); dataModel.each(function(data){ if(!(data instanceof ht.Shape)){ var position = data.getPosition(); var vx = data.a('vx'); var vy = data.a('vy'); var w = data.getWidth()/2; var h = data.getHeight()/2; var x = position.x + vx; var y = position.y + vy; if(x - w < -d){ data.a('vx', -vx); x = -d + w; } if(x + w > d){ data.a('vx', -vx); x = d - w; } if(y - h < -d){ data.a('vy', -vy); y = -d + h; } if(y + h > d){ data.a('vy', -vy); y = d - h; } data.setPosition(x, y); var obj = data.a('obj'); obj.x = x - w; obj.y = y - h; quadtree.insert(obj); setColor(data, undefined); } }); dataModel.each(function(data){ if(!(data instanceof ht.Shape)){ var obj = data.a('obj'); var objs = quadtree.retrieve(obj); if(objs.length > 1){ for(var i=0; i<objs.length; i++ ) { var data2 = objs[i].data; if(data === data2){ continue; } if(ht.Default.intersectsRect(obj, data2.a('obj'))){ setColor(data, 'red'); setColor(data2, 'red'); } } } } }); requestAnimationFrame(update); } function randMinMax(min, max) { return min + (Math.random() * (max - min)); } function createShape(points){ shape = new ht.Shape(); shape.setPoints(points); shape.setThickness(4); shape.setTall(10); shape.s({ 'all.color': 'red', 'shape.background': null, 'shape.border.width': 2, 'shape.border.color': 'red' }); dataModel.add(shape); return shape; } function setColor(data, color){ data.s({ 'all.color': color, 'shape.background': color }); }