A Star 搜索算法 常用于游戏中的路径查找

Posted by Yinode on Tuesday, February 11, 2020

TOC

非常酷的一个启发式搜索算法,整体类似于dij最短路径算法,只不过加入了预估距离的概念,当然由于博客数据丢失,之前的博客内容没了,不过还好代码还在,大家凑活看看吧。

// 使用了 p5.js
// 基本配置
const GameConfig = {
  col: 25,
  row: 25,
  width: 600,
  height: 600
}
// 主矩阵
let gameGrid
// 所有还未确定最短路径的集合
let openSet = []
// 所有已经确定最短路径的集合
let closedSet = []
// 当前的路径集合
let pathSet = []

// 起点
let start = null
// 终点
let end = null

// 启发式距离预估函数
// 常用的有 曼哈顿距离 欧几里得距离
// 这里采用欧几里得距离
function heuristic(a, b) {
  return dist(a.x, a.y, b.x, b.y)
  // return Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
}

// 矩阵节点类
class Spot {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    // 距离起点的实际距离
    this.g = 0
    // 距离终点的预估距离
    this.h = 0
    // 综合距离 通过 g + h
    this.f = 0
    this.width = width
    this.height = height
    this.prev = null
    // 所有邻接边
    this.neighBors = []
    this.isWall = Math.random() < 0.2 ? true : false
  }

  draw(_color) {
    if (this.isWall) {
      _color = color(0)
    }
    fill(_color)
    noStroke(0)
    rect(
      this.x * this.width,
      this.y * this.height,
      this.width - 1,
      this.height - 1
    )
  }

  // 记录所有邻接边
  addNeighbors(grid, col, row) {
    const { x, y } = this
    // RIGHT
    if (this.x < col - 1) {
      this.neighBors.push(grid[x + 1][y])
    }
    // LEFT
    if (this.x > 0) {
      this.neighBors.push(grid[x - 1][y])
    }
    // TOP
    if (this.y < row - 1) {
      this.neighBors.push(grid[x][y + 1])
    }
    // BOT
    if (this.y > 0) {
      this.neighBors.push(grid[x][y - 1])
    }
    // LEFT TOP
    if (this.x > 0 && this.y > 0) {
      this.neighBors.push(grid[x - 1][y - 1])
    }
    // RIGHT BOT
    if (this.x < col - 1 && this.y < row - 1) {
      this.neighBors.push(grid[x + 1][y + 1])
    }
  }
}

class GameGrid {
  constructor(col, row, width, height) {
    this._grid = []
    this.col = col
    this.row = row

    let spWidth = width / col
    let spHeight = height / row
    for (let i = 0; i < col; i++) {
      this._grid[i] = []
      for (let j = 0; j < row; j++) {
        this._grid[i][j] = new Spot(i, j, spWidth, spHeight)
      }
    }

    this.forEach(v => {
      v.addNeighbors(this._grid, this.col, this.row)
    })
  }

  get(i, j) {
    return this._grid[i][j]
  }

  forEach(callback) {
    for (let i = 0; i < this.col; i++) {
      for (let j = 0; j < this.row; j++) {
        callback(this._grid[i][j])
      }
    }
  }

  draw() {
    this.forEach(item => {
      item.draw(color(255))
    })
  }
}

function setup() {
  createCanvas(GameConfig.width, GameConfig.height)

  gameGrid = new GameGrid(
    GameConfig.col,
    GameConfig.row,
    GameConfig.width,
    GameConfig.height
  )

  start = gameGrid.get(0, 0)
  end = gameGrid.get(GameConfig.col - 1, GameConfig.row - 1)
  start.isWall = false
  end.isWall = false

  openSet.push(start)
}

function draw() {
  let winner = null
  if (openSet.length > 0) {
    // 找到 openSet 中 F 最小的节点
    let winnerIndex = 0
    openSet.forEach((item, index) => {
      if (item.f < openSet[winnerIndex].f) {
        winnerIndex = index
      }
    })
    
    winner = openSet[winnerIndex]
    
    // 如果这个winner正好等于终点 结束
    if (winner === end) {
      console.log('END!')
      console.log(end)
      noLoop()
    }
    
    // 将这个节点放入到 closedSet 中
    openSet.splice(winnerIndex, 1)
    closedSet.push(winner)

    // 遍历该节点所有的邻接边
    // 更新他们的预估最短距离
    winner.neighBors.forEach(n => {
      // 该节点必须还没有被确定 && 不属于墙
      if (!closedSet.includes(n) && !n.isWall) {
        let tempG = winner.g + 1
        let newPath = false
        if (openSet.includes(n)) {
          // 为这个节点找到了一条更短的路径
          if (tempG < n.g) {
            n.g = tempG
            newPath = true
          }
        } else {
          // 该节点属于新节点 放入到  openSet 中
          n.g = tempG
          openSet.push(n)
          newPath = true
        }
        
        // 如果路径有更新 更新 综合距离
        if (newPath) {
          n.h = heuristic(n, end)
          n.f = n.h + n.g
          n.prev = winner
        }
      }
    })
  } else {
    console.log('i cant')
    noLoop()
  }
  background(0)

  gameGrid.draw()

  openSet.forEach(item => {
    item.draw(color(0, 255, 0))
  })

  closedSet.forEach(item => {
    item.draw(color(255, 0, 0))
  })

  pathSet = []
  let prev = winner
  while (prev) {
    pathSet.push(prev)
    prev = prev.prev
  }

  pathSet.forEach(item => {
    item.draw(color(0, 0, 255))
  })

  noFill()
  stroke(255)
  strokeWeight(1)
  beginShape()
  pathSet.forEach(item => {
    const { x, y, width, height } = item
    vertex(x * width + width / 2, y * height + height / 2)
  })
  endShape(0)
}