从零实现A*寻路算法:Python+QT可视化工具开发全解析

📅 2026/7/14 4:34:30
从零实现A*寻路算法:Python+QT可视化工具开发全解析
1. 项目概述与核心价值最近在做一个游戏原型需要实现一个智能的寻路系统。市面上虽然有很多现成的寻路库但总觉得“黑盒”用起来不踏实出了问题不好调试性能瓶颈也摸不着头脑。于是我决定自己动手用 Python 和 QT 从零实现一个经典的 A-starA*寻路算法并把它做成一个带图形界面的可视化工具。这个项目听起来像是“重复造轮子”但实际做下来收获远超预期。它不仅仅是一个算法实现更是一个集算法理解、性能优化、图形界面交互和调试工具于一体的综合练习。对于开发者而言无论是刚接触算法的新手还是想深入理解寻路机制的老手这个项目都极具价值。通过 QT 的图形界面你可以实时看到算法如何“思考”——它探索了哪些节点为什么选择某条路径最终路径是如何一步步被找到的。这种可视化的反馈比单纯看代码或者控制台输出要直观得多。它解决了算法学习过程中“看不见、摸不着”的痛点让你能亲手设置障碍物、拖动起点终点并立刻看到寻路结果和算法内部的搜索过程。接下来我就把这次从零搭建的完整过程、踩过的坑以及一些性能调优的心得毫无保留地分享出来。2. 技术选型与整体架构设计2.1 为什么选择 Python QT 这个组合首先说 Python。A-star 算法的核心是逻辑和数据结构Python 语法简洁开发效率高能让我们更专注于算法本身而不是繁琐的语法细节。像列表、字典、优先队列heapq这些内置数据结构用来实现算法的开放列表Open List和关闭列表Closed List非常方便。然后是 QT。QT 是一个成熟的跨平台 C 图形界面框架而 PyQt/PySide 是其 Python 绑定。我选择的是 PySide6因为它在协议上更友好。QT 的强大在于其丰富的图形组件QGraphicsView/Scene/Item 体系和稳健的信号槽机制。对于这个项目我们需要一个可以自由绘制网格、响应鼠标事件设置起点、终点、障碍物、并能动态更新算法搜索过程的可视化画布QT 的 QGraphics 框架是绝佳选择。这个组合的优势很明显Python 负责核心算法逻辑保证开发速度和可读性QT 负责构建直观、交互性强的图形界面将抽象的算法过程具象化。两者通过信号槽松耦合比如界面点击触发寻路计算算法每步搜索的结果再通知界面更新显示。2.2 项目整体架构拆解整个项目可以清晰地分为三个层次数据模型层Model这是核心。它定义了一个GridMap类用来表示整个寻路地图。地图由许多Node节点组成每个节点有坐标、是否可通行、代价g值、启发值h值、父节点等属性。AStarSolver类则封装了完整的 A-star 算法逻辑它接收一个GridMap实例并计算出最优路径。视图与控制层View Controller使用 QT 的QGraphicsView,QGraphicsScene和自定义的QGraphicsRectItem来构建可视化界面。MainWindow类作为主窗口负责界面布局和事件处理如鼠标点击、按钮点击。GraphicsView或Scene的子类则负责将GridMap中的数据每个Node绘制成一个个可交互的方格。通信层使用 QT 的信号槽机制。例如当用户在视图上点击设置了一个障碍物视图会发射一个信号数据模型层接收后更新对应Node的通行状态。反之当AStarSolver在计算过程中探索了一个新节点它也可以发射一个信号触发视图层将该节点高亮显示为“已探索”状态实现算法过程的动画演示。这种架构确保了逻辑清晰数据与显示分离。算法部分可以独立进行单元测试而界面改动不会影响核心逻辑。注意在项目初期我曾尝试将地图数据和界面显示强耦合导致后期想更换渲染方式或增加新的算法如 Dijkstra, JPS时异常困难。务必坚持 Model 和 View 的分离。3. 核心算法原理与 Python 实现细节3.1 A-star 算法核心思想再理解A-star 之所以高效是因为它结合了 Dijkstra 算法和贪婪最佳优先搜索的优点。它为每个待评估的节点n计算一个估价函数f(n) g(n) h(n)。g(n)从起点到节点n的实际移动代价。在均匀网格中通常就是步数每步代价为1或者考虑对角线移动的近似距离。h(n)从节点n到终点的预估代价这就是启发函数。它引导搜索方向但必须满足“可采纳性”即永远不能高估实际代价。对于网格地图曼哈顿距离只允许上下左右移动或切比雪夫距离/对角线距离允许八方向移动是常用选择。算法维护两个集合开放列表Open List存放所有已发现但未评估的节点通常用**优先队列最小堆**实现按f(n)值排序保证每次都能取出当前最有希望的节点。关闭列表Closed List存放所有已评估过的节点避免重复搜索。流程简述如下将起点加入开放列表。循环直到开放列表为空或找到终点 a. 从开放列表中取出f值最小的节点current将其移入关闭列表。 b. 如果current是终点回溯构建路径算法结束。 c. 遍历current的所有邻居节点。 d. 如果邻居不可通行或在关闭列表中跳过。 e. 计算从起点经过current到该邻居的新g值tentative_g。 f. 如果邻居不在开放列表中或者新的tentative_g比它原有的g值更小则更新该邻居的g,h, f值并设置其父节点为current。如果它不在开放列表中则加入。3.2 Python 实现的关键代码与数据结构首先我们定义节点类。这里我使用dataclass来简化并重写了__lt__方法以便于优先队列比较。from dataclasses import dataclass, field from typing import Optional import math dataclass(orderTrue) # orderTrue 会根据定义的字段生成比较方法 class Node: 寻路网格中的节点 # 为了排序我们把 f 值放在第一个字段 f: float field(defaultmath.inf, compareTrue) x: int field(default0, compareFalse) y: int field(default0, compareFalse) walkable: bool field(defaultTrue, compareFalse) # 是否可通行 g: float field(defaultmath.inf, compareFalse) # 起点到本节点的实际代价 h: float field(default0, compareFalse) # 本节点到终点的预估代价 parent: Optional[Node] field(defaultNone, compareFalse) # 路径回溯用 property def position(self): return (self.x, self.y)接下来是网格地图GridMap它负责创建和管理所有节点。class GridMap: def __init__(self, width: int, height: int): self.width width self.height height self.nodes [[Node(xx, yy) for y in range(height)] for x in range(width)] def get_node(self, x: int, y: int) - Optional[Node]: if 0 x self.width and 0 y self.height: return self.nodes[x][y] return None def set_walkable(self, x: int, y: int, walkable: bool): node self.get_node(x, y) if node: node.walkable walkable def get_neighbors(self, node: Node, allow_diagonal: bool True): 获取一个节点的邻居节点。allow_diagonal 决定是否允许对角线移动。 directions [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上下左右 if allow_diagonal: directions [(-1, -1), (-1, 1), (1, -1), (1, 1)] # 四个对角线方向 neighbors [] for dx, dy in directions: nx, ny node.x dx, node.y dy neighbor self.get_node(nx, ny) if neighbor and neighbor.walkable: # 如果是对角线移动需要检查是否“卡墙角”即相邻的横向和纵向节点至少有一个是障碍物 if abs(dx) 1 and abs(dy) 1: if not self.get_node(node.x dx, node.y).walkable: continue if not self.get_node(node.x, node.y dy).walkable: continue neighbors.append(neighbor) return neighbors最后是算法的核心AStarSolver。这里我使用了 Python 的heapq作为优先队列并加入了过程回调用于可视化。import heapq from typing import List, Callable, Optional class AStarSolver: def __init__(self, heuristic_func: Callable[[Node, Node], float] None): # 默认使用曼哈顿距离 self.heuristic heuristic_func or self.manhattan_distance staticmethod def manhattan_distance(a: Node, b: Node) - float: return abs(a.x - b.x) abs(a.y - b.y) staticmethod def euclidean_distance(a: Node, b: Node) - float: return math.sqrt((a.x - b.x)**2 (a.y - b.y)**2) staticmethod def chebyshev_distance(a: Node, b: Node) - float: return max(abs(a.x - b.x), abs(a.y - b.y)) def solve(self, grid_map: GridMap, start: Node, end: Node, step_callback: Optional[Callable[[Node, List[Node]], None]] None) - List[Node]: 执行 A-star 寻路。 Args: step_callback: 单步回调函数接收 (当前节点, 开放列表节点) 用于可视化。 Returns: 找到的路径节点列表如果未找到则返回空列表。 if not start.walkable or not end.walkable: return [] # 初始化起点 start.g 0 start.h self.heuristic(start, end) start.f start.g start.h open_list [] closed_set set() # 使用集合来快速判断节点是否在关闭列表中 # 将起点放入优先队列。队列元素为 (f值, 插入计数器, 节点) # 插入计数器用于在 f 值相同时避免直接比较 Node 对象 counter 0 heapq.heappush(open_list, (start.f, counter, start)) node_entry_map {start.position: (start.f, counter, start)} # 跟踪节点在堆中的最新状态 while open_list: current_f, _, current heapq.heappop(open_list) current_pos current.position # 如果取出的节点不是最新状态g值已被更新则跳过 if node_entry_map.get(current_pos, (None, None, None))[2] is not current: continue # 加入关闭集合 closed_set.add(current_pos) # 可视化回调显示当前节点和开放列表 if step_callback: # 获取开放列表中所有节点用于高亮显示 open_nodes [item[2] for item in open_list] step_callback(current, open_nodes) # 找到终点回溯路径 if current is end: path [] while current: path.append(current) current current.parent return path[::-1] # 反转从起点到终点 # 遍历邻居 for neighbor in grid_map.get_neighbors(current, allow_diagonalTrue): if neighbor.position in closed_set: continue # 计算移动代价直线为1对角线约为1.414 move_cost 1.0 if (neighbor.x current.x or neighbor.y current.y) else math.sqrt(2) tentative_g current.g move_cost # 如果邻居不在开放列表中或者找到更优路径 neighbor_entry node_entry_map.get(neighbor.position) if neighbor_entry is None or tentative_g neighbor_entry[2].g: neighbor.g tentative_g neighbor.h self.heuristic(neighbor, end) neighbor.f neighbor.g neighbor.h neighbor.parent current counter 1 new_entry (neighbor.f, counter, neighbor) heapq.heappush(open_list, new_entry) node_entry_map[neighbor.position] new_entry return [] # 开放列表为空未找到路径实操心得在实现优先队列时我掉进过一个坑。如果两个节点的f值相同heapq会尝试比较下一个元素即Node对象本身。如果没有定义__lt__方法Python 无法比较会抛出异常。这就是为什么我在Node类中使用了dataclass(orderTrue)并将f作为第一个字段。另一种常见做法是在堆中存储三元组(f, counter, node)其中counter是一个递增的整数确保不会直接比较node对象。4. QT 图形界面设计与交互实现4.1 使用 QGraphics 框架构建可视化网格QT 的QGraphicsView/QGraphicsScene/QGraphicsItem体系非常适合用来做这种网格化、可交互的可视化。我的设计思路是QGraphicsScene作为场景管理所有图形项。自定义一个GridScene继承自QGraphicsScene它内部关联一个GridMap数据模型。为地图中的每个Node创建一个GridItem继承自QGraphicsRectItem作为其图形代表。GridItem根据Node的状态起点、终点、障碍物、普通、已探索、路径等显示不同的颜色。from PySide6.QtWidgets import QGraphicsScene, QGraphicsRectItem from PySide6.QtGui import QBrush, QPen, QColor from PySide6.QtCore import Qt, QRectF class GridItem(QGraphicsRectItem): 代表一个网格方块的图形项 COLORS { default: QColor(240, 240, 240), # 默认-浅灰 start: QColor(255, 100, 100), # 起点-红色 end: QColor(100, 200, 100), # 终点-绿色 obstacle: QColor(50, 50, 50), # 障碍物-深灰 open: QColor(200, 230, 255), # 开放列表-浅蓝 closed: QColor(255, 255, 200), # 关闭列表-浅黄 path: QColor(255, 150, 50), # 最终路径-橙色 } def __init__(self, x: int, y: int, size: float, node: Node): super().__init__(x * size, y * size, size, size) self.node node self.size size self.setPen(QPen(Qt.black, 1)) self.update_color() def update_color(self): 根据节点状态更新颜色 if not self.node.walkable: color self.COLORS[obstacle] # 这里需要从外部传入当前起点、终点、路径等信息来判断 # 我们稍后在 GridScene 中统一管理 else: color self.COLORS[default] self.setBrush(QBrush(color)) class GridScene(QGraphicsScene): 管理整个网格地图的图形场景 def __init__(self, grid_map: GridMap, cell_size: int 30): super().__init__() self.grid_map grid_map self.cell_size cell_size self.start_item None self.end_item None self.path_items [] self.open_items set() self.closed_items set() self._init_grid() def _init_grid(self): self.clear() self.items_dict {} # 映射 (x, y) - GridItem for x in range(self.grid_map.width): for y in range(self.grid_map.height): node self.grid_map.get_node(x, y) item GridItem(x, y, self.cell_size, node) self.addItem(item) self.items_dict[(x, y)] item def set_start(self, x, y): 设置起点 if self.start_item: self.start_item.setBrush(QBrush(GridItem.COLORS[default])) self.start_item self.items_dict.get((x, y)) if self.start_item: self.start_item.setBrush(QBrush(GridItem.COLORS[start])) # 类似地实现 set_end, add_to_open, add_to_closed, set_path 等方法...4.2 实现鼠标交互与算法动画交互逻辑主要在MainWindow或GraphicsView中通过重写鼠标事件来实现。from PySide6.QtWidgets import QMainWindow, QGraphicsView, QVBoxLayout, QWidget, QPushButton from PySide6.QtCore import Qt, QTimer class MainWindow(QMainWindow): def __init__(self): super().__init__() self.grid_map GridMap(30, 20) # 30x20 的网格 self.scene GridScene(self.grid_map, cell_size25) self.view QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing) # 设置鼠标跟踪和事件 self.view.setMouseTracking(True) self.view.mousePressEvent self.on_mouse_press # 可以设置不同的模式设置障碍、设置起点、设置终点 self.edit_mode obstacle # start, end, obstacle # 控制按钮 self.btn_run QPushButton(开始寻路) self.btn_clear QPushButton(清空地图) self.btn_run.clicked.connect(self.run_astar) self.btn_clear.clicked.connect(self.clear_map) # 布局 central_widget QWidget() layout QVBoxLayout() layout.addWidget(self.view) layout.addWidget(self.btn_run) layout.addWidget(self.btn_clear) central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.astar_solver AStarSolver() self.search_timer QTimer() self.search_timer.timeout.connect(self.astar_step) self.current_open_list [] self.current_path [] def on_mouse_press(self, event): 处理鼠标点击设置起点、终点、障碍物 scene_pos self.view.mapToScene(event.pos()) x int(scene_pos.x() // self.scene.cell_size) y int(scene_pos.y() // self.scene.cell_size) node self.grid_map.get_node(x, y) if not node: return if event.button() Qt.LeftButton: if self.edit_mode start: self.scene.set_start(x, y) self.start_node node elif self.edit_mode end: self.scene.set_end(x, y) self.end_node node elif self.edit_mode obstacle: walkable not node.walkable # 点击切换通行状态 self.grid_map.set_walkable(x, y, walkable) self.scene.update_item(x, y) # 可以添加右键清除等功能... def run_astar(self): 开始寻路并启动定时器以实现动画效果 if not hasattr(self, start_node) or not hasattr(self, end_node): return # 重置场景高亮 self.scene.clear_highlights() # 初始化算法状态 self.astar_solver AStarSolver() # 这里我们用一个生成器或列表来逐步执行算法为了简单我们用定时器模拟单步 # 更优雅的方式是修改 AStarSolver.solve使其成为一个生成器每次 yield 当前状态。 # 本例中我们采用回调方式并在回调中暂停一下以实现动画。 self.searching True self.current_step 0 # 为了演示我们用一个慢速定时器 self.search_timer.start(50) # 每50毫秒一步 def astar_step(self): 单步执行寻路算法简化演示实际需改造 solver 支持单步 # 注意这是一个简化示例。实际需要将 AStarSolver 改造成支持单步迭代。 # 一种方法是使用 yield 将 solve 方法变成生成器。 # 另一种是在 solver 内维护状态机。 # 由于篇幅这里仅给出概念。 if not self.searching: return # 假设我们有一个单步方法result self.astar_solver.step() # if result is not None: # 找到路径或失败 # self.search_timer.stop() # self.searching False # if result path_found: # self.draw_path() pass def clear_map(self): 清空地图重置所有状态 self.grid_map GridMap(30, 20) self.scene.set_map(self.grid_map) self.start_node None self.end_node None注意事项实现算法动画时直接在一个循环中调用solve并更新界面会导致界面卡死因为计算阻塞了 UI 主线程。正确的做法有两种1) 使用QTimer分步执行每步计算后调用QApplication.processEvents()更新界面2) 将耗时的计算放在一个单独的线程QThread中通过信号槽将中间状态传递回主线程更新 UI。对于教学演示第一种方法更简单对于大型地图第二种方法更优。5. 性能优化与高级功能探讨5.1 算法性能瓶颈分析与优化一个基础的 A-star 实现在小网格上运行很快但当网格变大如 1000x1000或者需要高频寻路如游戏每帧时性能就可能成为问题。主要的瓶颈在以下几点优先队列操作开放列表的插入和弹出最小元素操作。Python 的heapq是纯 Python 实现对于超大规模节点数十万其O(log n)的性能可能不够。可以考虑使用更高效的数据结构如heapq的 C 语言加速版本或者使用sortedcontainers库中的SortedList但在大多数情况下heapq已经足够。节点状态管理我们使用node_entry_map字典来跟踪节点在堆中的最新条目以处理节点g值更新的情况。字典的查找是O(1)效率很高。启发函数计算h(n)的计算应尽可能简单。曼哈顿距离只涉及加减法和绝对值是最快的。欧几里得距离涉及开方代价较高除非必要否则可以用切比雪夫距离或预计算的距离表来近似。邻居查找get_neighbors方法中对于每个节点都要计算八个方向并检查边界和通行性。这是一个固定的开销但可以通过预先计算静态障碍物信息或使用空间划分数据结构来优化动态环境。一个重要的优化技巧使用__slots__。 Python 默认使用字典来存储对象属性这带来了很大的内存开销和访问开销。对于成千上万的Node实例这会导致内存占用激增和缓存不友好。使用__slots__可以显著改善。class Node: __slots__ (x, y, walkable, g, h, f, parent) def __init__(self, x0, y0, walkableTrue): self.x x self.y y self.walkable walkable self.g float(inf) self.h 0.0 self.f float(inf) self.parent None # ... 比较方法等其他定义实测在生成 1000x1000 的网格时使用__slots__可以将内存占用减少 50% 以上同时属性访问速度也有提升。5.2 扩展功能多种启发函数与移动方式我们的AStarSolver已经支持传入自定义的启发函数。这让我们可以轻松尝试不同的寻路策略。曼哈顿距离适用于只能上下左右移动四方向的场景。它是可采纳的。对角线距离切比雪夫距离适用于可以八方向移动的场景且对角线移动代价与直线相同通常为1。它也是可采纳的并且比欧几里得距离计算快。欧几里得距离适用于可以任意角度移动的连续空间或在网格中希望路径更“直”时。它是可采纳的但计算稍慢且可能导致搜索节点更多。零启发函数当h(n) 0时A-star 退化为 Dijkstra 算法保证找到最短路径但效率最低。在GridMap.get_neighbors方法中我们通过allow_diagonal参数控制移动方式。对于对角线移动我们还需要处理“卡墙角”问题即当物体有体积时不能贴着障碍物的角落斜向移动。上面的代码已经通过检查对角线方向相邻的两个正交节点是否可通行来处理了这个问题。5.3 路径平滑与后处理A-star 在网格上找到的路径往往是“锯齿状”的因为搜索被限制在网格点上。对于视觉要求高的应用如游戏我们需要对路径进行平滑。一种简单有效的方法是“拉直”算法从起点开始将其加入平滑路径。检查从当前点到路径中后续的某个点之间是否有视线即连线不经过任何障碍物。如果可以直达就跳过中间的所有点将那个可达的远点作为新的当前点。重复直到终点。def smooth_path(grid_map: GridMap, path: List[Node]) - List[Node]: if len(path) 3: return path smoothed [path[0]] current_index 0 while current_index len(path) - 1: next_index len(path) - 1 # 从最远的点开始检查是否能直达 while next_index current_index 1: if has_line_of_sight(grid_map, path[current_index], path[next_index]): break next_index - 1 smoothed.append(path[next_index]) current_index next_index return smoothed def has_line_of_sight(grid_map, a: Node, b: Node) - bool: 使用 Bresenham 画线算法检查两点间是否有障碍物。 x0, y0 a.x, a.y x1, y1 b.x, b.y dx abs(x1 - x0) dy -abs(y1 - y0) sx 1 if x0 x1 else -1 sy 1 if y0 y1 else -1 err dx dy while True: # 检查当前网格点 node grid_map.get_node(x0, y0) if node is None or not node.walkable: return False if x0 x1 and y0 y1: break e2 2 * err if e2 dy: err dy x0 sx if e2 dx: err dx y0 sy return True将平滑后的路径在 QT 界面中用不同颜色如蓝色曲线绘制出来可以与原始网格路径进行对比效果非常直观。6. 常见问题排查与调试技巧在实际开发和使用这个工具的过程中我遇到了不少问题。这里总结一份速查表希望能帮你快速定位。问题现象可能原因排查与解决思路算法找不到路径1. 起点或终点被设置为障碍物。2. 起点和终点被障碍物完全隔离没有连通区域。3. 对角线移动被禁用而唯一路径需要对角线移动。4. “卡墙角”检查过于严格导致可通行路径被误判。1. 检查起点和终点的walkable属性。2. 在界面上手动检查障碍物是否形成了闭合包围圈。3. 尝试启用allow_diagonalTrue。4. 检查get_neighbors中关于对角线移动的“卡墙角”逻辑根据实际需求调整。找到的路径明显绕远1. 启发函数h(n)不可采纳高估了实际代价。2. 移动代价设置不合理如对角线代价不是sqrt(2)而是2。3. 算法实现有误g值更新逻辑错误。1. 确保使用的启发函数对于你的移动方式是可采纳的。在网格中曼哈顿距离用于四方向对角线/切比雪夫距离用于八方向。2. 检查move_cost的计算是否正确对应了移动方式。3. 在solve方法中加入详细日志打印每次从开放列表取出的节点及其f, g, h值观察搜索过程。界面卡顿特别是地图较大时1. 算法计算阻塞了 UI 线程。2. 图形项 (GridItem) 过多渲染效率低。3. 在搜索动画中每步都更新大量图形项属性触发重绘。1.必须将耗时的寻路计算放到子线程 (QThread)通过信号将进度和结果传回主线程更新UI。2. 考虑使用QGraphicsItemGroup管理同类项或对不可见区域进行图形项裁剪。3. 减少动画更新频率或者只更新发生状态变化的少数几个图形项而不是全部重绘。鼠标点击位置不准确将视图的鼠标坐标映射到场景坐标时除法取整或边界判断有误。检查on_mouse_press事件中的坐标转换逻辑。确保scene_pos计算正确并且x, y的转换考虑了网格的偏移例如网格从(0,0)开始绘制。添加打印日志输出转换后的坐标进行验证。内存占用过高1.Node对象数量巨大且未使用__slots__。2. 在寻路过程中创建了大量临时列表或对象如邻居列表。3. 图形场景保留了过多历史状态。1. 为Node类添加__slots__。2. 优化代码复用数据结构避免在循环内频繁创建新列表。3. 及时清理不再需要的图形项或数据。QT 程序崩溃或界面异常1. 在多线程中错误地操作了 UI 组件非主线程操作 GUI。2. 信号槽连接错误导致递归调用或死循环。3. Python 对象被意外销毁而 QT 仍在引用。1.牢记所有对 QT 图形对象的创建、修改、删除都必须在主线程进行。子线程应通过发射信号来请求主线程执行这些操作。2. 检查信号槽连接确保没有形成闭环。使用QTimer.singleShot或QMetaObject.invokeMethod进行跨线程调用。3. 确保对象的生命周期管理清晰必要时使用QObject的父子关系或 Python 的强引用。调试技巧可视化调试是王牌充分利用你做的这个工具在算法执行时高亮显示当前节点、开放列表、关闭列表。一眼就能看出算法是不是在“犯傻”。打印关键变量在算法循环中打印current节点的坐标和f,g,h值打印开放列表的长度。这能帮你理解算法的搜索过程。制作最小复现案例当遇到一个奇怪的 bug 时不要在大地图上调试。创建一个 5x5 的小地图手动设置起点、终点和几个障碍物复现问题。小地图让你能一眼看清所有状态。单元测试为GridMap,Node,AStarSolver的核心方法编写单元测试。例如测试get_neighbors在边界和障碍物旁是否正确测试solve在一个简单已知地图上是否能返回正确路径。这能极大提升代码的健壮性。这个项目做到最后它已经不仅仅是一个 A-star 算法的演示而成了一个强大的寻路算法实验平台。你可以很容易地修改启发函数、移动代价、甚至将 A-star 替换成 Dijkstra、BFS、JPS跳跃点搜索等其他算法并在统一的图形界面下观察它们的表现差异。这种直观的对比对于深入理解不同寻路算法的特性有着无可替代的价值。如果你对游戏开发、机器人路径规划或者算法可视化感兴趣强烈建议你亲手实现一遍过程中遇到的每一个问题都会让你对“寻路”这件事有更深一层的认识。