四叉樹優化碰撞檢測
轉自:http://blog.csdn.net/qq276592716/article/details/45999831
遊戲中碰撞檢測分為兩個階段:broad phase 和 narrow phase。接下來要介紹的就是broad phase。在broad phase這個階段,我們的主要任務是將屏幕上的物體進行篩選,篩選出最可能發生碰撞的物體集合。
試想想,屏幕上有N個物體,如果我們對每兩個物體都進行碰撞檢測,那時間復雜度就有N^2。但實際上,在遊戲畫面中,並不是每兩個物體都需要進行碰撞檢測,比如一個在屏幕右上方的物體和一個在屏幕左上方的物體之間明顯是不會發生碰撞的,所以我們不需要對這兩個物體進行碰撞檢測。那麽,現在我們就需要一個這樣的算法去將屏幕上可能和不可能發生碰撞的物體區分開來。
四叉樹原理
正如其名,四叉樹就是每個父節點都具有四個子節點的樹狀數據結構。由於要區分屏幕上的物體,我們要將屏幕劃分為四個區域,所以四叉樹的四個節點正合適表示這四個區域。
屏幕上四個區域分別為:左上區域 + 右上區域 + 右下區域 + 左下區域,方便起見,我們分別命名為:象限1、象限2、象限3、象限4:
我們將完全處於某一個象限的物體存儲在該象限對應的子節點下,當然,也存在跨越多個象限的物體,我們將它們存在父節點中:
如果某個象限內的物體的數量過多,它會同樣會分裂成四個子象限,以此類推:
實現四叉樹
我們先定義四叉樹的結構:
/* 四叉樹節點包含: - objects: 用於存儲物體對象 - nodes: 存儲四個子節點 - level: 該節點的深度,根節點的默認深度為0 - bounds: 該節點對應的象限在屏幕上的範圍,bounds是一個矩形*/ var QuadTree = function QuadTree(bounds, level) { this.objects = []; this.nodes = []; this.level = typeof level === ‘undefined‘ ? 0 : level; this.bounds = bounds; } /* 常量: - MAX_OBJECTS: 每個節點(象限)所能包含物體的最大數量 - MAX_LEVELS: 四叉樹的最大深度 */ QuadTree.prototype.MAX_OBJECTS = 10; QuadTree.prototype.MAX_LEVELS = 5;
接下來,我們需要判斷屏幕上的物體屬於哪個象限:
/* 獲取物體對應的象限序號,以屏幕中心為界限,切割屏幕: - 右上:象限一 - 左上:象限二 - 左下:象限三 - 右下:象限四 */ QuadTree.prototype.getIndex = function(rect) { var bounds = this.bounds, onTop = rect.y + rect.height <= bounds.centroid.y, onBottom = rect.y >= bounds.centroid.y, onLeft = rect.x + rect.w <= bounds.centroid.x, onRight = rect.x >= bounds.centroid.x; if (onTop) { if (onRight) { return 0; } else if (onLeft) { return 1; } } else if (onBottom) { if (onLeft) { return 2; } else if (onRight) { return 3; } } // 如果物體跨越多個象限,則放回-1 return -1; };
我們知道,如果某一個象限(節點)內存儲的物體數量超過了MAX_OBJECTS
最大數量,則需要對這個節點進行劃分,所以我們同樣需要一個劃分函數,它的工作就是將一個象限看作一個屏幕,將其劃分為四個子象限:
// 劃分 QuadTree.prototype.split = function() { var level = this.level, bounds = this.bounds, x = bounds.x, y = bounds.y, sWidth = bounds.width / 2, sHeight = bounds.height / 2; this.nodes.push( new QuadTree(new Rect(bounds.centroid.x, y, sWidth, sHeight), level + 1), new QuadTree(new Rect(x, y, sWidth, sHeight), level + 1), new QuadTree(new Rect(x, bounds.centroid.y, sWidth, sHeight), level + 1), new QuadTree(new Rect(bounds.centroid.x, bounds.centroid.y, sWidth, sHeight), level + 1) ); };
為了初始化四叉樹,我們也需要實現四叉樹的插入功能,用於將物體插入到四叉樹中:
/* 插入功能: - 如果當前節點[ 存在 ]子節點,則檢查物體到底屬於哪個子節點,如果能匹配到子節點,則將該物體插入到該子節點中 - 如果當前節點[ 不存在 ]子節點,將該物體存儲在當前節點。隨後,檢查當前節點的存儲數量,如果超過了最大存儲數量,則對當前節點進行劃分,劃分完成後,將當前節點存儲的物體重新分配到四個子節點中。 */ QuadTree.prototype.insert = function(rect) { var objs = this.objects, i, index; // 如果該節點下存在子節點 if (this.nodes.length) { index = this.getIndex(rect); if (index !== -1) { this.nodes[index].insert(rect); return; } } // 否則存儲在當前節點下 objs.push(rect); // 如果當前節點存儲的數量超過了MAX_OBJECTS if (!this.nodes.length && this.objects.length > this.MAX_OBJECTS && this.level < this.MAX_LEVELS) { this.split(); for (i = objs.length - 1; i >= 0; i--) { index = this.getIndex(objs[i]); if (index !== -1) { this.nodes[index].insert(objs.splice(i, 1)[0]); } } } };
篩選功能
重頭戲來啦!現在我們已經能夠初始化一個四叉樹了,接下來我們要解決——如何將可能發生碰撞的物體集合選取出來:
/* 檢索功能: 給出一個物體對象,該函數負責將該物體可能發生碰撞的所有物體選取出來。該函數先查找物體所屬的象限,該象限下的物體都是有可能發生碰撞的,然後再遞歸地查找子象限... */ QuadTree.prototype.retrieve = function(rect) { var result = [], index; if (this.nodes.length) { index = this.getIndex(rect); if (index !== -1) { resutl = result.concat(this.nodes[index].retrieve(rect)); } } result = result.concat(this.objects); return result; };
咋一看,這個函數貌似沒什麽問題,但是我們知道,並不是所有物體都恰好完全屬於某一個象限的,比如有個物體跨越了象限一和象限二:
一眼就能看出來,矩形6可能發生碰撞的物體集合包括:矩形1、矩形2和矩形5。而我們實現的retrive
函數篩選出來的集合則是為空,顯然是不正確的,我們需要改進這塊代碼。
為了讓跨越多個象限的物體也能遞歸地執行retrive
函數,從而找到所有可能碰撞的物體集合,我們需要讓這個物體同時屬於這些象限。
以矩形6為例,我們如何讓矩形6同時屬於象限二和象限三呢?我們的做法是:以象限的邊界為切割線,將矩形6切割為兩個子矩形。我們能夠確定的是:這兩個子矩形分別屬於象限二和象限三,所以我們能用這兩個子矩形遞歸的調用retrive
函數,從而找到所有可能碰撞的物體集合。
改進後的retrive
函數:
// 檢索 QuadTree.prototype.retrieve = function(rect) { var result = [], arr, i, index; if (this.nodes.length) { index = this.getIndex(rect); if (index !== -1) { resutl = result.concat(this.nodes[index].retrieve(rect)); } else { // 切割矩形 arr = rect.carve(this.bounds); for (i = arr.length - 1; i >= 0; i--) { index = this.getIndex(arr[i]); resutl = result.concat(this.nodes[index].retrieve(rect)); } } } result = result.concat(this.objects); return result; }
動態更新
我們已經實現了四叉樹全部的功能,先介紹一下四叉樹的用法。
首先創建一個四叉樹:
var tree = new Quadtree(new Rect(0, 0, 1000, 500));
接下來,我們需要初始化四叉樹,我們將屏幕上的所有物體都插入到這個四叉樹中:
var rectsArr = [/* ... */]; rectsArr.forEach(function(rect) { tree.insert(rect); });
一棵四叉樹已經初始化完成,我們調用retrive
找出每個物體對應的碰撞物體集合,並進行下一步的narrow phase部分的碰撞檢測了:
rectsArr.forEach(function(rect) { var result = tree.retrive(rect); result.forEach(function() { // norrow phase部分的碰撞檢測... }); })
我們現在知道四叉樹的使用方法了,但同時我們也註意到一個問題:由於屏幕的物體是運行的,前一秒在象限一的物體可能下一秒就跑到象限二了,所以每一幀都需要重新初始化四叉樹。這意味著,每16ms就要初始化一次四叉樹,這個代價太大,太得不償失了:
var run = function run() { // 重新向四叉樹中插入所有物體,重新初始化四叉樹 // ... // 篩選物體集合並進行碰撞檢測 // ... requestAnimationFrame(run); }; requestAnimationFrame(run);
我們想想,是不是有這樣做的必要?實際上,只是部分物體從一個象限跑到另一個象限,而其他物體都是保持在原先象限中,所以我們只需要重新插入這部分物體即可,從而避免了對所有物體進行插入操作。
我們為四叉樹增添這部分的功能,其名為動態更新:
// 判斷矩形是否在象限範圍內 function isInner(rect, bounds) { return rect.x >= bounds.x && rect.x + width <= bounds.x + bounds.width && rect.y >= bounds.y && rect.y + rect.height <= bounds.y + bounds.height; } /* 動態更新: 從根節點深入四叉樹,檢查四叉樹各個節點存儲的物體是否依舊屬於該節點(象限)的範圍之內,如果不屬於,則重新插入該物體。 */ QuadTree.prototype.refresh = function(root) { var objs = this.objects, rect, index, i, len; root = root || this; for (i = objs.length - 1; i >= 0; i--) { rect = objs[i]; index = this.getIndex(rect); // 如果矩形不屬於該象限,則將該矩形重新插入 if (!isInner(rect, this.bounds)) { if (this !== root) { root.insert(objs.splice(i, 1)[0]); } // 如果矩形屬於該象限 且 該象限具有子象限,則 // 將該矩形安插到子象限中 } else if (this.nodes.length) { this.nodes[index].insert(objs.splice(i, 1)[0]); } } // 遞歸刷新子象限 for (i = 0, len = this.nodes.length; i < len; i++) { this.nodes[i].refresh(root); } };
現在有了動態更新功能,每一幀中只需要對該四叉樹進行動態更新即可:
var run = function run() { // 動態更新 tree.refresh(); // 篩選物體集合並進行碰撞檢測 // ... requestAnimationFrame(run); }; requestAnimationFrame(run);
四叉樹優化碰撞檢測