我们继续上一篇,上次我们分析到world.init的map.init,接下来我们继续看地图初始化中的这句:
this.pfinder = buildFinder(this);
该方法对应的文件是require('pomelo-pathfinding').buildFinder,这又是一个独立的模块,实际上就是实现了一套A*寻路算法。
如果有不熟悉A*算法的朋友,看这一篇可能会比较困难,建议可以自己先去查一下资料,我这里也推荐一篇http://wenku.baidu.com/view/8b3c5f232f60ddccda38a068.html;
A*算法重要的公式:
F = G + H
G=从起点A沿着已生成的路径到一个给定方格的移动开销。
H=从给定方格到目的方格的估计移动开销。这种方式常叫做试探,有点困惑人吧。其实之所以叫做试探法是因为这只是一个猜测。在找到路径之前我们实际上并不知道实际的距离,因为任何东西都有可能出现在半路上(墙啊,水啊什么的)。
A*算法过程要点大概是如下的:
1. 将开始节点放入开放列表(开始节点的F和G值都视为0);
2. 重复一下步骤:
i. 在开放列表中查找具有最小F值的节点,并把查找到的节点作为当前节点;
ii. 把当前节点从开放列表删除, 加入到封闭列表;
iii. 对当前节点相邻的每一个节点依次执行以下步骤:
a. 如果该相邻节点不可通行或者该相邻节点已经在封闭列表中,则什么操作也不执行,继续检验下一个节点;
b. 如果该相邻节点不在开放列表中,则将该节点添加到开放列表中, 并将该相邻节点的父节点设为当前节点,同时保存该相邻节点的G和F值;
c. 如果该相邻节点在开放列表中, 则判断若经由当前节点当前节点到达该相邻节点的G值是否小于原来保存的G值,若小于,则将该相邻节点的父节点设为当前节点,并重新设置该相邻节点的G和F值.
iv. 循环结束条件:
当终点节点被加入到开放列表作为待检验节点时, 表示路径被找到,此时应终止循环; 或者当开放列表为空,表明已无可以添加的新节点,而已检验的节点中没有终点节点则意味着路径无法被找到,此时也结束循环
3. 从终点节点开始沿父节点遍历, 并保存整个遍历到的节点坐标,遍历所得的节点就是最后得到的路径;
在分析代码之前,先了解下tile的构成,之前就有提到过,tile就是构成地图的一个个正方形小切片它的大小是20*20,每一个tile包含6个属性:
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