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

我们继续上一篇,上次我们分析到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

你可能感兴趣的:(nodejs,pomelo,网游服务端)