☰
基于A*算法的网格地图多机器人导航Matlab仿真与实现
2026/10/21 8:31:09 网站建设 项目流程

最近整理了一个机器人导航的小Demo,项目标题是“基于A_Star算法模拟网格地图多机器人导航”,配套的Matlab代码也一起放出来了。简单说,就是给定一张二维栅格地图,用A算法给每个机器人找一条从起点到终点的无碰撞路径,然后让它们在仿真环境里依次或同时移动。这个项目表面看起来不复杂,但真把它跑起来、把机器人数量从一台加到三台、五台,各种问题就冒出来了:路径重叠、互相堵死、某台机器人绕远路绕到离谱……这篇文章主要是把整个项目的设计思路、A算法在网格地图里的实现细节、多机器人冲突处理办法,以及我调参排坑的过程一次性讲清楚。它适合刚接触路径规划的学生、准备做移动机器人竞赛的小队,也适合那些想快速验证A*效果,又不想从零造轮子的开发者。

1. 项目到底做了什么:一个网格地图上的多机器人导航Demo

1.1 先说清楚A_Star和网格地图之间的关系

网格地图是机器人导航里最常用的环境表达方式之一。把现实空间按固定分辨率切成一个个方块,每个格子记录一个状态,0表示空闲,1表示障碍,这就构成了一张栅格地图。A_Star算法做的事情,是把这张图看作一张带权图,每个格子是一个节点,格子之间的通行关系是边,然后从起点节点不断向外扩展,找出一条代价最小的格子序列。

这里有个常见的误区:A本身并不知道机器人是什么,也不关心机器人长什么样,它只负责在格子里搜索。真正让机器人“动起来”的是上层调度逻辑,比如多台机器人按什么顺序规划、遇到冲突怎么处理。所以做这个项目时,第一步是把地图建对,第二步才是写A。很多人一上来就写算法,地图没建明白,后面所有调试都会乱成一团。

1.2 多机器人导航为什么不是把A*跑N遍就完事

把A在单台机器人上跑通之后,最自然的想法是:有三台机器人就调用三次A,各走各的最短路径。真这么做了,你会发现大概率会撞车。原因很简单,单机A*规划时只考虑静态障碍物,完全不考虑其他机器人在时间轴上的位置。三台机器人各自的最短路径可能都通过同一个路口,如果时间上又恰好重叠,那就堵死了。

所以多机器人导航的本质,是把“空间维度”和“时间维度”一起考虑进去,或者至少用某种策略让多台机器人在时间和空间上错开。这个Demo里我采用的策略是顺序规划加简单时间窗:先给优先级高的机器人规划路径,然后把它的占用信息记录下来,后续机器人规划时把这些占用信息作为临时约束,必要时等待原地而不是重新绕一大圈。这个思路第四章会详细展开。

1.3 这个Demo适合谁,能验证什么

我整理这份代码时,主要面向三类人。第一类是刚接触路径规划的学生,可以用它理解A*的完整执行过程。代码里开列表、闭列表、启发式函数、路径回溯这些概念,都可以实际跑一遍再对照变量变化,理解比看公式快得多。第二类是准备竞赛或者做小车项目的团队,需要快速搭一个多机避碰仿真原型,验证调度思路是否可行。第三类是手里有路径规划任务、又不想自己从头抠算法的工程师,拿到代码改地图、改参数就能用。

当然它也有很明确的边界:这是一个二维静态栅格地图下的Matlab仿真,不涉及真实机器人的里程计、定位、SLAM和底层电机控制。想直接跑到实体小车上,还需要很多外围工作,但这部分不在今天这篇文章范围内。

2. 网格地图建模与A*算法的核心原理

2.1 栅格地图建模的细节:坐标系、障碍物和膨胀

这个项目的输入是一张二维矩阵,约定很直接:用map = zeros(rows, cols)生成全空地图,然后把障碍物所在格子置1。坐标顺序是[行, 列],也就是map的第几行第几列,路径点也用这个格式表示。矩阵的行列坐标和图像坐标里的xy并不一样,但A*只关心节点之间的邻接关系,所以用行列坐标完全够用,不需要额外转换。

需要注意的一点是障碍物膨胀。如果障碍物只占一个格子,但机器人实际是有尺寸的,那么路径规划出来虽然安全通过相邻格子,真实环境中可能根本开不过去。所以建图时要在障碍物周围再扩展一圈不可通行区域,比如障碍物在(5,10),就把(4,10)、(5,9)、(6,10)、(5,11)这些相邻格子也标成障碍。膨胀半径取决于机器人尺寸与栅格分辨率的比例,这是一个非常容易漏掉的工程细节。

2.2 启发式函数怎么选:曼哈顿、欧几里得还是对角距离

A的估价函数是f(n) = g(n) + h(n),g(n)是从起点到当前节点的实际代价,h(n)是当前节点到目标点的启发式估计值。启发式函数的选择直接决定搜索效率和路径形态。如果h(n)始终不大于真实代价,也就是满足“可采纳”条件,A保证能搜出最短路径;h越接近真实代价,扩展的节点越少,搜索越快。

这个Demo里我用了四种常见距离,实际代码默认是第四种:

启发式公式适用邻域特点
曼哈顿距离|dx| + |dy|四连通路径呈折线,扩展较均匀,适合只能上下左右走的场景
欧几里得距离sqrt(dx^2 + dy^2)任意永远满足可采纳,但偏离真实代价较多,扩展节点偏多
切比雪夫距离max(|dx|, |dy|)八连通且斜走代价为1各方向代价相同时精确,用于普通八邻域搜索很稳
Octile距离max + (sqrt(2)-1)*min八连通且斜走代价为sqrt(2)最贴合斜向移动的实际情况,推荐默认使用

如果你用四连通邻域,建议用曼哈顿距离;用八连通邻域,则应把斜移动代价设为sqrt(2),并配合Octile距离。比如何:从(1,1)到(5,5),曼哈顿距离是8,但斜切过去只需要约5.66,如果还拿曼哈顿距离当启发式,虽然不会出错,但搜索范围明显变大,网格一多就能感觉到卡顿。

2.3 A*主循环的数据结构:开列表、代价值与路径回溯

代码里我用矩阵直接存储g值和f值,用单独的标志矩阵表示某个节点是否在开列表中。对于20乘20这种小地图完全够用,真要跑500乘500以上再换成堆结构。主循环的逻辑是:从开列表中取出f值最小的节点作为当前节点,如果它就是终点,就回溯路径并结束;否则遍历它的所有可行邻居,计算从起点经过当前节点到邻居的临时g值,如果这个临时g值比邻居原来的g值小,就更新邻居的g值、f值和父节点,并把邻居加入开列表。

路径回溯用了一个很朴素的方法:维护两个矩阵prevR和prevC,分别记录每个节点的父节点行坐标和列坐标。到达终点后,从终点倒着找父节点,直到回到起点,再把顺序反过来就是完整路径。这个方法牺牲了一点空间,但好处是调试非常方便,你可以随时停下来看某个格子是从哪个格子走过来的,特别适合学习阶段。

3. Matlab实现与核心代码拆解

3.1 地图初始化和参数定义

我这次提供一个完整的执行流程。首先建图并设置机器人的起点和终点。

% demo_main.m —— 多机器人网格地图导航入口 clear; clc; close all; % 地图尺寸:20行20列 rows = 20; cols = 20; map = zeros(rows, cols); % 手动设置一些障碍物,按 [行 列] 填写 obsIdx = [5 5; 5 6; 6 5; 9 12; 9 13; 10 13; 14 3; 15 3; 16 3; 17 8; 17 9; 18 8]; for i = 1:size(obsIdx, 1) map(obsIdx(i,1), obsIdx(i,2)) = 1; end % 三台机器人的起点终点 robotStarts = {[2 2], [2 19], [19 19]}; robotGoals = {[19 19], [19 2], [2 2]}; % 邻域模式:4表示四连通,8表示八连通 neighborMode = 8; % 提前为每台机器人缓存路径 nRobots = length(robotStarts); paths = cell(1, nRobots);

这里起点和终点最好不要直接放在障碍物格子里。我调试时遇到过有人把起点放在障碍物上的情况,A*循环跑完也返回空路径,容易让人误以为算法写错了。

3.2 A*核心函数实现

接下来是A*主体函数,我尽量写得直观,没做过重的性能优化,方便阅读和修改。

function [path, cost] = astar_grid(map, startPos, goalPos, mode) % 输入:map地图,startPos和目标点为[row col],mode为4或8 [rows, cols] = size(map); g = inf(rows, cols); % g值 f = inf(rows, cols); % f值 prevR = zeros(rows, cols); % 父节点行 prevC = zeros(rows, cols); % 父节点列 startIdx = sub2ind([rows, cols], startPos(1), startPos(2)); goalIdx = sub2ind([rows, cols], goalPos(1), goalPos(2)); g(startIdx) = 0; f(startIdx) = heuristicCost(startPos, goalPos, mode); openFlag = false(rows, cols); openFlag(startIdx) = true; while any(openFlag(:)) % 在开列表中找f值最小的节点 fCopy = f; fCopy(~openFlag) = inf; [~, currentIdx] = min(fCopy(:)); [cr, cc] = ind2sub([rows, cols], currentIdx); if currentIdx == goalIdx path = reconstructPath(prevR, prevC, startPos, goalPos); cost = g(currentIdx); return; end openFlag(currentIdx) = false; % 相当于加入闭列表 nbrs = findNeighbors(cr, cc, rows, cols, mode); for i = 1:size(nbrs, 1) nr = nbrs(i,1); nc = nbrs(i,2); if map(nr, nc) == 1 continue; end if abs(nr-cr) + abs(nc-cc) == 2 stepCost = sqrt(2); else stepCost = 1; end nIdx = sub2ind([rows, cols], nr, nc); tentativeG = g(currentIdx) + stepCost; if tentativeG < g(nIdx) g(nIdx) = tentativeG; f(nIdx) = tentativeG + heuristicCost([nr nc], goalPos, mode); prevR(nIdx) = cr; prevC(nIdx) = cc; openFlag(nIdx) = true; end end end path = []; cost = inf; end

这个版本没有单独维护开列表的堆结构,而是每次用min在整张f矩阵里找最小值。好处是代码短、不容易写错,坏处是地图大了以后速度会明显下降。如果你只需要在20乘20的地图上跑几台机器人,这个写法完全没问题。

配套的两个辅助函数也比较关键。邻居生成函数决定了搜索连通方式,启发式函数前面已经讲过。

function nbrs = findNeighbors(r, c, rows, cols, mode) if mode == 4 dirs = [-1 0; 1 0; 0 -1; 0 1]; else dirs = [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1]; end nbrs = []; for i = 1:size(dirs, 1) nr = r + dirs(i,1); nc = c + dirs(i,2); if nr >= 1 && nr <= rows && nc >= 1 && nc <= cols nbrs(end+1, :) = [nr nc]; end end end function h = heuristicCost(pos, goalPos, mode) dr = abs(pos(1) - goalPos(1)); dc = abs(pos(2) - goalPos(2)); if mode == 4 h = dr + dc; else m = min(dr, dc); h = m * sqrt(2) + (max(dr, dc) - m); end end function path = reconstructPath(prevR, prevC, startPos, goalPos) path = goalPos; cr = goalPos(1); cc = goalPos(2); while ~(cr == startPos(1) && cc == startPos(2)) nr = prevR(cr, cc); nc = prevC(cr, cc); if nr == 0 || nc == 0 break; end path = [[nr nc]; path]; cr = nr; cc = nc; end end

这里要注意,reconstructPath里如果父节点是0,说明路径出了问题,直接跳出避免死循环。在调试阶段,这种防御性写法能帮你快速定位是搜索失败还是回溯失败。

3.3 路径可视化与多机器人移动仿真

路径算出来后,怎么展示是一个很重要的问题。我的做法是先画地图,再把路径点画成折线,然后在仿真循环里让机器人沿着路径点逐格移动。

% 显示地图与路径 figure; hold on; axis equal; for i = 1:rows for j = 1:cols if map(i,j) == 1 rectangle('Position', [j-1, i-1, 1, 1], 'FaceColor', [0.3 0.3 0.3], 'EdgeColor', 'none'); end end end % 规划所有机器人的路径 for r = 1:nRobots [paths{r}, cost] = astar_grid(map, robotStarts{r}, robotGoals{r}, neighborMode); plot(paths{r}(:,2)-0.5, paths{r}(:,1)-0.5, 'LineWidth', 2); end

真正仿真移动时,不能直接让机器人“瞬移”到终点。我通常把每段路径细分,在每两个路径点之间线性插值几步,然后用drawnow刷新画面。这样看起来就是连续移动,而不是跳格子。插值步数越多,动画越平滑,但要注意实际机器人速度是有上限的,插值步数其实对应了时间分辨率,这个细节到第四章还会提到。

3.4 参数调优:栅格粒度、邻域方式与代价单位

地图分辨率直接决定搜索空间的规模和路径的精细度。同样的真实环境下,栅格越细,路径越贴近可行道路,但节点数变多,计算时间成倍上升。栅格越粗,计算快,但机器人可能通过不了狭窄通道,路径也容易出现明明靠墙很近却必须绕一大圈的尴尬情况。实际使用时,我会先根据机器人尺寸和通行通道宽度确定栅格分辨率,一般让机器人直径占1到2个栅格,这样既能保证安全,又不会让地图太大。

邻域方式的选择同样影响结果。四连通路径只能横平竖直,转折多但不钻墙角;八连通路径可以斜走,视觉上更像人走出来的路线,但是容易出现斜穿对角缝隙的“偷懒”行为。比如两个障碍物的对角位置恰好空出一个格子,八连通路径会斜着穿过去,这在某些真实场景里会撞车。解决方案是在邻居扩展时加判断:斜向移动前,先检查相邻的两个正方向格子是否都是空地,如果其中一个是障碍则不允许斜走。

4. 多机器人导航的冲突处理与协同调度

4.1 最简单可用的方案:固定优先级顺序规划

多机器人导航最直接的做法是按优先级依次规划。比如让1号机器人先规划,2号机器人规划时把1号机器人规划的整条路径格子都视为临时障碍,3号机器人则把1号和2号的路径都视为临时障碍。这样做三台机器人的路径在空间上完全不重叠,不会撞车。

但这个方案有明显的缺点:后规划的机器人很容易被前面的完全堵住,绕很远的路。我跑过一个案例,三台机器人从三个角落互相换位,按固定优先级规划后,第三台机器人的路径长度是第一台的三倍,看起来很不自然。所以固定优先级通常只适合机器人数量很少、空间比较宽敞的场景。它的优点是逻辑简单,作为起点非常好,方便你验证A*和整个仿真框架是否正常。

4.2 加入时间维度:时间窗与等待策略

我更推荐在路径规划的后期处理中加入“时间窗”概念。思路是:每台机器人的路径不只是格子序列,而是带时间戳的节点序列。1号机器人规划完后,把它在每个时刻占用的格子存下来;2号机器人规划时,每走一步都要检查当前时刻是否有其他机器人已经占用目标格子,如果占用就等待一拍,等格子释放了再走。

实现时可以维护一个单元格的占用时间表,把每台机器人路径上的(行, 列, 时间步)标记为占用。新机器人规划时,如果发现下一步的目标格子在下一步时间已被占用,就让当前机器人停留在原地,同时把停留产生的额外占用也写进时间表。这个策略在MATLAB里实现很直接,本质上是把一维路径变成二维时空路径,空间冲突就被转换成了时间冲突,然后用等待解决。

4.3 冲突检测方法对照

做多机器人导航,首先要能识别冲突,然后才能处理冲突。我把最常见的冲突类型和处理思路整理在下面:

冲突类型判断条件典型处理方式
节点冲突两台机器人在同一时间步到达同一格子低优先级机器人等待一拍,或重新规划
相向冲突两个机器人在相邻两个格子互相靠近,下一时刻会交换位置其中一台原地等待,避免“对穿”
交叉冲突两条路径在空间上交叉,但到达时间不同一般无需处理,只需要关注时间窗口重叠部分
追尾冲突同一条路径上前后机器人距离过近通过调整速度或插入等待来拉开间距

处理相向冲突很容易被忽略。举个例子,1号机器人在格子A,下一步要到格子B,2号机器人在格子B,下一步要到格子A,如果只看“同一时刻到达同一格”,它们并没有撞车,因为它们会交换位置。但现实中两个机器人如果尺寸足够大,交换位置这一步就会挤在一起。所以判断时要把相邻格子互换也识别为冲突,最简单的办法是检查两台机器人是否同时出现在相邻两格,并且移动方向相对。

4.4 动态重规划:遇到突发障碍怎么办

有时候地图不是完全静态的,或者路径预先规划好之后,某台机器人因为执行偏差晚了几步,原本的时间窗就废了。这种情况下可以在仿真循环里做局部检查:每一步判断机器人下一位置是否安全,如果不安全,就调用一次A*重新规划剩余路径。

重规划时要注意别把问题复杂化。我见过有人每走一步就重新规划一遍整条路径,其实没必要。网格地图没那么大时,每5到10个时间步检查一次就够了。重规划的起点是机器人当前位置,终点不变,其他机器人当前占用的格子可以作为临时障碍写入地图副本。这样做等于把全局规划变成了局部滚动规划,牺牲一点最优性,换来了对动态环境的适应能力。

4.5 仿真参数建议

多机器人导航的效果和几个参数关系很大,我整理了一份常用的起始参数,你可以按项目实际场景再去调:

参数建议值说明
地图尺寸20x20到50x50再大建议换成堆优化的A*
机器人数量3到6台数量更多时需要更复杂的调度策略
邻域模式8路径好看,但记得检查斜穿问题
等待时间上限10个时间步超过上限就强制重规划,避免死锁
路径插值步数每格分5步仿真动画平滑,也对应时间分辨率

密度是一个需要观察的核心指标。地图上有30%的区域是障碍、6台机器人同时跑,和只有3台机器人跑,冲突概率完全不同。机器人越密集,等待时间占比越高,路径越长。如果你的场景里机器人数量多,建议把地图扩大或把起点终点设置分散一些,否则调度算法的瓶颈很快会暴露。

5. 实操中遇到的常见问题与排查经验

5.1 路径为什么会穿墙或者斜穿障碍物

八连通模式下最容易出现斜穿障碍物的情况。比如障碍物在(3,4)和(4,3),地图上(3,3)是空地,(4,4)也是空地,八连通A*会让机器人从(3,3)斜着走到(4,4),看起来就是从两个障碍物之间挤过去了。如果机器人是圆形且半径很大,实际肯定蹭到障碍物。

解决办法是在邻居扩展时增加一个“拐角检查”。对于斜向邻居,检查与当前格和邻居格都相邻的正方向格,也就是水平垂直方向上的那两个格子是否都是空地;只要有一个是障碍,就禁止斜走。这个检查对路径安全性提升明显,强烈建议加进去。

5.2 有通路却提示找不到路径

这个问题我踩过几次,原因主要有三种。第一种是起点或终点本身被标记为障碍,这个检查几行代码就能做,但经常被漏掉。第二种是启发式函数返回了NaN或Inf,比如某个坐标越界,导致f值全部是Inf,开列表一直为空的假象。第三种是地图本身被障碍分隔成了多个不连通的区域,A*确实搜不到,这时要检查地图的连通性。

排查顺序建议是:先用一个单起点单终点的A*调用做测试,地图用一个2x2的无障碍小地图,如果这个简单的场景都跑不通,那肯定是代码问题而不是环境问题。等到小地图通了,再逐步换成实际地图。

5.3 多机器人跑到一半全部卡死

多机器人导航里很典型的问题是“循环等待”,也叫死锁。机器人A在等B让路,B在等C让路,C又在等A让路,结果谁都不动。我的经验是给等待加一个上限,等待步数超过上限就强制重新规划。重规划时可以把等待中的邻居视为障碍,这样至少能打破循环。

另外,多机器人路径规划的初始状态很重要。如果所有机器人的起点和终点在布局上就构成了一个交叉换位问题,那么无论怎么调度都会很难看。我遇到过三台机器人把起点和终点设置成了一个旋转对称的布局,结果每台机器人都必须穿到地图中心,绕路绕得极其痛苦。后来把终点换成分散布局,问题迎刃而解。

5.4 地图变大之后计算时间暴涨

用矩阵找最小值的方式在20x20地图上毫无压力,但一旦地图变成500x500,每次循环都扫描整张f矩阵,效率会低得让人怀疑人生。这时候要做两件事:一是把开列表改成最小堆,或者至少用Matlab的sort维护一个有序数组;二是考虑用Jump Point Search这类改进算法,它在栅格地图这样的均匀网格上会跳过大量中间节点,搜索速度一般能提升数倍。

如果不想改算法,还有两个工程上的技巧。一是降采样地图,把4个格子合并成1个,搜索完之后再映射回原分辨率;二是先做一次粗粒度全局规划,再用细粒度局部分段优化。这两种方式在实际项目里都很常用。

5.5 问题速查表

现象可能原因解决办法
路径斜穿障碍物八连通未做拐角检查检查斜向移动时相邻正方向格子
规划结果为空起点终点在障碍上,或地图不连通检查start和goal,测试小地图
多机器人互相堵死没有等待上限或路径交叉集中加等待上限,超时强制重规划
地图一大就很慢开列表用矩阵扫描实现换最小堆,或改为JPS算法
路径明显绕远启发式与移动代价不匹配四邻域配曼哈顿,八邻域配Octile
仿真动画跳格缺少路径点间插值每段路径做线性插值并drawnow

6. 后续扩展方向与个人体会

6.1 从Matlab仿真到真实机器人之间差了什么

这个Demo验证的是路径规划算法,但真实机器人和仿真之间还有不少距离。真实场景的地图通常不是手工填出来的,而是通过激光雷达或视觉SLAM构建的栅格地图,里面充满了噪声和动态障碍物。A*规划出来的路径通常是折线,但真实机器人底盘有运动学约束,不能瞬间转向,所以还需要在局部做一个运动规划,比如把路径平滑成曲线,或者用动态窗口法去跟踪全局路径。

我自己的做法是先把Matlab里规划的路径点导出成文本,每行一个坐标点,再在实体小车上用这套点做全局指引。真实运行时只保留全局路径的整体形状,具体怎么走、多快走,完全交给底层的局部避障模块处理。这个分工非常重要,全局规划负责在宏观上找一条可行路线,局部规划负责微观上的安全和运动。

6.2 算法层面的升级建议

如果你对这个项目有兴趣,下一步可以尝试在几个方向上做改动。第一个方向是单机寻路升级,把A*换成Jump Point Search,看看在相同地图上搜索效率提升了多少。第二个方向是地图表达升级,把栅格地图换成概率路图或快速扩展随机树,用来处理连续空间里栅格化带来的精度问题。第三个方向是多机调度升级,引入更系统的冲突解决算法,比如基于冲突搜索的CBS算法,它把多机问题拆成多棵搜索树,在高密度场景下效果比简单优先级法好很多。

每个方向都能独立做一个新项目。这个Demo最值得学习的地方不是代码本身,而是它把所有基础模块串起来了:环境建模、路径搜索、时间窗、可视化、参数分析。你在这套框架上换算法、换地图、换调度策略,都不会太费劲。

6.3 一点个人心得

我在实际调试这个Demo的过程中,最深的体会是:路径规划的核心不是“找一条路”,而是“让规划出来的路真正能用”。单机A*很容易让人产生错觉,以为路径通就是结束。加上多机器人之后,你才会明白时间、速度、等待、优先级这些东西对最终效果影响有多大。所以建议拿到代码后,不要只跑一边看效果交差,可以试着把机器人数量改成6台,把地图障碍物调密集一些,你会在调整优先级和等待策略的过程中,真正理解多机器人导航的难点。

如果你也打算在自己的项目里用这套代码,我最后再提醒一句:大胆改参数,但每次只改一个变量。我见过不少同学一次改地图、改启发式、改邻居数,出了问题根本不知道是哪一步引入的。保持“单一变量”的实验习惯,能让你调参时省掉大量时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询