恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

游戏寻路算法实战:Dijkstra与A*原理、实现与优化

  • 首页
  • 资讯中心
  • /
  • 游戏寻路算法实战:Dijkstra与A*原理、实现与优化

相关资讯

DeepEP:专为MoE模型打造的All-to-All通信库,如何优化训练与推理? 2026/10/14 3:22:56
快速制作汇报总结PPT工具怎么选?2026横评实测 2026/10/14 3:22:56
Linux 驱动开发实战:从手写 LED 字符设备驱动到设备树 2026/10/14 3:22:56

最新资讯

systemd与systemctl核心操作:RH124第9章服务控制实战详解
峰特 FoamTech:NRC、λ、UL94 参数怎么读
Claude Code实战笔记:从安装到工程协作全解析
运维开发转网络安全:三大方向选择与30周学习路径
SpringBoot+Vue+MySQL客户关系管理系统毕设设计与实现全攻略
HR 告别手工做贺卡!企业微信自动推送带名字生日祝福海报

今日推荐

IBM Verify Identity Access 修复 22 个漏洞:两个 9.8 严重 RCE 无需认证
小区物业管理系统
小型超市仓库管理系统

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

游戏寻路算法实战:Dijkstra与A*原理、实现与优化

发布时间:2026/10/14 3:22:56
游戏寻路算法实战:Dijkstra与A*原理、实现与优化 1. 寻路算法到底在解决什么问题但凡做过游戏开发的人迟早会撞上同一个问题地图上有个单位要让它从A点走到B点中间有墙、有河、有各种障碍物怎么让它自己找一条合理的路这就是寻路算法要干的事。我第一次接触这个需求是在做一个2D塔防小项目的时候。当时想得很简单——让敌人朝目标直线移动不就行了结果第一波测试就翻车了敌人全卡在墙角原地抽搐看起来像在跳机械舞。后来才明白直线移动只适用于无障碍场景一旦地图复杂起来就必须有一套系统化的路径搜索方案。在众多寻路算法里Dijkstra算法和AStarA*算法是绕不开的两个核心。Dijkstra是基础它解决的是“从起点到所有点的最短路径”问题A是在Dijkstra基础上加了启发式函数专门优化“从起点到某一个终点”的效率。你可以这样理解Dijkstra像一个勤勤恳恳的快递员把整个城市的每条路都跑一遍确保找到最短路线而A像一个有经验的老司机凭直觉先朝目标方向找边走边修正通常能更快到达。这篇文章适合谁看如果你正在做游戏开发、机器人路径规划、地图导航相关的项目或者单纯对寻路算法好奇想动手实现一个那接下来的内容应该能帮到你。我会从算法原理讲起然后给出完整的代码实现最后分享一些实际项目中踩过的坑和优化技巧。代码用Python写因为可读性好方便你理解逻辑后再移植到C或C#里。2. 两种算法的核心原理拆解2.1 Dijkstra算法贪心策略下的最短路径Dijkstra算法的核心思想其实很朴素每次从“还没确定的节点”里挑一个当前距离起点最近的把它标记为“已确定”然后用它去更新邻居的距离。重复这个过程直到所有节点都被确定或者找到目标节点。为什么这样能保证最短关键在于边权非负这个前提。因为所有边的权重都是非负的所以当你从待定集合中取出距离最小的节点时不可能存在另一条更短的路径绕到它——任何绕路都只会增加距离。这个性质叫做“最优子结构”是Dijkstra正确性的基石。用生活化的例子来说假设你要从家去城市里各个地方Dijkstra的策略是先把离家最近的那个路口确定下来然后以这个路口为中转站看看能不能更近地到达其他路口。确定一个再确定下一个像水波扩散一样一层层往外推。具体实现时我们需要几个关键数据结构dist字典/数组记录从起点到每个节点的当前已知最短距离初始时起点为0其余为无穷大。visited集合记录已经确定最短路径的节点。优先队列最小堆每次快速取出当前距离最小的未确定节点。时间复杂度方面如果用二叉堆实现优先队列是O((VE)logV)其中V是节点数E是边数。对于游戏地图这种网格图来说每个节点最多4个或8个邻居E大约是V的4到8倍所以整体接近O(VlogV)。但Dijkstra有个明显的浪费它不关心终点在哪会把所有方向都均匀探索。如果起点在左下角终点在右上角它会把左下角周围所有节点都翻个遍才慢慢摸到右上角。这就引出了A*算法。2.2 A*算法给搜索装上“指南针”A*算法在Dijkstra的基础上引入了一个启发式函数h(n)用来估计从当前节点n到终点的代价。每次从优先队列里取节点时不再只看“从起点到n的实际代价g(n)”而是看f(n) g(n) h(n)。这个h(n)就像给搜索过程装了一个指南针让它优先朝终点方向探索。如果h(n)设计得当A*能比Dijkstra少探索大量无关节点速度提升非常明显。h(n)的设计有几个关键原则可采纳性Admissibilityh(n)不能高估从n到终点的实际代价。也就是说h(n) ≤ 真实代价。这保证了A*一定能找到最短路径。一致性Consistency/Monotonicity对于任意边(n, m)满足h(n) ≤ cost(n,m) h(m)。一致性是可采纳性的加强版满足一致性的启发函数能保证每个节点只被扩展一次效率更高。在网格地图中常用的启发函数有启发函数公式适用移动方式特点曼哈顿距离|x1-x2| |y1-y2|四方向移动计算快但允许斜走时会高估欧几里得距离√((x1-x2)² (y1-y2)²)任意方向移动最直观但开方运算稍慢对角距离max(dx,dy) (√2-1)×min(dx,dy)八方向移动八方向网格的最佳选择切比雪夫距离max(|x1-x2|, |y1-y2|)八方向移动代价相同简单但不够精确选错启发函数会怎样如果你在允许八方向移动的网格里用曼哈顿距离h(n)会高估实际代价A*可能找不到最短路径。如果你用欧几里得距离但实际只能四方向走h(n)会低估搜索效率会下降但结果仍然正确。所以启发函数必须和移动规则匹配这是实操中最容易犯的错误之一。2.3 两者对比什么时候用哪个很多人会问既然A*更快那Dijkstra还有存在的必要吗答案是有的取决于你的需求场景。对比维度DijkstraA*搜索目标起点到所有点的最短路径起点到特定终点的最短路径是否用启发函数否是搜索范围全方向均匀扩散偏向终点方向时间复杂度O((VE)logV)取决于启发函数质量最坏同Dijkstra结果最优性保证最优启发函数可采纳时保证最优适用场景需要知道到多个目标的最短距离单一起点终点、地图大且障碍多实际项目中如果你需要频繁查询“从某个点到地图上任意点的距离”比如做影响力地图、威胁评估那Dijkstra一次计算就能得到所有结果反而更划算。如果只是单次寻路A*通常是更好的选择。3. 从零实现完整代码与关键细节3.1 地图表示与基础数据结构在写算法之前先要确定地图怎么表示。游戏开发中最常见的是网格地图用一个二维数组表示0代表可通行1代表障碍。也有用**导航网格NavMesh**的把可行走区域切成多边形节点是多边形中心边是多边形之间的连通关系。NavMesh更省内存、路径更平滑但构建复杂。这里为了讲清楚算法本身用网格地图。import heapq import math # 地图定义0可通行1障碍 # 用二维列表表示行列对应y和x game_map [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 1, 1, 0], [0, 1, 0, 0, 0, 0, 1, 0], [0, 1, 0, 1, 1, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0, 0], [0, 1, 0, 0, 0, 1, 1, 0], [0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], ] ROWS len(game_map) COLS len(game_map[0]) # 四方向移动上、下、左、右 DIRS_4 [(-1, 0), (1, 0), (0, -1), (0, 1)] # 八方向移动加上四个对角 DIRS_8 [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]这里有个细节值得说八方向移动时对角移动的代价应该是√2而不是1。如果你把对角代价也设为1那实际路径长度会失真角色走对角会比走直线“更快”看起来像在作弊。正确做法是直线代价1对角代价约1.414。当然有些游戏为了简化会统一用1但那样路径形状会偏向对角需要根据项目需求取舍。3.2 Dijkstra算法完整实现def dijkstra(game_map, start, end): Dijkstra算法寻路 start/end: (row, col) 元组 返回: (最短路径列表, 总代价) 或 (None, inf) rows, cols len(game_map), len(game_map[0]) # dist记录起点到各点的最短距离 dist [[float(inf)] * cols for _ in range(rows)] # prev记录路径前驱节点用于回溯路径 prev [[None] * cols for _ in range(rows)] dist[start[0]][start[1]] 0 # 优先队列(当前距离, 行, 列) pq [(0, start[0], start[1])] visited set() while pq: d, r, c heapq.heappop(pq) # 如果已经确定过跳过 if (r, c) in visited: continue visited.add((r, c)) # 到达终点可以提前结束 if (r, c) end: break # 遍历邻居 for dr, dc in DIRS_4: nr, nc r dr, c dc # 边界检查 if not (0 nr rows and 0 nc cols): continue # 障碍检查 if game_map[nr][nc] 1: continue # 已确定最短路径的节点不需要再更新 if (nr, nc) in visited: continue new_dist d 1 # 四方向移动代价为1 if new_dist dist[nr][nc]: dist[nr][nc] new_dist prev[nr][nc] (r, c) heapq.heappush(pq, (new_dist, nr, nc)) # 回溯路径 if dist[end[0]][end[1]] float(inf): return None, float(inf) path [] cur end while cur is not None: path.append(cur) cur prev[cur[0]][cur[1]] path.reverse() return path, dist[end[0]][end[1]]这段代码有几个容易出错的地方。第一visited的判断时机。我在弹出堆顶元素时才标记visited而不是在入堆时标记。为什么因为同一个节点可能被多次入堆每次发现更短路径都会入堆如果在入堆时就标记后面更短的路径就进不来了。弹出时标记能保证第一次弹出的一定是最短距离这是Dijkstra的标准写法。第二提前终止条件。当弹出的节点是终点时可以直接break不需要继续探索。这个优化在只需要单条路径时很有用能省不少时间。第三路径回溯。用prev数组记录每个节点的前驱最后从终点倒着走回起点再反转。这是标准做法比在队列里存路径要省内存。3.3 A*算法完整实现def heuristic(a, b, methodmanhattan): 启发函数估计从a到b的代价 dr abs(a[0] - b[0]) dc abs(a[1] - b[1]) if method manhattan: return dr dc elif method euclidean: return math.sqrt(dr * dr dc * dc) elif method diagonal: # 八方向移动的对角距离 return max(dr, dc) (math.sqrt(2) - 1) * min(dr, dc) elif method chebyshev: return max(dr, dc) return 0 def astar(game_map, start, end, allow_diagonalFalse): A*算法寻路 allow_diagonal: 是否允许八方向移动 rows, cols len(game_map), len(game_map[0]) dirs DIRS_8 if allow_diagonal else DIRS_4 h_method diagonal if allow_diagonal else manhattan # g值起点到当前的实际代价 g [[float(inf)] * cols for _ in range(rows)] g[start[0]][start[1]] 0 prev [[None] * cols for _ in range(rows)] # 优先队列(f值, g值, 行, 列) # 注意堆比较时如果f相同会比较g再比较坐标避免比较到不可比较的对象 h_start heuristic(start, end, h_method) pq [(h_start, 0, start[0], start[1])] visited set() while pq: f, g_cur, r, c heapq.heappop(pq) if (r, c) in visited: continue visited.add((r, c)) if (r, c) end: break for dr, dc in dirs: nr, nc r dr, c dc if not (0 nr rows and 0 nc cols): continue if game_map[nr][nc] 1: continue if (nr, nc) in visited: continue # 计算移动代价对角为√2直线为1 if dr ! 0 and dc ! 0: move_cost math.sqrt(2) else: move_cost 1.0 new_g g_cur move_cost if new_g g[nr][nc]: g[nr][nc] new_g prev[nr][nc] (r, c) h heuristic((nr, nc), end, h_method) heapq.heappush(pq, (new_g h, new_g, nr, nc)) if g[end[0]][end[1]] float(inf): return None, float(inf) path [] cur end while cur is not None: path.append(cur) cur prev[cur[0]][cur[1]] path.reverse() return path, g[end[0]][end[1]]A*的实现和Dijkstra结构几乎一样区别就在优先队列的排序依据从g变成了f g h。但就是这一点改动效果天差地别。有个细节需要特别注意优先队列里存了(f, g, r, c)四个值。为什么要把g也存进去因为Python的heapq在比较元组时如果f相同会继续比较第二个元素。如果只存(f, r, c)当f相同时会比较r和c这没问题。但如果你存的是(f, node_object)而node_object没有实现比较方法就会报错。存g值既能避免这个问题又能在f相同时优先扩展g更大的节点离起点更近的这其实是一个微小的优化。3.4 路径平滑与后处理算法跑出来的路径是网格节点的序列直接让角色沿着走会显得很“锯齿”尤其是八方向移动时。实际项目中通常需要做路径平滑。最简单的平滑方法是视线检测从当前点出发尝试跳过中间节点直接连到更远的节点如果两点之间没有障碍物就删掉中间节点。def line_of_sight(game_map, p1, p2): Bresenham直线算法检测两点间是否无障碍 r1, c1 p1 r2, c2 p2 dr abs(r2 - r1) dc abs(c2 - c1) sr 1 if r1 r2 else -1 sc 1 if c1 c2 else -1 err dr - dc while True: if game_map[r1][c1] 1: return False if (r1, c1) (r2, c2): break e2 2 * err if e2 -dc: err - dc r1 sr if e2 dr: err dr c1 sc return True def smooth_path(game_map, path): 路径平滑删除不必要的中间节点 if len(path) 2: return path smoothed [path[0]] i 0 while i len(path) - 1: # 从最远的节点开始尝试 j len(path) - 1 while j i 1: if line_of_sight(game_map, path[i], path[j]): break j - 1 smoothed.append(path[j]) i j return smoothed这个平滑算法的时间复杂度是O(n²)最坏情况但对于一般游戏路径几十到几百个节点完全够用。如果路径特别长可以用更高效的算法但那是另一个话题了。4. 性能优化与实战避坑指南4.1 优先队列的选择与优化Python的heapq是最小堆功能没问题但在大量节点时性能一般。如果你用Cstd::priority_queue默认是最大堆需要自定义比较器。如果用C#SortedSet或自己实现二叉堆都可以。一个常见的优化是使用索引堆Indexed Priority Queue它支持修改队列中已有元素的优先级而不是每次都插入新元素。在A*中同一个节点可能被多次入堆如果用索引堆发现更短路径时直接更新能减少堆的大小。另一个优化是用桶排序替代堆。如果所有边的代价都是小整数比如网格地图中只有1和√2可以用多个桶来管理不同f值的节点实现O(1)的取出操作。这在超大地图上效果显著。4.2 启发函数的权重调整标准A要求h(n) ≤ 真实代价保证最优性。但实际游戏中稍微高估一点启发函数反而效果更好。这叫“加权A”用f(n) g(n) w × h(n)w 1。为什么因为游戏AI寻路往往不需要严格最短路径只需要“看起来合理”的路径。w越大搜索越偏向终点方向速度越快但路径可能不是最优。w1是标准A*w1.5到2之间通常能在速度和路径质量之间取得不错的平衡。我实测下来在大多数游戏地图上w1.2到1.5是比较甜的点。但要注意w不能太大否则路径会明显绕远甚至撞墙因为启发函数高估太多算法会忽略实际代价。如果发现角色走的路很诡异先检查一下w是不是设太大了。4.3 地图预处理与缓存策略每次寻路都从零开始算在大地图上会很慢。常见的优化手段有分层寻路Hierarchical Pathfinding把地图分成多个区域先在区域级别找一条粗略路径再在区域内部细化。这能把搜索空间从几万个节点降到几百个。路径缓存如果多个单位经常从相同起点到相同终点可以缓存结果。但要注意地图动态变化时缓存失效的问题。跳点搜索JPS这是A*在网格地图上的一个著名优化能跳过大量对称路径速度提升可达10倍以上。但JPS实现复杂且只适用于均匀代价的网格有障碍物时需要特殊处理。如果你的项目对寻路性能要求极高值得花时间研究。4.4 常见问题速查表问题现象可能原因排查方向找不到路径终点被障碍包围地图边界处理错误检查终点是否可通行检查边界条件路径不是最短启发函数高估对角代价设置错误检查h(n)是否可采纳检查对角代价是否为√2寻路速度慢地图太大启发函数太弱频繁重复寻路考虑分层寻路调整启发函数权重加缓存路径贴墙走代价函数没有惩罚贴墙平滑算法过于激进增加贴墙惩罚调整平滑策略角色走对角线穿墙八方向移动时没有检查对角障碍对角移动时检查两个相邻格子是否都可通行内存占用高每次寻路都新建大数组复用数组用位图代替二维列表4.5 实操心得与避坑经验第一个坑对角穿墙问题。八方向移动时如果只检查目标格子是否可通行角色可能从两个障碍物的对角缝隙中穿过去。正确做法是对角移动时要求两个相邻的直线格子都必须是可通行的。这个细节在文档里很少提但实际项目中一定会遇到。第二个坑浮点数比较。A*中g值用浮点数累加可能出现0.10.2≠0.3的情况。在比较new_g g[nr][nc]时最好加一个极小值epsilon比如new_g g[nr][nc] - 1e-9。否则可能因为浮点误差导致路径不是最优。第三个坑优先队列的稳定性。Python的heapq不是稳定堆相同优先级的元素弹出顺序不确定。如果你的游戏要求相同代价的路径有确定的选择比如总是优先走直线需要在比较元组里加入额外的tie-breaker比如节点坐标或一个递增的序号。第四个坑动态障碍物。如果地图上的障碍物会变化比如其他单位在移动寻路时需要把动态障碍物也考虑进去。但这样每次都要重新寻路性能开销大。常见做法是静态地图用A*算一条基础路径动态障碍物用局部避障算法如RVO处理两者结合。第五个坑起点或终点在障碍物上。这在玩家点击不可通行区域时经常发生。我的处理方式是如果终点不可通行就找离终点最近的可通行格子作为实际终点。如果起点不可通行比如单位被卡住了先把它推到最近的可通行格子再寻路。5. 从算法到产品实际项目中的工程化考量5.1 多单位寻路的性能瓶颈单个单位寻路A通常几毫秒就能搞定。但如果有几十上百个单位同时寻路性能就会成为问题。我做过一个测试在100×100的网格上单次A平均耗时约2毫秒100个单位同时寻路就是200毫秒如果每帧都算帧率直接崩掉。解决方案有几个方向分帧寻路把寻路请求放到队列里每帧只处理固定数量比如5个其余单位暂时沿旧路径移动或原地等待。这能平滑CPU占用但单位响应会有延迟。时间片轮转每个单位分配一个固定的时间片比如0.5毫秒时间到了就暂停下一帧继续。这需要算法支持中断和恢复实现复杂度较高。路径共享如果多个单位起点终点相近可以共享一条路径只在最后一段分叉。这在RTS游戏中很常见一队士兵沿同一条路走只是到达位置略有不同。流场寻路Flow Field这是RTS游戏的经典方案。对整个地图计算一次流场每个格子指向下一个格子的方向所有单位查表移动。缺点是地图变化时需要重新计算流场适合静态地图或变化不频繁的场景。5.2 不同游戏类型的寻路需求差异不同类型的游戏对寻路的要求差别很大不能一套方案打天下。RPG/动作游戏地图通常不大但障碍物复杂需要路径平滑和局部避障。A*加路径平滑基本够用重点是路径要自然不能有明显的锯齿。RTS即时战略单位多地图大需要高效的群体寻路。流场寻路或分层寻路是标配还要处理单位之间的碰撞和避让。开放世界地图极大可能需要跨区域寻路。分层寻路加路径缓存是基础还要考虑动态加载的地图块。回合制策略对实时性要求低但对路径的精确性和可预测性要求高。Dijkstra可能比A*更合适因为需要显示所有可到达区域。5.3 调试与可视化工具寻路算法调试起来很痛苦因为你看不到算法内部在干什么。强烈建议在开发阶段做一个可视化调试工具把搜索过程画出来。最简单的做法是用不同颜色标记节点状态白色未访问、灰色在优先队列中、黑色已确定、绿色路径。每扩展一个节点就刷新一次画面你就能直观看到搜索的扩散过程。这能帮你快速定位问题——比如发现搜索范围明显偏了就知道启发函数写错了发现搜索范围太大就知道启发函数太弱。如果不想自己写可视化也可以用现成的工具比如一些在线的寻路可视化网站把地图数据导进去就能看。但自己写一个更灵活能针对项目的地图格式定制。5.4 算法选择的决策流程最后给一个实用的决策流程帮你在实际项目中快速选型需要知道到多个目标的最短距离吗是→Dijkstra否→下一步。地图是均匀代价的网格吗是→考虑JPS或A*否→下一步。单位数量多吗50是→考虑流场或分层寻路否→下一步。地图动态变化频繁吗是→A加局部避障否→A加路径缓存。对路径质量要求高吗是→标准A加平滑否→加权A提速。这个流程不是绝对的但能帮你快速缩小选择范围。实际项目中往往需要组合多种技术比如分层A*加流场加局部避障具体怎么组合取决于你的场景和性能预算。寻路算法这东西看原理觉得简单真正动手实现才会发现到处都是细节。我建议你先用一个小地图把Dijkstra和A*都手写一遍跑通之后再逐步加优化。踩过一遍坑比看十篇文章都管用。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号