用一个简单的场景来举例,咱们看一看A星寻路算法的工作过程。
    image.png
    迷宫游戏的场景通常都是由小方格组成的。假设我们有一个7×5大小的迷宫,上图中绿色的格子是起点,红色的格子是终点,中间的3个蓝色格子是一堵墙。
    AI角色从起点开始,每一步只能向上下/左右移动1格,且不能穿越墙壁。那么如何让AI角色用最少的步数到达终点呢?
    在解决这个问题之前,我们先引入2个集合和1个公式。
    两个集合如下。

    • OpenList:可到达的格子
    • CloseList:已到达的格子

    一个公式如下。

    • A* 寻路算法 - 图2

    每一个格子都具有F、G、H这3个属性,就像下图这样。
    image.png
    G:从起点走到当前格子的成本,也就是已经花费了多少步。
    H:在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远。
    F:G和H的综合评估,也就是从起点到达当前格子,再从当前格子到达目标格子的总步数。
    我们通过实际场景来分析一下,你就明白了。
    第1步,把起点放入OpenList,也就是刚才所说的可到达格子的集合。
    image.png
    第2步,找出OpenList中F值最小的方格作为当前方格。虽然我们没有直接计算起点方格的F值,但此时OpenList中只有唯一的方格Grid(1,2),把当前格子移出OpenList,放入CloseList。代表这个格子已到达并检查过了。
    image.png
    第3步,找出当前方格(刚刚检查过的格子)上、下、左、右所有可到达的格子,看它们是否在OpenList或CloseList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。
    image.png
    在上图中,每个格子的左下方数字是G,右下方是H,左上方是F。
    一个格子的“父节点”代表它的来路,在输出最终路线时会用到。
    刚才经历的几个步骤是一次局部寻路的步骤。我们需要一次又一次重复刚才的第2步和第3步,直到找到终点为止。
    下面进入A星寻路的第2轮操作。
    第1步,找出OpenList中F值最小的方格,即方格Grid(2,2),将它作为当前方格,并把当前方格移出OpenList,放入CloseList。代表这个格子已到达并检查过了。
    image.png
    第2步,找出当前方格上、下、左、右所有可到达的格子,看它们是否在OpenList或CloseList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。
    image.png
    为什么这一次OpenList只增加了2个新格子呢?因为Grid(3,2)是墙壁,自然不用考虑,而Grid(1,2)在CloseList中,说明已经检查过了,也不用考虑。
    下面我们进入第3轮寻路历程。
    第1步,找出OpenList中F值最小的方格。由于此时有多个方格的F值相等,任意选择一个即可,如将Grid(2,3)作为当前方格,并把当前方格移出OpenList,放入CloseList。代表这个格子已到达并检查过了。
    image.png
    第2步,找出当前方格上、下、左、右所有可到达的格子,看它们是否在OpenList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。
    image.png
    剩下的就是以前面的方式继续迭代,直到OpenList中出现终点方格为止。这里我们仅仅使用图片简单描述一下,方格中的数字表示F值。
    image.pngimage.png
    image.png
    像这样一步一步来,当终点出现在OpenList中时,我们的寻路之旅就结束了。
    还记得刚才方格之间的父子关系吗?我们只要顺着终点方格找到它的父亲,再找到父亲的父亲……如此依次回溯,就能找到一条最佳路径了。
    image.png
    这就是A星寻路算法的基本思想。像这样以估值高低来决定搜索优先次序的方法,被称为启发式搜索 。

    1. class Grid {
    2. public int x;
    3. public int y;
    4. public int f;
    5. public int g;
    6. public int h;
    7. public Grid parent;
    8. public Grid(int x, int y) {
    9. this.x = x;
    10. this.y = y;
    11. }
    12. public void initGrid(Grid parent, Grid end) {
    13. this.parent = parent;
    14. if (parent != null) {
    15. this.g = parent.g + 1;
    16. } else {
    17. this.g = 1;
    18. }
    19. this.h = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
    20. this.f = this.g + this.h;
    21. }
    22. }
    23. public class AStarSearch {
    24. // 迷宫地图
    25. public static final int[][] MAZE = {
    26. {0, 0, 0, 0, 0, 0, 0},
    27. {0, 0, 0, 1, 0, 0, 0},
    28. {0, 0, 0, 1, 0, 0, 0},
    29. {0, 0, 0, 1, 0, 0, 0},
    30. {0, 0, 0, 0, 0, 0, 0}
    31. };
    32. private static boolean containGrid(List<Grid> grids, int x, int y) {
    33. for (Grid n : grids) {
    34. if ((n.x == x) && (n.y == y)) {
    35. return true;
    36. }
    37. }
    38. return false;
    39. }
    40. private static boolean isValidGrid(int x, int y, List<Grid> openList, List<Grid> closeList) {
    41. // 是否超过边界
    42. if (x < 0 || x >= MAZE.length || y < 0 || y >= MAZE[0].length) return false;
    43. // 是否有障碍物
    44. if (MAZE[x][y] == 1) return false;
    45. // 是否已经在 openList 中
    46. if (containGrid(openList, x, y)) return false;
    47. // 是否已经在 closeList 中
    48. if (containGrid(closeList, x, y)) return false;
    49. return true;
    50. }
    51. private static ArrayList<Grid> findNeighbors(Grid grid, List<Grid> openList, List<Grid> closeList) {
    52. ArrayList<Grid> gridList = new ArrayList<>();
    53. if (isValidGrid(grid.x, grid.y - 1, openList, closeList)) {
    54. gridList.add(new Grid(grid.x, grid.y - 1));
    55. }
    56. if (isValidGrid(grid.x, grid.y + 1, openList, closeList)) {
    57. gridList.add(new Grid(grid.x, grid.y + 1));
    58. }
    59. if (isValidGrid(grid.x - 1, grid.y, openList, closeList)) {
    60. gridList.add(new Grid(grid.x - 1, grid.y));
    61. }
    62. if (isValidGrid(grid.x + 1, grid.y, openList, closeList)) {
    63. gridList.add(new Grid(grid.x + 1, grid.y));
    64. }
    65. return gridList;
    66. }
    67. private static Grid findMinGird(ArrayList<Grid> openList) {
    68. Grid tempGrid = openList.get(0);
    69. for (Grid grid : openList) {
    70. if (grid.f < tempGrid.f) {
    71. tempGrid = grid;
    72. }
    73. }
    74. return tempGrid;
    75. }
    76. // 主逻辑
    77. public static Grid aStartSearch(Grid start, Grid end) {
    78. ArrayList<Grid> openList = new ArrayList<>();
    79. ArrayList<Grid> closeList = new ArrayList<>();
    80. // 把起点加入 openList
    81. openList.add(start);
    82. // 主循环,每一轮检测一个当前方格节点
    83. while (openList.size() > 0) {
    84. // 在 openList 中查找 F 值最小的节点,将其作为当前方格节点
    85. Grid currentGrid = findMinGird(openList);
    86. // 将当前节点从 openList 中删除
    87. openList.remove(currentGrid);
    88. // 当前方格节点进入 closeList
    89. closeList.add(currentGrid);
    90. // 找到所有邻近节点
    91. ArrayList<Grid> neighbors = findNeighbors(currentGrid, openList, closeList);
    92. for (Grid grid : neighbors) {
    93. if (!openList.contains(grid)) {
    94. // 邻近节点不在 openList 中,标记“父节点”、G、H、F,并放入 openList
    95. grid.initGrid(currentGrid, end);
    96. openList.add(grid);
    97. }
    98. }
    99. // 如果终点在 openList 中,直接返回终点格子
    100. for (Grid grid : openList) {
    101. if ((grid.x == end.x) && (grid.y == end.y)) {
    102. return grid;
    103. }
    104. }
    105. }
    106. // openList 用尽,仍然找不到终点,说明终点不可到达,返回空
    107. return null;
    108. }
    109. public static void main(String[] args) {
    110. // 设置起点和终点
    111. Grid start = new Grid(2, 1);
    112. Grid end = new Grid(2, 5);
    113. // 搜索迷宫终点
    114. Grid resultGrid = aStartSearch(start, end);
    115. // 回溯迷宫路径
    116. ArrayList<Grid> path = new ArrayList<>();
    117. while (resultGrid != null) {
    118. path.add(new Grid(resultGrid.x, resultGrid.y));
    119. resultGrid = resultGrid.parent;
    120. }
    121. // 输出迷宫和路径,路径用 * 表示
    122. for (int i = 0; i < MAZE.length; i++) {
    123. for (int j = 0; j < MAZE[0].length; j++) {
    124. if (containGrid(path, i, j)) {
    125. System.out.print("*, ");
    126. } else {
    127. System.out.print(MAZE[i][j] + ", ");
    128. }
    129. }
    130. System.out.println();
    131. }
    132. }
    133. }