做多无人机协同路径规划的同学,对Dubins路径应该都不陌生。但说实话,网上能跑的代码不少,能讲清楚“为什么这么设计”的却不多。尤其当场景从“空旷地图两点间飞一下”变成“复杂威胁环境多机协同”时,坑一下子就变多了。这几天我正好在调一套基于多段Dubins路径的协同规划Matlab代码,借这个机会把整个思路、实现细节和踩过的坑整理出来,给准备入手或正在改代码的朋友做个参考。
1. 问题到底长什么样
先把这个项目要解决的事情说清楚。简单来讲,我们要让多架无人机从各自起点飞到各自终点,同时满足四个约束:一是飞行路径要符合无人机最小转弯半径(运动学约束,也就是Dubins路径存在的意义);二是路径要避开所有威胁区域(雷达威胁区、禁飞区、地形障碍等);三是机间不能碰撞;四是最好能让所有无人机同时或者按指定时序到达,实现协同。
单机路径规划其实已经比较成熟了,A*、RRT*、人工势场法都能做。但一旦加上“协同”两个字,问题性质就变了:单机规划是“在约束下找一条可行路径”,多机协同是“在约束下找一组路径,且这组路径之间存在时空耦合”。这个耦合体现在两个层面——空间上不能互相撞,时间上要么同时到、要么错峰到。用教科书一点的话说,这是一个带有运动学约束的多智能体协同路径规划问题,直接求解的复杂度随无人机数量指数增长。工程上通常的做法是分而治之:先做单机路径规划,再做协同调整。
这套代码的思路就是典型的“分而治之”。每架无人机单独跑一条多段Dubins路径,绕开威胁区;然后通过调整每段路径的等待时间或速度曲线,实现时间协同。最后用Matlab把三维仿真结果画出来,直观验证方案可行性。
2. 为什么是多段Dubins路径
要理解多段Dubins,先得理解标准Dubins路径。
标准Dubins路径解决的是这么一个问题:给定起点位置和朝向、终点位置和朝向,以及一个最小转弯半径,找一条从起点到终点、满足曲率约束的最短路径。这个问题的解只有六种类型:RSR、RSL、LSR、LSL、RLR、LRL(R代表右转弯,L代表左转弯,S代表直线段)。也就是说,最短路径要么是“转弯—直行—转弯”,要么是“转弯—转弯—转弯”。这个结论是上世纪50年代Dubins证明的,到现在仍然是固定翼无人机路径规划的基础。
但标准Dubins路径有一个前提:两点之间是一条直线可达的。如果中间有威胁区挡着,标准Dubins路径直接穿过雷区,那就不能用。怎么办?思路很自然:把一条路径拆成多段,每一段都是标准的Dubins路径,段与段之间用航路点衔接,整个路径就是一条“多段Dubins路径”。这就是项目标题里“多段”二字的来源。
打个比方。标准Dubins路径像你在空旷停车场倒车入库,一把舵到底,路径最优;多段Dubins像在塞满障碍物的老小区里停车,先右打一把绕开垃圾桶,回正走一段,再左打一把进车位。虽然路径不是全局最优,但它是可行的,而且每一段都满足车辆的最小转弯半径约束。
多段Dubins路径的构建通常分两步走:
第一步,用某种搜索算法生成一串避开威胁区的航路点。这个算法可以是A*、RRT、遗传算法或者粒子群,核心目标只有一个:航路点序列本身不穿越威胁区。
第二步,对相邻航路点逐对生成标准Dubins路径,然后把各段首尾相接。这里要注意:前一终点的朝向必须等于后一始点的朝向,否则路径会“断”。所以航路点不仅要给出位置坐标,还要给出期望朝向角。
这套代码里采用的是基于随机采样的航路点生成方式,结合威胁区排斥策略。说白了就是:先把地图离散化,随机撒点;威胁区里的点直接丢弃;然后用启发式规则保留可能构成可行路径的点;最后根据航向约束生成多段Dubins曲线。这种方式的好处是简单直接,坏处是随机性比较大,可能需要多跑几次才能得到理想结果。实际应用中,我会在采样阶段加一些确定性种子,保证结果可复现。
3. 威胁环境怎么建模
复杂威胁环境的建模,直接决定路径规划的“难度天花板”。我在代码里看到的是圆形威胁区模型,这在实际工程中非常常见——雷达覆盖范围、防空火力范围、禁飞区通常都可以近似成圆形或圆形的并集。
圆形威胁区的数学表达很简单:平面上的一个圆,给定圆心坐标和半径,无人机质心进入这个圆即视为被探测或被击毁。但实际规划中不能只用原始半径判断,因为无人机本身有体积,而且需要留出安全余量。通常做法是对威胁区做膨胀处理:膨胀后的半径 = 原始半径 + 无人机安全半径 + 额外安全余量。这个膨胀系数怎么取,是工程上一个比较微妙的问题。取大了,可行路径变少,甚至没有可行解;取小了,路径贴着威胁区边缘飞,抗扰动能力差。我一般取安全余量等于0.5倍的最小转弯半径,这样既保证安全,又不至于过度牺牲可行空间。
多威胁区并存时,还有一个隐蔽的问题:威胁区之间的狭窄通道。两个膨胀后的威胁区如果靠得近,它们之间的“可通行走廊”会变得很窄,甚至完全封闭。这时候就算采样到航路点,Dubins路径也可能因为转弯半径太大穿进威胁区。
我在调试中专门验证过这个问题:两个半径1.5km的威胁区,圆心距2.8km时,膨胀后(安全余量0.5km)的威胁区刚好相切,中间没有空隙;圆心距3.2km时,中间只有0.4km宽的走廊,而无人机最小转弯半径对应的转弯直径是0.6km,也就是说无人机根本转不过去。这种情况下,即使采样算法找到了航路点,路径生成阶段也会失败。所以实际使用时,我会在建模阶段先做一个连通性预判:两威胁区膨胀后是否相交;如果相交,直接视为同一簇障碍,路径规划绕这一簇整体走,而不是试图从缝隙里钻过去。
4. 核心模块拆解与参数设计
这套代码从结构上可以拆成几个独立模块:地图与威胁区定义、Dubins路径计算、威胁检测、航路点搜索、多机协同调度、仿真可视化。下面逐个过一遍。
4.1 Dubins路径计算模块
这个模块是整个系统的“发动机”,输入两个状态(位置加朝向角)和最小转弯半径,输出一条标准Dubins路径。我看的这套代码用的是经典几何解法,取六种路径类型中长度最短的那条作为结果。
function [path, len] = dubins_path(start, goal, radius) % start = [x0, y0, yaw0] % goal = [x1, y1, yaw1] % radius: 最小转弯半径 % 返回 path: 采样点序列, len: 路径长度六种类型的计算公式网上很多,这里不展开。但有几个实现细节容易被忽视:
角度归一化。航向角如果不归一化到[-pi, pi],计算相对角度时会出问题。比如航向角是179度和-179度,它们实际只差2度,但直接相减得到358度,会导致路径类型判断错误。代码里必须在计算前把所有角度归一化。
采样间距。Dubins路径算出来是数学曲线,仿真时需要离散成点序列。采样间距直接影响后续威胁检测的精度。间距太大,可能漏检细小的威胁区穿插;间距太小,计算量爆炸。经验值:采样间距取最小转弯半径的1/20到1/50。比如转弯半径是0.6km,采样间距取15到30米就够用。
数值稳定性。当起终点距离很近、或者航向差很小时,部分路径类型的几何计算会出现分母接近0的情况。必须在代码里加保护:如果某类型计算出现NaN或者Inf,直接跳过该类型。
4.2 航段威胁检测模块
这个模块要做的事情很纯粹:判断一段Dubins路径是否与某个威胁区相交。但实现上有讲究——不能只检测采样点是否在威胁区内,还要检测采样点之间的路径段是否穿过了威胁区。
为什么?因为采样点间距即使很小,在两个采样点之间的曲线段仍然可能擦过威胁区边缘。稳妥的做法是:先判断整段路径的包围盒是否与威胁区相交;如果相交,再做逐点检测;如果逐点检测发现没有点落在威胁区内,再用更密的采样做二次确认。
这其实是一个典型的“多级碰撞检测”思路,第一级粗筛,第二级精检。我在实际调试中发现,只做逐点检测时,偶发出现“路径看起来没进威胁区,但仿真时被打下来了”的情况,问题就出在这个擦边穿越上。后来加了二次确认,问题才彻底解决。
function isCollide = check_collision(pathPoints, threats) % pathPoints: 采样点集合 % threats: 威胁区结构体数组 % 先粗筛后精检, 返回是否碰撞4.3 航路点搜索模块
航路点搜索是这个系统里最“智能”的部分,也是算法复杂度的大头。代码里的做法是:基于随机采样的迭代搜索,每次迭代生成一个候选航路点序列,用威胁检测模块验证可行性,再用Dubins路径模块计算总长度,以长度最短为优化目标迭代保留最优解。
这里有个参数特别关键:航路点的数量。航路点越少,路径越短、越平滑,但绕障能力越差;航路点越多,绕障能力越强,但路径会变得臃肿(每段都要转弯),而且计算时间变长。我在调参时试过:同样一张地图,3个航路点(共4段Dubins)跑出来的路径总长约8.4km,而6个航路点(共7段)跑出来总长11.2km,而且6个航路点的路径有明显的“多余动作”——明明直飞就行,非要转两下。所以我的建议是:先用较少航路点跑,如果路径不可行,再逐步增加。不要一上来就设很多航路点。
4.4 多机协同调度模块
单机路径算好后,多机协同的核心问题就变成了“怎么让这些路径在时间上配合起来”。
最朴素的做法是:如果所有无人机的路径长度差距不大,直接让它们按相同速度起飞,就能近似同时到达。但现实中路径长度差异往往很大——一架绕了一大圈,一架几乎是直线,如果速度一样,先到的要等后到的,协同效果差。
更好的做法是为每架无人机设计速度曲线或者等待策略。这套代码里采用的方法是:在较长路径的无人机上设置“待机点”,通过调整Dubins路径的弧长分配来弥补时间差。具体做法是:找出最长路径作为基准,计算每架无人机需要的“额外等待时间”,然后在起点或某个航路点附近插入一圈360度盘旋(在Dubins路径中就是一圈完整的圆),从而增加路径长度而不改变终点状态。
这个“空中待机”策略在工程上很实用。飞机不能像汽车一样踩刹车停在半路,但可以让它绕圈飞。不过要注意:绕圈飞行意味着暴露在威胁区的时间更久,所以绕圈点必须选在没有威胁区的空域。最好是在每个航路点搜索阶段就把“可盘旋空域”作为附加约束考虑进去。
function schedule = compute_schedule(paths, v_mean) % paths: 每架无人机的路径 % v_mean: 平均巡航速度 % 返回 schedule: 每架无人机的起飞时间/速度调整4.5 仿真可视化模块
代码里用Matlab的绘图函数做了二维平面动态仿真。无人机用三角符号表示,威胁区用半透明圆表示,路径用不同颜色曲线绘制。
一个比较实用的可视化技巧是:把时间维度用颜色渐变表示。比如路径上的点,颜色从蓝到红渐变,蓝色代表起点,红色代表终点。这样一眼就能看出无人机在不同时间点的位置分布,比单纯画一条线直观得多。
另外要把“同时到达”这个协同效果直观展示出来,画“到达时间柱状图”比画路径更有说服力。我看到不少代码只画路径不画时间,协同效果没有直观证据,这在论文审稿时是比较吃亏的。
5. 完整仿真流程与代码运行逻辑
这一节把完整流程串一遍,方便你直接对照代码理解。
第一步,加载地图参数。包括地图尺寸、威胁区圆心和半径列表、无人机初始位置和朝向、目标位置和朝向、最小转弯半径、巡航速度。
第二步,构建威胁区模型。对每个威胁区做膨胀处理,膨胀后的威胁区用于后续所有碰撞检测。
第三步,单机路径规划。对每架无人机独立执行航路点搜索,生成可行航路点序列,然后逐段生成Dubins路径。每段Dubins路径都要过威胁检测模块,不过就重新搜索。
第四步,多机协同调度。对比各路径的长度,计算时间差,确定每架无人机的起飞时间或盘旋策略。这里有一个细节:如果某架无人机的路径长度显著小于最长路径,直接通过加盘旋圈来补齐时间,可能使路径长度严重虚增、燃油浪费。更优做法是“路径长度预调整”——在航路点搜索阶段,就对路径长度设定一个下限,让所有单机路径长度尽量接近。这个做法在仿真结果上更漂亮。
第五步,时间同步仿真。所有无人机按调度时间依次起飞,在统一时间轴上推进。每推进一个时间步,计算各无人机当前位置,检测是否进入威胁区、是否与他机距离小于安全间距。出现违规则记录违规信息并停止仿真(或者标记后再继续)。
第六步,结果输出。打印路径长度、飞行时间、计算耗时、碰撞检查结果,绘图展示最终路径和动态仿真过程。
下面是主循环的核心伪码:
% 主仿真循环 for t = time_steps for i = 1:N % 按调度时间判断是否已起飞 if t < schedule(i) continue; end % 沿路径推进 pos_i = interpolate(paths{i}, t - schedule(i), v_mean); % 威胁检测 if in_threat_zone(pos_i) warning('UAV%d entered threat zone at t=%.2f', i, t); end % 机间距离检测 for j = i+1:N if norm(pos_i - pos_j) < safety_distance warning('UAV%d and UAV%d too close', i, j); end end end end这里面interpolate函数比较关键:它根据无人机速度和路径弧长,把时间映射到路径点位置。Dubins路径是分段曲线(圆弧+直线),弧长计算要分段算。直线段就是两点距离,圆弧段就是半径乘以转角。插值时先判断当前弧长落在哪一段,再在该段内按比例插值。这个逻辑不复杂,但是容易写错,尤其在多段Dubins拼接处,弧长累加时容易漏掉前一端口径。
6. 复现时最容易踩的坑
这几个坑是我实际调代码时踩过的,网上教程基本不会提,这里详细说一下。
6.1 威胁区膨胀过度导致无可解路径
这是最“隐性”的坑。看起来是算法不行,找不到路径,实际上是威胁区膨胀太狠,把可行走廊全堵死了。我遇到过一组参数:威胁区原始半径1.6km,膨胀系数取1.5,膨胀后半径2.4km;三个威胁区一膨胀,整张地图被切成了孤岛,起点到终点在拓扑上已经不连通了。这种情况下,不管什么算法都搜不出路径。
排查方法:把膨胀后的威胁区画出来,人工检查起点到终点是否还存在视觉上的通路。如果肉眼都看不出通路,那就不是算法问题,是建模参数问题。此时应该减小膨胀系数,或者允许路径从威胁区边缘较近处通过。
6.2 Dubins路径在航路点处“断裂”
严格说这不是代码bug,而是约束建模不完整。当两段Dubins路径拼接时,如果前一段的重点朝向和后一段的起点朝向不一致,整个路径会在拼接点产生一个角度跳变。这种跳变在几何上会形成折线,看起来路径是连续的,但物理上无人机是无法瞬间改变航向的。
解决这个问题的关键是:航路点搜索阶段就要把朝向角作为优化变量,而不是只优化位置。我看过的代码里,有的把朝向角作为附加维度随机生成,然后用“前一终点朝向等于后一起点朝向”作为约束条件筛选;有的则引入“直线过渡段”来消化朝向差——在两段Dubins之间插入一个自由直线段,用于平滑过渡。
我的建议是优先用第二种:直线过渡段。原因很简单,它不增加搜索维度,只是把约束放宽了。插入直线段后,整条路径变成“Dubins—直线—Dubins”,本质上仍然是分段可飞路径。但你需要注意:直线段越长,路径越“飘”,所以直线段长度要设上限。
6.3 时间同步只看起飞时间不看实际飞行时间
我在第一次跑通多机协同仿真时犯过这个错:起飞时间差算好了,仿真也跑了,雷达威胁也避了,但最后发现“同时到达”根本没有实现。原因是每架无人机的路径不仅有长度差异,还有曲率差异。曲率大的路径,在同样速度下,转弯耗时其实更多(速度是线速度,转弯时沿弧线运动,线速度不变但前进方向变化更快,在风场扰动下实际地速会有所损失)。
严谨的做法是:用每段路径的弧长除以期望速度计算飞行时间,而不是直接用“总长度除以平均速度”。如果仿真中引入风场模型,还要考虑顺风逆风对地速的影响。多数仿真代码会简化这个过程,但既然要做到“协同”,时间计算这块不能简化过头。如果要考虑风,至少要能在调度模块里设置“等效速度”参数。
6.4 随机算法的可复现性问题
航路点搜索用了随机采样,直接后果是每次跑代码结果都不同。这在调试阶段很要命——你改了一个参数,结果变了,你无法判断是参数引起的还是随机噪声引起的。解决方法是固定随机种子。Matlab里用rng(2024)固定种子,后续所有随机数都从这个种子生成,结果就可复现了。项目演示时一定要固定种子,否则评审或者交作业时两次跑出的路径不一样,非常尴尬。
6.5 可视化阶段性能瓶颈
如果路径采样点很多、无人机数量也多、仿真步长又细,Matlab绘图会变得极其卡顿。我的经验是:动态仿真时,每帧只更新无人机位置和已飞过的路径段,不要把整条路径重新绘制。老代码常见的做法是每帧plot全部路径,这在N=3、采样点几千时还能忍受,N=6、采样点上万时基本卡成幻灯片。改用set更新已有图形对象的XData和YData,速度能快一个数量级。
7. 代码架构的优化方向和扩展思路
如果这套代码已经跑通了,想继续深入,可以从下面几个方向扩展。
第一个方向,从二维扩展到三维。真实无人机飞行高度也是一个维度,威胁区在三维空间中通常是半球或者圆柱体。三维Dubins路径的数学复杂度高不少,但工程上常用做法是分层规划:先在高空平面做二维Dubins规划,再在垂直方向做高度剖面设计,最后把两者合成三维路径。这个近似方法虽然不是数学最优,但工程上足够稳定。
第二个方向,引入动态威胁。目前这套代码的威胁区是固定的,真实战场中雷达探测区域可能随时间变化(比如扫描扇区旋转)。动态威胁环境下,路径规划需要具备“重规划”能力——无人机沿路径飞行过程中,如果检测到前方威胁状态变化,需要局部重新搜索航路点,生成新的Dubins路径段。这个扩展对代码架构的考验比较大,因为要区分“全局规划”和“局部重规划”两个层次。
实际上动态威胁的建模可以将威胁区的状态定义为一个随时间变化的函数,比如扫描雷达的探测区域会周期性扫过某个扇区。无人机可以选择等待扇区转过去再通过,也可以选择从另一个方向绕行。这种时间窗口决策很依赖第三章说的协同调度模块——不同的到达时间会影响面对动态威胁窗口的相位。
第三个方向,多目标优化。当前的优化目标只有“路径长度最短”或者“时间同步”,实际任务中可能还要考虑油耗、被发现概率、飞行高度变化幅度等多个目标。把Dubins路径规划从单目标变成多目标,可以用NSGA-II这类多目标进化算法。框架上,只需要把航路点搜索模块的目标函数从单值改成向量,其他部分基本可以复用。
第四个方向,与编队控制结合。路径规划解决的是“飞哪里”,编队控制解决的是“怎么飞在一起”。如果在规划阶段就把编队约束加进去(比如无人机之间保持一定的相对队形),协同会从“同时到达”升级为“编队到达”。这个方向对算法复杂度要求比较高,但只要基础的多段Dubins模型跑得通,扩展是顺理成章的事。
8. 我的实操心得
最后说几句实在的。
多段Dubins路径协同规划这个选题,在课程项目、毕业设计、甚至小课题研究里都非常适合,因为它自带“学术外衣”——有数学模型、有约束条件、有优化目标、有可视化结果,而且Matlab生态成熟,代码量不大,一两个星期就能跑出像样的仿真结果。但如果你只是把开源代码下载下来跑一遍,那就只能学到“调参数的技能”,核心的运动学建模、威胁建模、协同调度逻辑才是值得深挖的地方。
我个人调试过程中的感受是:这个系统百分之六十的复杂度不在Dubins数学公式里,而在工程细节里。航向角归一化没做,路径就断;威胁区膨胀系数没调对,路径就无解;时间同步只算长度不算曲率,协同就失效。这些细节都不是“算法创新”,但每一个都能让仿真结果从不可用变成可用,从“能跑”变成“能看”。
如果你打算在这套代码基础上做改进,我建议从“航路点与威胁区的约束建模”入手。现有的随机采样方式效率有限,你可以试试在采样时加入威胁区排斥势场信息,让候选航路点天然落在安全区域中,而不是靠运气撒点后用碰撞检测筛掉。这会显著提升搜索效率和路径成功率。
还有一个实用小技巧:调参时把威胁区半径、膨胀系数、航路点数量、采样间距这几个参数做成全局变量,放在文件顶部统一管理。我用过一个参数矩阵的方法:行是无人机编号,列是各参数,跑批量仿真时非常方便。比改代码内部数值效率高太多了。
最后再分享一个小经验,路径规划的仿真结果记得保留原始随机种子、参数文件、输出图的对应关系。我至少遇到过三次这种情况:调参调出很好的路径,但忘了保存参数,第二天怎么跑都复现不了。做个简单的日志函数,把每组参数和结果自动保存成文件,能让你的调试效率提升一大截。