咱們繼續上一篇,上次咱們分析到world.init的map.init,接下來咱們繼續看地圖初始化中的這句: html
this.pfinder = buildFinder(this);
該方法對應的文件是require('pomelo-pathfinding').buildFinder,這又是一個獨立的模塊,實際上就是實現了一套A*尋路算法。 算法
若是有不熟悉A*算法的朋友,看這一篇可能會比較困難,建議能夠本身先去查一下資料,我這裏也推薦一篇http://wenku.baidu.com/view/8b3c5f232f60ddccda38a068.html; ui
A*算法重要的公式: this
F = G + H
G=從起點A沿着已生成的路徑到一個給定方格的移動開銷。
H=從給定方格到目的方格的估計移動開銷。這種方式常叫作試探,有點困惑人吧。其實之因此叫作試探法是由於這只是一個猜想。在找到路徑以前咱們實際上並不知道實際的距離,由於任何東西都有可能出如今半路上(牆啊,水啊什麼的)。 spa
A*算法過程要點大概是以下的: prototype
1. 將開始節點放入開放列表(開始節點的F和G值都視爲0);
2. 重複一下步驟:
i. 在開放列表中查找具備最小F值的節點,並把查找到的節點做爲當前節點;
ii. 把當前節點從開放列表刪除, 加入到封閉列表;
iii. 對當前節點相鄰的每個節點依次執行如下步驟:
a. 若是該相鄰節點不可通行或者該相鄰節點已經在封閉列表中,則什麼操做也不執行,繼續檢驗下一個節點;
b. 若是該相鄰節點不在開放列表中,則將該節點添加到開放列表中, 並將該相鄰節點的父節點設爲當前節點,同時保存該相鄰節點的G和F值;
c. 若是該相鄰節點在開放列表中, 則判斷若經由當前節點當前節點到達該相鄰節點的G值是否小於原來保存的G值,若小於,則將該相鄰節點的父節點設爲當前節點,並從新設置該相鄰節點的G和F值.
iv. 循環結束條件:
當終點節點被加入到開放列表做爲待檢驗節點時, 表示路徑被找到,此時應終止循環; 或者當開放列表爲空,代表已無能夠添加的新節點,而已檢驗的節點中沒有終點節點則意味着路徑沒法被找到,此時也結束循環
3. 從終點節點開始沿父節點遍歷, 並保存整個遍歷到的節點座標,遍歷所得的節點就是最後獲得的路徑; code
在分析代碼以前,先了解下tile的構成,以前就有提到過,tile就是構成地圖的一個個正方形小切片它的大小是20*20,每個tile包含6個屬性: htm
1. tile.X和tile.Y : 表明tile在地圖上的座標 對象
2. tile.processed: 標示該tile是否已經被計算過距離 隊列
3. tile.prev:記錄到達該tile的前一個tile
4. tile.cost:從起點tile到達當前tile的最短距離
5. tile.heursitic : 從起點tile起,通過當前tile,到達終點tile的距離
buildFinder最終返回的是finder對象,finder對應的代碼以下:
var finder = function (sx,sy,gx,gy) { if(map.getWeight(gx,gy) >= CAN_NOT_MOVE) { return null; } clearTileInfo(); var cmpHeuristic = function (t1,t2) { return t2.heuristic - t1.heuristic; } var queue = createPriorityQueue(cmpHeuristic); var found = false; var ft = getTileInfo(sx,sy); ft.cost = 0; ft.heuristic = 0; queue.enqueue(ft); while(0 < queue.length()) { var footTile = queue.dequeue(); var x = footTile.x; var y = footTile.y; if(x === gx && y === gy) { found = true; break; } if(footTile.processed) { continue; } footTile.processed = true; var processReachable = function (theX, theY, weight) { if(weight >= CAN_NOT_MOVE) { //??? return; } var neighbourTile = getTileInfo(theX, theY); if(neighbourTile.processed) { return; } var costFromSrc = footTile.cost + weight * distance(theX - x, theY - y); if(!neighbourTile.prev || (costFromSrc < neighbourTile.cost)) { neighbourTile.cost = costFromSrc; neighbourTile.prev = footTile; var distToGoal = distance(theX - gx, theY - gy); neighbourTile.heuristic = costFromSrc + distToGoal; queue.enqueue(neighbourTile); } } map.forAllReachable(x,y,processReachable); } if(!found) { return null; } var paths = new Array(); var goalTile = getTileInfo(gx,gy); var t = goalTile; while(t) { paths.push({x:t.x, y:t.y}); t = t.prev; } paths.reverse(); return {paths: paths, cost:goalTile.cost}; }
finder傳進來4個參數,起點的x,y座標,終點的x,y座標,首先是判斷終點是否能夠是障礙物,若是是則返回,而後是clearTileInfo();對應的代碼以下:
var clearTileInfo = function () { tiles.forEach(function (row) { row.forEach(function(o) { if(!o) { return; } o.processed = false; o.prev = null; o.cost = 0; o.heuristic = 0; }); }) }
這個方法很簡單,就是清空地圖tile集合,給他們設置初始值,而後繼續往下看finder的代碼:
var cmpHeuristic = function (t1,t2) { return t2.heuristic - t1.heuristic; } var queue = createPriorityQueue(cmpHeuristic);
這裏的createPriorityQueue(cmpHeuristic);就是前面介紹過的順序隊列,cmpHeuristic就是順序算法。return t2.heuristic - t1.heuristic;即把經由後一個tile點,從起點到終點花費的距離減去前一個tile點的距離,在這個方法的返回值中實現了enqueue、dequeue、length這三個方法,具體的做用在稍後使用到時再說明。繼續看finder接下來的代碼:
var found = false; var ft = getTileInfo(sx,sy); ft.cost = 0; ft.heuristic = 0; queue.enqueue(ft);
這個就是前面提到的A*尋路的
1. 將開始節點放入開放列表(開始節點的F和G值都視爲0);
首先是getTileInfo(sx,sy),做用就是取到起點的tile的信息,它的代碼以下:
var getTileInfo = function (x,y) { assert("number" === typeof(x) && "number" === typeof(y)) var row = tiles[y]; if(!row) { row = new Array; tiles[y] = row; } var tileInfo = row[x]; if (!tileInfo) { tileInfo = { x: x, y: y, processed: false, prev: null, cost: 0, heuristic: 0 } row[x] = tileInfo; } return tileInfo; }
這一段代碼很簡單,就是先取得座標所在的行,若是不存在,則初始化一行,而後再取得所在的格即tile,若是不存在,在初始化一個tile進行填充. tile包含的集合信息有:
{x: x, y: y, processed: false, prev: null, cost: 0, heuristic: 0},
繼續回到剛纔的finder,接下來就是把cost和heuristic初始化爲0, 在調用queue.enqueue(ft);這個方法就是前面createPriorityQueue是返回的對象包含的enqueue方法,咱們來看代碼:
obj.enqueue = function (e) { this.arr.push(e); var idx = this.arr.length - 1; var parentIdx = floor((idx - 1) / 2); while(0 <= parentIdx) { if(cmpPriority(this.arr[idx],this.arr[parentIdx]) <= 0) { break; } var tmp = this.arr[idx] this.arr[idx] = this.arr[parentIdx]; this.arr[parentIdx] = tmp; idx = parentIdx; parentIdx = floor((idx - 1) / 2); } }
enqueue是隊列入列的算法,目前我看的不是很明白,可是通過個人實際演算,它的做用就是把隊列按照距離進行升序排列,哪位大俠看明白了這段算法介紹一下原理吧,謝謝!
接下來繼續看finder:
while(0 < queue.length()) { var footTile = queue.dequeue(); var x = footTile.x; var y = footTile.y; if(x === gx && y === gy) { found = true; break; } if(footTile.processed) { continue; } footTile.processed = true; var processReachable = function (theX, theY, weight) { if(weight >= CAN_NOT_MOVE) { //??? return; } var neighbourTile = getTileInfo(theX, theY); if(neighbourTile.processed) { return; } var costFromSrc = footTile.cost + weight * distance(theX - x, theY - y); if(!neighbourTile.prev || (costFromSrc < neighbourTile.cost)) { neighbourTile.cost = costFromSrc; neighbourTile.prev = footTile; var distToGoal = distance(theX - gx, theY - gy); neighbourTile.heuristic = costFromSrc + distToGoal; queue.enqueue(neighbourTile); } } map.forAllReachable(x,y,processReachable); }
首先是把queue裏第一個元素取出 footTile = queue.dequeue();
這個的做用就是A*尋路要點中的
i. 在開放列表中查找具備最小F值的節點,並把查找到的節點做爲當前節點;
ii. 把當前節點從開放列表刪除, 加入到封閉列表;
它的代碼以下:
obj.dequeue = function () { if(this.arr.length <= 0) { return null; } var max = this.arr[0]; var b = this.arr[this.arr.length - 1]; var idx = 0; this.arr[idx] = b; while(true) { var leftChildIdx = idx * 2 + 1; var rightChildIdx = idx * 2 + 2; var targetPos = idx; if(leftChildIdx < this.arr.length && cmpPriority(this.arr[targetPos], this.arr[leftChildIdx]) < 0) { targetPos = leftChildIdx; } if(rightChildIdx < this.arr.length && cmpPriority(this.arr[targetPos], this.arr[rightChildIdx]) < 0) { targetPos = rightChildIdx; } if(targetPos === idx) { break; } var tmp = this.arr[idx]; this.arr[idx] = this.arr[targetPos]; this.arr[targetPos] = tmp; idx = targetPos; } this.arr.length -= 1; return max; }
這是隊列的出列算法,一樣沒看懂,可是它可以保證每次把距離最短的點取出,而後是finder的接下來這一段:
if(x === gx && y === gy) { found = true; break; } if(footTile.processed) { continue; } footTile.processed = true;
這個的做用就是A*尋路要點中的
iv. 循環結束條件:
當終點節點被加入到開放列表做爲待檢驗節點時, 表示路徑被找到,此時應終止循環; 或者當開放列表爲空,代表已無能夠添加的新節點,而已檢驗的節點中沒有終點節點則意味着路徑沒法被找到,此時也結束循環
而後是finder的這句map.forAllReachable(x,y,processReachable);
Map.prototype.forAllReachable = function(x, y, processReachable) { var x1 = x - 1, x2 = x + 1; var y1 = y - 1, y2 = y + 1; x1 = x1<0?0:x1; y1 = y1<0?0:y1; x2 = x2>=this.rectW?(this.rectW-1):x2; y2 = y2>=this.rectH?(this.rectH-1):y2; if(y > 0) { processReachable(x, y - 1, this.weightMap[x][y - 1]); } if((y + 1) < this.rectH) { processReachable(x, y + 1, this.weightMap[x][y + 1]); } if(x > 0) { processReachable(x - 1, y, this.weightMap[x - 1][y]); } if((x + 1) < this.rectW) { processReachable(x + 1, y, this.weightMap[x + 1][y]); } };
實際上它就是取地圖上當前點的下上左右四個tile分別依次進行4次processReachable計算,processReachable就是A*算法的核心所在,
這個的做用就是A*尋路要點中的
iii. 對當前節點相鄰的每個節點依次執行如下步驟:
a. 若是該相鄰節點不可通行或者該相鄰節點已經在封閉列表中,則什麼操做也不執行,繼續檢驗下一個節點;
b. 若是該相鄰節點不在開放列表中,則將該節點添加到開放列表中, 並將該相鄰節點的父節點設爲當前節點,同時保存該相鄰節點的G和F值;
c. 若是該相鄰節點在開放列表中, 則判斷若經由當前節點當前節點到達該相鄰節點的G值是否小於原來保存的G值,若小於,則將該相鄰節點的父節點設爲當前節點,並從新設置該相鄰節點的G和F值.
代碼以下:
var processReachable = function (theX, theY, weight) { if(weight >= CAN_NOT_MOVE) { //??? return; } var neighbourTile = getTileInfo(theX, theY); if(neighbourTile.processed) { return; } var costFromSrc = footTile.cost + weight * distance(theX - x, theY - y); if(!neighbourTile.prev || (costFromSrc < neighbourTile.cost)) { neighbourTile.cost = costFromSrc; neighbourTile.prev = footTile; var distToGoal = distance(theX - gx, theY - gy); neighbourTile.heuristic = costFromSrc + distToGoal; queue.enqueue(neighbourTile); } }
這裏傳進theX與theY,指的就是上下左右4個點中的某一個tile, 首先判斷該tile是否障礙物,若是是則跳過,不然經過getTileInfo(theX, theY);取出該tile的信息,若是該tile是process過的,則也做爲排除點跳過,同時接下來計算通過上一個tile到本tile的cost,算法爲footTile.cost + weight * distance(theX - x, theY - y);若是結果小於以前已保存的距離,則把當前cost當作這個tile的cost,同時更新heuristic,而後再從新入列.
這一段不知道說清楚了沒有,若是看不明白的同窗先看下A*算法吧,看完了基本上就懂了,接下來咱們看最後一段代碼:
if(!found) { return null; } var paths = new Array(); var goalTile = getTileInfo(gx,gy); var t = goalTile; while(t) { paths.push({x:t.x, y:t.y}); t = t.prev; } paths.reverse(); return {paths: paths, cost:goalTile.cost};
這個很簡單,也是標準的A*算法的最後處理.
這個的做用就是A*尋路要點中的
3. 從終點節點開始沿父節點遍歷, 並保存整個遍歷到的節點座標,遍歷所得的節點就是最後獲得的路徑;
若是沒有發現路徑,結束,而後從目的地倒推,獲得path的全部座標,在paths.reverse();倒序,最後返回path點的集合,以及到達目的地的花銷。
好了,這一篇就到這裏,你們88