1. 程式人生 > >javascript實現A*尋路演算法

javascript實現A*尋路演算法

A*尋路演算法是遊戲中經常用到的一種自動路徑計算演算法,比如連連看、NPC自動巡邏等等。本文章預設使用者已經熟悉A*尋路演算法演算法,不熟悉的可參閱下面連結的文章:

先來看看效果圖:

測試結果:0表示空地,1表示牆壁,+表示起點和終點,*表示路徑

Node

定義尋路過程中的點物件

var Node = function (x, y) {
  this.x = x;
  this.y = y;
  this.g = 0; // 到起點的長度
  this.h = 0; // 到終點的長度
  this.p = null; // 父節點
}

heapMin

定義一個最小堆,便於後面尋路中用到的OpenSet。最小衡量值為F = G + H,即點的g+h和。

/**
 * 實現一個最小堆,方便從OpenSet中選擇最小代價的點
 */
var heapMin = function () {
  this.set = [];
}

heapMin.prototype.adjust = function (index) {
  let len = this.set.length,
      l = index * 2 + 1,
      r = index * 2 + 2,
      min = index,
      node = null;

  if (l <= len-1 && this.set[min].g + this
.set[min].h > this.set[l].g + this.set[l].h) { min = l; } if (r <= len-1 && this.set[min].g + this.set[min].h > this.set[r].g + this.set[r].h) { min = r; } // 如果min發生改變,則需要進行交換,並繼續遞迴min子樹 if (min != index){ node = this.set[min]; this.set[min] = this
.set[index]; this.set[index] = node; this.adjust(min); } } /** * 向最小堆中新增一個元素 */ heapMin.prototype.push = function(node) { // 新增到陣列尾部 this.set.push(node); // 調整堆 for (let i = Math.floor(this.set.length / 2) - 1; i >= 0 ; i--) { this.adjust(i); } } /** * 從最小堆中移除頂部元素 */ heapMin.prototype.pop = function () { // 移除頂部元素,為最小元素 let node = this.set.shift(); // 調整堆 this.adjust(0); return node; } /** * 檢查堆是否為空 */ heapMin.prototype.empty = function () { return this.set.length > 0 ? false : true; } /** * 檢查堆是否包含指定元素 */ heapMin.prototype.contain = function (node) { for (let len = this.set.length, i = 0; i < len; i++) { if (this.set[i].x === node.x && this.set[i].y === node.y) return true; } return false; }

Set

使用者CloseSet

/**
 * Set類
 */
function Set(){
  this.set = [];
}

Set.prototype.push = function (node) {
  this.set.push(node);
}

Set.prototype.pop = function () {
  return this.set.pop();
}

Set.prototype.contain = function (node) {
  for (let len = this.set.length, i = 0; i < len; i++) {
    if (this.set[i].x === node.x && this.set[i].y === node.y) return true;// 存在
  }
  return false;
}

AStarPathFinding

尋路演算法物件

/**
 * AStarPathFinding
 * 
 * W:地圖的寬度
 * H:地圖的高度
 * map:地圖陣列,0表示可以通過,1表示不可以通過
 *
 */
function AStarPathFinding (W, H, map) {
  this.W = W;// 地圖的寬度
  this.H = H;// 地圖的高度
  this.map = map;// 地圖
}

/**
 * 計算距離
 *
 * 採用曼哈頓估量演算法
 */
function calcDistance (startNode, endNode) {
  return (startNode && endNode) ?
          Math.abs(startNode.x - endNode.x) + Math.abs(startNode.y - endNode.y)
          :
          -1;
}

/**
 * 查詢鄰居點
 *
 * 返回鄰居點陣列
 */
AStarPathFinding.prototype.getNeighbors = function (node) {
  let arr = [],
      x,y;

  for (let i = -1; i < 2; i++) {
    for (let j = -1; j < 2; j++) {
      if ((i == 0 && j == 0) || (i === j) || (i === -j)) continue;
      x = node.x + i;
      y = node.y + j;
      if (x < this.W && x > -1 && y < this.H && y > -1) {
        arr.push(new Node(x, y));
      }
    }
  }

  return arr;
}

/**
 * 查詢路徑
 *
 * 1 初始化起始點,計算g,h
 * 2 將起始點加入到OpenSet中
 * 3 當OpenSet不為空的時候,進入迴圈
 * 3.1 從OpenSet中取出最小點,設為當前點
 * 3.2 迴圈遍歷當前點的鄰居點
 * 3.2.1 如果鄰居點不可通過(為牆壁)或者已經在CloseSet中,就略過continue
 * 3.2.2 如果不在OpenSet中,計算FGH數值,並加入到CloseSet的尾部
 * 3.3 迴圈遍歷鄰居點結束
 * 4 OpenSet迴圈結束
 */
AStarPathFinding.prototype.findPath = function (startNode, endNode){
  let OpenSet = new heapMin(),
      CloseSet = new Set(),
      curNode = null;

  OpenSet.push(startNode);

  // 迴圈遍歷OpenSet直到為空
  while (!OpenSet.empty()) {
    curNode = OpenSet.pop();
    CloseSet.push(curNode);

    if (curNode.x === endNode.x && curNode.y === endNode.y) { return CloseSet.set;}

    let arr = this.getNeighbors(curNode);

    for (let i = arr.length - 1; i >= 0; i--) {
      if (this.map[ arr[i].y ][ arr[i].x ] === 1 || CloseSet.contain(arr[i])) continue;

      // 不存在,加入到OpenSet集合
      if (!OpenSet.contain(arr[i])) {
        arr[i].g = calcDistance(arr[i], startNode);
        arr[i].h = calcDistance(arr[i], endNode);
        // 更新父節點,便於之後路徑查詢
        arr[i].p = curNode;
        OpenSet.push(arr[i]);
      }

    }

  }

  return null;

}

/**
 * 列印尋路結果地圖
 */
AStarPathFinding.prototype.printMap = function(s, e){
  if (s.x < 0 || s.x > this.W-1 || s.y < 0 || s.y > this.H-1 || e.x < 0 || e.x > this.W-1 || e.y < 0 || e.y > this.H-1)
    return;
  let arr = this.findPath(s, e);

  if (arr == null) {
    console.log('Not found Path...');
    return;
  } 

  let map = this.map.slice(),
      node = arr.pop();

  while(node !== null) {
    map[node.y][node.x] = ((s.x === node.x && s.y === node.y ) || (e.x === node.x && e.y === node.y )) ? '+' : '*';
    node = node.p;
  }

  for (let i = 0; i < this.H; i++) {
    let temp = [];
    for (let j = 0; j < this.W; j++) {
      temp[j] = map[i][j];
    }
    document.write(temp.join(' ') + '<br />');
  }

}

測試

// 定義地圖陣列 0表示可以通過,1表示不可以通過
let map = [//0 1 2 3 4 5 6 7
            [0,0,0,0,1,0,0,0],//0
            [0,0,1,1,1,0,0,0],//1
            [0,0,0,0,1,0,0,0],//2
            [1,1,1,0,1,0,0,0],//3
            [0,0,0,0,1,0,0,0],//4
            [0,0,1,1,1,0,0,0],//5
            [0,0,1,0,0,0,0,0],//6
            [0,0,0,0,0,0,0,0],//7
          ];

let AStarPathFindingObj = new AStarPathFinding(8, 8, map);

AStarPathFindingObj.printMap(new Node(0, 0), new Node(6, 4));

測試結果

其中:0表示空地,1表示牆壁,+表示起始點,*表示路徑

測試結果:0表示空地,1表示牆壁,+表示起點和終點,*表示路徑

覺得有用,就點個贊吧。如果有問題,歡迎下方留言諮詢!