Lordofpomelo源碼分析 (三):World初始化之buildFinder

咱們繼續上一篇,上次咱們分析到world.init的map.init,接下來咱們繼續看地圖初始化中的這句: html

this.pfinder = buildFinder(this);

該方法對應的文件是require('pomelo-pathfinding').buildFinder,這又是一個獨立的模塊,實際上就是實現了一套A*尋路算法。 算法

若是有不熟悉A*算法的朋友,看這一篇可能會比較困難,建議能夠本身先去查一下資料,我這裏也推薦一篇http://wenku.baidu.com/view/8b3c5f232f60ddccda38a068.htmlui

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

相關文章
相關標籤/搜索