[A*寻路]基于javascript A*寻路获取最佳路径(附带例子)

直接上代码仓库https://github.com/applelee/a-plus

先上一个结果图
付上一张结果图

关于A*算法的细节,网上有太多的讲解,我这里就不做搬运工了。

本着搞明白算法,不如理清思路的道理,我在这里就简单的把我的代码思路讲解下,并附上部分代码与截图。

第一步 生成地图

在写我们的核心代码前我们先要一个简单的地图,这里我使用的一个15 * 15方格子组成的场景,加上简单的样式后形成了下面的样子(这里我将每个格子的矢量显示了出来,方便理解,后面不再显示矢量)。
带矢量的格子图
有了基本的地图结构,接下来就是添加障碍物,我的方法是在生成基本地图的同时加入一个随机数来判断是否生成障碍物,同时我们可以在生成障碍物的同时得到一个障碍物的矢量栈备用。带障碍的格子地图
附上部分代码(这部分代码放在例子文件a+.html中)

	const box = document.getElementById('container');
    // 场景高宽
    const box_w = box_h = 600;
    // 格子尺寸
    const el_w = el_h = 40;
    // 列
    const col = box_w / el_w;
    // 行
    const row = box_h / el_h;
    // 障碍物
    const obstacles = [];

    // 开始位置矢量
    let startVector = [];
    // 终点位置矢量
    let endVector = [];
    // 点击格子的次数
    let clickCount = 0;

    // 生成格子
    for (let i = 0; i < row; i ++) {
      for (let j = 0; j < col; j ++) {
        const element = document.createElement('div');
        // 生成 0 - 7 随机数
        const randomNum = Math.random() * 834819 & 7;
        // const randomNum = (j/33, i/33) * 834819 & 7;
        
        element.setAttribute('x', j);
        element.setAttribute('y', i);
        element.setAttribute('id', `${j}_${i}`);
        element.setAttribute('style', `left: ${j * el_w}px;top: ${i * el_h}px`);

        // 显示格子的矢量
        element.textContent = `${j}, ${i}`;

        // 生成障碍物
        if (randomNum > 5) {
          obstacles.push([j, i]);
          element.style.background = '#666';
        }
        box.appendChild(element);

        element.addEventListener('click', clickFn);
      }
    }

    // 点击事件的方法
    function clickFn () {
      clickCount += 1;

      // 获取两个格子的间的最短路径
      if (clickCount === 1) {
        startVector = [Number(this.getAttribute('x')), Number(this.getAttribute('y'))];
      } else if (clickCount === 2) {
        clickCount = 0;
        endVector = [Number(this.getAttribute('x')), Number(this.getAttribute('y'))];
      }
    }

接下来需要通过点击方格子的事件,来获取我们的起点与终点来获取对应矢量。
这样渲染层的工作就准备好了。

第二部 逻辑

第一部分我们已经为我们的核心代码准备好了三个重要的参数。
1、场景的大小(15 * 15)
2、障碍物矢量栈
3、点击格子获得的起点与终点的矢量

options = {
  // 起点矢量
  startVector: [0, 0],
  // 终点矢量
  endVector: [1, 1],
  // 场景大小 width height
  screenSize: [15, 15],
  // 障碍矢量集合
  obstacles: [...],
}
算法的选择

关于A*的算法,我看网上大多都是采用的贪心算法。但是贪心算法有个弊端就是,很多时候无法获得全局最优解。特别是在障碍物分布复杂的时候。
在这里插入图片描述

上图是贪心算法得到的预期的最短路径。

在这里插入图片描述

上图同样是是贪心算法得到的非最短路径。

如果想要得到稳定的最优解就采用其它的算法策略。

比如穷举法

和贪心算法每次扩展探索只从所有有效分支路径中选中当前最有解进行下一次扩展探索(起点到当前点实际的长度 + 当前点离终点的预期长度最短)。
穷举法不会选取当前最优解的分支单独进行扩展,而是同时对所有的有效分支进行扩展探索。

在这里插入图片描述

上图1-20是贪心算法的结果。其它每一种颜色(除了深灰色是障碍物)都是是每次扩展探索派生出来的分支。

在这里插入图片描述

上图1-20是穷举法的结果。同样的其它每一种颜色(除了深灰色是障碍物)都是是每次扩展探索派生出来的分支。
这里可以看出来,穷举法在效率上是明显不如贪心算法的。


上面代码说到的扩展探索有几个个比较重要的变量

  // 有效路径分支集合
  // 主要用来储存每次探索生成的有效分支路径
  // 如果在探索过程中某条分支无法产生新分支就视为无效分支,可以删除。
  let branchs = new Map();
  // 已探索过的矢量集合
  // 每次探索成功后就把格子的位置信息储存下来
  let solved = new Set();

我们在扩展探索的时候会对当前点的临近点进行探索,主要有两种。+探索(4向)*探索(8向)

  // + 检测
  const plus = [[0, -1], [1, 0], [0, 1], [-1, 0]];
  // * 检测
  const star = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]];

第三部分 完成条件

分为有解和无解两种情况
有解很好理解,无解的情况主要就一种。起点位置和终点位置被障碍物隔开,并且无法通过。

有解:当探索过程中,其中一条分支在探索临近点的时候发现与终点重合,就可以判断为完成,并将当前分支路径作为结果返回。

无解:当有效分支集合size为0的时候(地图所有的点被探索完毕,无法达到终点);循环或递归超过预设的阀值,通常地图尺度比较大的时候可能出现。

到这里整个逻辑代码结束,接下来就是返回路径数组交给html进行渲染

  // 路径染色
    function pathFillColor (path) {
      if (path.length < 1) alert('此路不通,离终点十万八千里 -_-|||!');
      const rgba = ramdomRGBA();
      let count = 0;

      const loop = (p = path) => {
        const vector = p[count];

        if (!vector) return;
        const el = document.getElementById(`${vector[0]}_${vector[1]}`);
        // el.textContent = count + 1;
        el.style.background = rgba;
        setTimeout(() => loop(p), 100)
        document.getElementById('text').innerText = count;

        count += 1;
        if (count >= p.length) {
          if (vector[0] !== endVector[0] || vector[1] !== endVector[1]) {
            setTimeout(() => alert('此路不通,要么翻山,要么打洞 -_-|||!'));
          }
          return;
        }
      }

      loop();
    }

到这里基本讲解完了,以上的代码都是片段,完整的代码去仓库里下载例子吧。觉得不错记得给个star哦!!

仓库地址https://github.com/applelee/a-plus

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Unity A*例子: 1. 创建一个空的游戏对象,将其命名为“A*”,在其上添加一个空的脚本组件“AStarPathfinding”。 2. 在脚本中添加以下代码: ``` public class AStarPathfinding : MonoBehaviour { public Transform startNode; public Transform endNode; public GameObject nodePrefab; public LayerMask unwalkableMask; public float nodeRadius; public float gridSize; public Vector2 gridWorldSize; Node[,] grid; void Start() { CreateGrid(); FindPath(startNode.position, endNode.position); } void CreateGrid() { int gridSizeX = Mathf.RoundToInt(gridWorldSize.x / gridSize); int gridSizeY = Mathf.RoundToInt(gridWorldSize.y / gridSize); grid = new Node[gridSizeX, gridSizeY]; Vector3 worldBottomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.forward * gridWorldSize.y / 2; for (int x = 0; x < gridSizeX; x++) { for (int y = 0; y < gridSizeY; y++) { Vector3 worldPoint = worldBottomLeft + Vector3.right * (x * gridSize + nodeRadius) + Vector3.forward * (y * gridSize + nodeRadius); bool walkable = !(Physics.CheckSphere(worldPoint, nodeRadius, unwalkableMask)); grid[x, y] = new Node(walkable, worldPoint, x, y); } } } void FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = NodeFromWorldPoint(startPos); Node targetNode = NodeFromWorldPoint(targetPos); List<Node> openSet = new List<Node>(); HashSet<Node> closedSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { Node currentNode = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || (openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { RetracePath(startNode, targetNode); return; } foreach (Node neighbor in GetNeighbors(currentNode)) { if (!neighbor.walkable || closedSet.Contains(neighbor)) { continue; } int newMovementCostToNeighbor = currentNode.gCost + GetDistance(currentNode, neighbor); if (newMovementCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor)) { neighbor.gCost = newMovementCostToNeighbor; neighbor.hCost = GetDistance(neighbor, targetNode); neighbor.parent = currentNode; if (!openSet.Contains(neighbor)) { openSet.Add(neighbor); } } } } } List<Node> GetNeighbors(Node node) { List<Node> neighbors = new List<Node>(); for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { if (x == 0 && y == 0) { continue; } int checkX = node.gridX + x; int checkY = node.gridY + y; if (checkX >= 0 && checkX < grid.GetLength(0) && checkY >= 0 && checkY < grid.GetLength(1)) { neighbors.Add(grid[checkX, checkY]); } } } return neighbors; } Node NodeFromWorldPoint(Vector3 worldPosition) { float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x; float percentY = (worldPosition.z + gridWorldSize.y / 2) / gridWorldSize.y; percentX = Mathf.Clamp01(percentX); percentY = Mathf.Clamp01(percentY); int x = Mathf.RoundToInt((grid.GetLength(0) - 1) * percentX); int y = Mathf.RoundToInt((grid.GetLength(1) - 1) * percentY); return grid[x, y]; } void RetracePath(Node startNode, Node endNode) { List<Node> path = new List<Node>(); Node currentNode = endNode; while (currentNode != startNode) { path.Add(currentNode); currentNode = currentNode.parent; } path.Reverse(); foreach (Node node in path) { Instantiate(nodePrefab, node.worldPosition, Quaternion.identity); } } int GetDistance(Node nodeA, Node nodeB) { int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX); int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY); if (dstX > dstY) { return 14 * dstY + 10 * (dstX - dstY); } else { return 14 * dstX + 10 * (dstY - dstX); } } public class Node { public bool walkable; public Vector3 worldPosition; public int gridX; public int gridY; public int gCost; public int hCost; public Node parent; public Node(bool _walkable, Vector3 _worldPos, int _gridX, int _gridY) { walkable = _walkable; worldPosition = _worldPos; gridX = _gridX; gridY = _gridY; } public int fCost { get { return gCost + hCost; } } } } ``` 3. 在场景中添加两个球体,将其中一个球体的Transform组件的位置设置为(-5, 0, 0),另一个球体的Transform组件的位置设置为(5, 0, 0),并将它们分别命名为“Start”和“End”。 4. 在场景中添加一个平面作为地图,将其缩放为(10, 1, 10),并将其位置设置为(0, -0.5, 0)。 5. 在场景中添加一个球体作为障碍物,将其缩放为(2, 2, 2),并将其位置设置为(0, 1, -2)。 6. 在“A*”游戏对象的脚本中,将“startNode”变量设置为“Start”游戏对象的Transform组件,将“endNode”变量设置为“End”游戏对象的Transform组件,将“nodePrefab”变量设置为一个球体预制体,将“unwalkableMask”变量设置为“Obstacle”层,将“nodeRadius”变量设置为0.5,将“gridSize”变量设置为1,将“gridWorldSize”变量设置为(10, 10)。 7. 运行游戏,可以看到在“Start”和“End”之间生成了一条路径路径上的节点用球体表示。可以尝试改变地图和障碍物的位置和大小,并在脚本中调整相关变量,观察结果的变化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值