A* 寻路 +寻路演示(js)

效果

在这里插入图片描述

每个单元格内文字:
(F) (Price)
(G) (H)

原理

原理是参考另一篇csdn博文,不过忘记收藏找不到了

  1. 初始化 open_setclose_set
  2. 起点 加入open_set中,并设置优先级为0(优先级最高)。
  3. 如果open_set不为空,则从open_set中选取优先级最高的节点n
    1. 如果节点n为终点,则:从终点开始逐步追踪prev(前一个)节点,一直达到起点;返回找到的结果路径,算法结束
    2. 如果节点n不是终点,则:将节点nopen_set中删除,并加入close_set中;遍历节点n所有的邻近节点
      1. 如果邻近节点mclose_set中,则:跳过,选取下一个邻近节点;
      2. 如果邻近节点mopen_set中,则:判断节点n到节点mF(n) + cost[n,m] 值是否 <节点mF(m) 。来尝试更新该点,重新设置f值和父节点等数据;
      3. 如果邻近节点m也不在open_set中,则:设置节点mprev为节点n 计算节点m的优先级将节点m加入open_set中。

个人理解

1、不用考虑死胡同情况,不同于普通的按照方向搜索,a* 遍历临近节点,即使碰到死胡同,也不需要回归到非死胡同节点。

代码

直接保存以下为html执行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a*</title>
</head>
<body>
<canvas id="cv" width="1000px" height="1000px"></canvas>
<script>
  /**
   * 输入起始、目标 坐标,返回寻路结果。
   * 每个单元格都有代价值
   */

    //初始点
  let sx, sy;
  //目标点
  let es, ey;
  //网格数量
  let mx = 20, my = 20;
  //格子大小
  let cell_size = 40;
  //初始化网格数据
  let cells = [];
  //不可逾越代价
  let notAllowPrice = 10;
  //代价上限
  let priceLimit = 11;

  //节点对象
  function cell(x, y, price) {
    this.x = x;
    this.y = y;
    //关联的之前单元
    this.prev = null;
    //关联单元格坐标
    this.relation = new Array(8);
    this.relation[0] = y == 0 ? null : [x, y - 1];
    this.relation[1] = (y == 0 || x == mx - 1) ? null : [x + 1, y - 1];
    this.relation[2] = (x == mx - 1) ? null : [x + 1, y];
    this.relation[3] = (x == mx - 1 || y == my - 1) ? null : [x + 1, y + 1];
    this.relation[4] = (y == my - 1) ? null : [x, y + 1];
    this.relation[5] = (x == 0 || y == my - 1) ? null : [x - 1, y + 1];
    this.relation[6] = (x == 0) ? null : [x - 1, y];
    this.relation[7] = (x == 0 || y == 0) ? null : [x - 1, y - 1];
    //通行代价
    this.price = ~~(price);
  }

  //初始化画布
  let el = document.getElementById('cv');
  let context = el.getContext("2d");
  let canvas_width = el.width;
  let canvas_height = el.height;

  //初始化单元数据
  function loadGridData() {
    cells = [];
    for (let i = 0; i < my; i++) {
      for (let j = 0; j < mx; j++) {
        cells.push(new cell(j, i, Math.random() * priceLimit))
      }
    }
  }

  //设置初始点,目标点
  function setStartAndEnd(x0, y0, x1, y1) {
    if (x0 != null && y0 != null && x1 != null && y1 != null) {
      sx = x0;
      sy = y0;
      ex = x1;
      ey = y1;
    } else {
      //设置初始点
      sx = ~~(Math.random() * mx);
      sy = ~~(Math.random() * my);
      // sx = 1;
      // sy = 17;
      //设置目标点
      ex = ~~(Math.random() * mx);
      ey = ~~(Math.random() * my);
    }
    let start = cells[sy * my + sx],
      end = cells[ey * my + ex];

    //确保起始点\终点是可通行
    start.price = 0;
    end.price = 0;

    start.f = 0;
    end.f = 0;
    start.g = 0;
    return {start: start, end: end}
  }

  //绘制画布
  function paint() {
    //清空之前
    context.fillStyle = '#fff';
    context.fillRect(0,0,canvas_width,canvas_height);

    context.stokeStyle = `#999`;
    context.fillStyle = `#000`;
    for (let i = 0, cell; (cell = cells[i]) != null; i++) {
      if (cell.price < notAllowPrice) {
        context.rect(cell.x * cell_size, cell.y * cell_size, cell_size, cell_size)
      } else {
        context.fillRect(cell.x * cell_size, cell.y * cell_size, cell_size, cell_size)
      }
    }
    context.stroke();
  }

  //开始寻路
  async function start(x0,y0,x1,y1) {
    //初始单元数据
    loadGridData();
    //绘制画板
    paint();
    //初始
    let {start, end} = setStartAndEnd(...arguments);

    //绘制起始点
    context.fillStyle = "green";
    context.fillRect(start.x * cell_size, start.y * cell_size, cell_size, cell_size);

    context.fillStyle = "red";
    context.fillRect(end.x * cell_size, end.y * cell_size, cell_size, cell_size);

    console.log('start:', start);
    console.log('end:', end);

    //寻路结果
    let result = [];
    //检测列表
    let open_set = [];
    //关闭列表
    let close_set = [];

    //从起始点开始检测
    open_set.push(start);

    let bestCell;
    while (open_set.length) {
      open_set.sort((a, b) => a.f - b.f);
      bestCell = open_set[0];
      if (bestCell == end) {
        while (bestCell.prev) {
          result.push(bestCell);
          bestCell = bestCell.prev;
        }
        console.log('result:', result);
        break;
      } else {
        close_set.push(open_set.shift());
        let relCell;
        for (let i = 0; i < bestCell.relation.length; i++) {
          relCell = bestCell.relation[i];
          if (relCell) {
            relCell = cells[relCell[1] * my + relCell[0]];
            //临近点不可通过|临近点已被关闭|临界点为夹角点不可到达
            if (relCell.price >= notAllowPrice || close_set.includes(relCell) || !angleAllowCell(bestCell, relCell)) {
              continue;
            }
            if (open_set.includes(relCell)) {
              if (bestCell.f + cost(bestCell, relCell) < relCell.f) {
                relCell.prev = bestCell;
                relCell.g = ~~(relCell.prev.g+cost(relCell.prev,relCell));
                relCell.f = ~~(relCell.g + cost(relCell, end)+relCell.price);
                relCell.h = ~~cost(relCell,end);
              }
              continue
            }
            relCell.prev = bestCell;
            //g的计算方式为 前一个单元 的G + 当前单元与前一个单元的距离
            relCell.g = ~~(relCell.prev.g+cost(relCell.prev,relCell));
            relCell.f = ~~(relCell.g + cost(relCell, end)+relCell.price);
            relCell.h = ~~cost(relCell,end);
            open_set.push(relCell);
            //标记为检测
            await delay(5).then(() => {
              context.fillStyle = "rgba(50,238,255,0.1)";
              context.fillRect(relCell.x * cell_size, relCell.y * cell_size, cell_size, cell_size);
              paintRelArrow(relCell, relCell.prev);
              paintCellText(relCell, relCell.f, 2, 10);//F
              paintCellText(relCell,relCell.g,2,cell_size -2);//G
              paintCellText(relCell,relCell.h,cell_size - 13,cell_size-2);//H
              paintCellText(relCell,relCell.price,cell_size - 10,10);//price
            })
          }
        }
      }
    }
    if(result.length){
      //标记路线
      result.shift();//移出终点
      result.forEach(d => {
        context.fillStyle = "rgba(255,253,114,0.6)";
        context.fillRect(d.x * cell_size, d.y * cell_size, cell_size, cell_size);
      });
    }else{
      console.log('目标不可到达')
    }
  }

  /*================================utils=======================================*/
  /**
   * 计算两点间曼哈顿距离(可以换成欧拉)
   */
  function cost(a, b) {
    return Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
  }

  /**
   * 绘制先后关系指向箭头,由后者指向前者并在后者中标注
   * @param child 子单元
   * @param parent
   */
  function paintRelArrow(after, prev) {
    let directions = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'];
    for (let i = 0; i < 8; i++) {
      if (after.relation[i] && after.relation[i][0] == prev.x && after.relation[i][1] == prev.y) {
        //←↑→↓↖↙↗↘↕
        let text = directions[i];
        paintCellText(after, text, cell_size / 2 - 5, cell_size / 2 + 5);
        break
      }
    }
  }

  /**
   * tb 相对 fa 的方向t
   * @param fa cell
   * @param tb cell
   * @param isStrict 是否严格要求相邻
   */
  function directionType(fa, tb, isStrict) {
    if (isStrict) {
      for (let i = 0; i < 8; i++) {
        if (fa.relation[i] && fa.relation[i][0] == tb.x && fa.relation[i][1] == tb.y) {
          //←↑→↓↖↙↗↘↕
          return i
        }
      }
    } else {
      let dx = tb.x - fa.x,
        dy = tb.y - fa.y;
      if (dx < 0) {
        return dy < 0 ? 7 : (dy == 0 ? 6 : 5)
      }
      if (dx == 0) {
        return dy < 0 ? 0 : (dy == 0 ? -1 : 4)
      }
      if (dx > 0) {
        return dy < 0 ? 1 : (dy == 0 ? 2 : 3)
      }
    }
  }

  /**
   * 根据x,y 坐标获取实际的cell对象
   * @param x
   * @param y
   */
  function getCellByXY(x, y) {
    return cells[y * my + x];
  }

  /**
   * 目标临近点是否是当前节点的夹角点,判断标准为临近点为当前点的对角点且对角两侧为不可通过点
   */
  function angleAllowCell(a, b) {
    let relation = a.relation;
    if (relation[1] && b == getCellByXY(...relation[1])) {
      return relation[0].price < notAllowPrice || relation[2].price < notAllowPrice
    }
    if (relation[3] && b == getCellByXY(...relation[3])) {
      return relation[2].price < notAllowPrice || relation[4].price < notAllowPrice
    }
    if (relation[5] && b == getCellByXY(...relation[5])) {
      return relation[4].price < notAllowPrice || relation[6].price < notAllowPrice
    }
    if (relation[7] && b == getCellByXY(...relation[7])) {
      return relation[6].price < notAllowPrice || relation[0].price < notAllowPrice
    }
    return true
  }

  /**
   * 给指定单元格的指定位置绘制文字
   * @param cell
   * @param text
   * @param x
   * @param y
   */
  function paintCellText(cell, text, x, y) {
    context.font = '12px';
    context.fillStyle = '#000';
    context.fillText(text, cell.x * cell_size + x, cell.y * cell_size + y);
  }

  /**
   * 延时
   */
  function delay(time) {
    time = time || 1000;
    return new Promise((res, rej) => {
      setTimeout(res, time)
    })
  }

  start();


  /**
   * 7  0  1
   * 6  *  2
   * 5  4  3
   *初始化open_set和close_set;
   * 将起点加入open_set中,并设置优先级为0(优先级最高);
   * 如果open_set不为空,则从open_set中选取优先级最高的节点n:
   *   如果节点n为终点,则:
   *     从终点开始逐步追踪parent节点,一直达到起点;
   *     返回找到的结果路径,算法结束;
   *   如果节点n不是终点,则:
   *      将节点n从open_set中删除,并加入close_set中;
   *      遍历节点n所有的邻近节点:
   *         如果邻近节点m在close_set中,则:
   *            跳过,选取下一个邻近节点
   *         如果邻近节点m在open_set中,则:
   *            判断节点n到节点m的 F(n) + cost[n,m] 值是否 < 节点m的 F(m) 。
   *            来尝试更新该点,重新设置f值和父节点等数据
   *         如果邻近节点m也不在open_set中,则:
   *            设置节点m的parent为节点n
   *            计算节点m的优先级
   *            将节点m加入open_set中
   *
   */
</script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值