ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

无人机集群协同攻击仿真:Dubins路径与候选集任务分配

无人机集群协同攻击仿真:Dubins路径与候选集任务分配

搞无人机集群仿真的同行应该都有同感:单机的路径规划和姿态控制都不算难,难的是让十几架无人机在一个任务区域里既能分散执行各自的任务,又能在关键节点快速协同起来,并且整套流程还必须符合真实的运动学约束。最近我花了不少时间调试一套基于Dubin路径和候选集的Matlab仿真程序,专门研究无人机集群协同攻击固定目标时的完整任务链路。这里说的Dubin路径,其实就是带最小转弯半径约束的二维最短路径方法,很多中文资料里简写成Dubin,业界标准写法是Dubins,本文后续统一用正确拼写。

这个程序覆盖了集群协同中最核心的几个环节:目标搜索、冲突避免、联盟组建和任务执行,并且把无人机资源分配像一条暗线一样嵌进了每个环节。说白了,它解决的是一个“在有限油量、有限弹药、有限感知条件下,如何让多架无人机自己商量着把攻击任务干完”的仿真验证问题。如果你正在研究集群任务分配、协同航迹规划,或者只是想找一套能跑通闭环的协同攻击Demo做二次开发,这套程序的整体思路可以直接拿来做底座。

我这篇文章不打算只贴代码,而是把我在梳理、运行、修改这套程序的过程中总结出的设计逻辑和调试经验一并写出来,方便你少走点弯路。

1. 先把这个题拆开:仿真到底在仿什么

1.1 协同攻击主链路的来龙去脉

一套完整的协同攻击流程,在真实系统里会牵扯到大量通信、感知和决策逻辑,但放到仿真中,最核心的其实就是五件事:搜索、避碰、组盟、执行、分配。这五个环节并不是简单的一条线走完,而是在同一段时间内反复循环、互相耦合的。

举一个具体场景:6架无人机进入一个10km×10km的任务区域,前两分钟它们都在做区域扫描,此时没有发现任何目标,所以不存在联盟问题,也没有任务分配问题,仿真模型只需要处理搜索路径和机间避碰。可一旦传感器扫到了目标,流程立刻从“搜索模式”切换成“目标确认+分配模式”。而且分配不是只做一次:目标可能移动,某架无人机可能在攻击途中被拦截,弹药剩余量会持续变化,所以分配需要按固定周期反复触发,每次决策都必须基于最新的状态数据。

这也是这套仿真程序最值得琢磨的地方:它把“资源分配”从单独一个模块,变成了贯穿整个主循环的约束线。每次构建候选集、每次计算飞行代价、每次决定联盟成员,本质上都在回答同一个问题——在资源有限的前提下,谁去做哪个目标最划算。我认为这是理解整套代码最重要的入口,比单纯看懂某个算法函数更有价值。

1.2 Dubins路径为什么是“绕不开的选项”

先补充一点背景。对固定翼无人机来说,最小转弯半径是硬约束,它决定了无人机不能像多旋翼那样原地掉头。简化理解:一个带航向角的无人机在平面上从A点运动到B点,同时要求到达时对准指定的方向,最短的可行路径往往不是直线,而是“圆弧-直线-圆弧”的组合。这就是Dubins路径。它按转向方向组合分成几大类:LSL、LSR、RSL、RSR,分别代表左转、直行、右转的排列组合,另外还有RLR、LRL这类包含连续转圈的路径。

为什么协同攻击仿真特别需要Dubins路径?因为攻击任务和普通巡飞不一样,它对末端航向有明确要求。比如打击某个地面目标,最后一截最好沿固定方位角进入,尽可能减少暴露窗口。如果你只在仿真里用直线连接出发点和目标点,无人机到达目标上空后可能头朝一个完全不可用的方向,真实环境下还得转个大圈才能进入攻击航线,这会严重影响时间协同和生存率。把Dubins路径放在规划层,就等于把运动学约束前置了,后面所有的时间估算和代价计算才会有实际意义。

候选集的出现则是在解决另一个问题:全连接分配太暴力。假设有6架无人机、2个待攻击目标,每架无人机可以选任意目标,再加上“不出击”的情况,组合空间已经不小。如果再叠加路径重规划、威胁避让、资源约束,直接做全局优化的迭代开销在Matlab里会非常吃力。所以工程上通常先做一轮快速筛选,把那些距离太远、油量不足、航向差太大的不可行项砍掉,留下的集合就是候选集。后续的联盟组建和任务分配,都只在候选集上做决策。

这样设计的好处很明显:第一,大问题被快速瘦身,计算负荷明显下降;第二,筛选条件本身就是对决策逻辑的显式约束,出了问题容易定位;第三,候选集规模越小,分配算法的实时性越好,仿真整体跑起来就越流畅。

2. 技术选型与模块设计:每一层都在解决什么问题

2.1 任务坐标系与无人机运动学模型

搭这套仿真之前,首先得把顶层抽象做对。我的做法是采用二维任务坐标系,x轴朝东、y轴朝北,所有位置和距离都基于这个坐标系计算。为什么不直接上三维?因为在“协同攻击地面目标”这个场景里,高度信息对任务决策的影响,远小于位置、航向、速度和剩余油量这些平动状态。先做二维能大幅降低模型复杂度,把研究精力集中在决策与协同逻辑上;等二维链路完全跑通,再接三维模型也只是换状态方程的事。

无人机运动学我用最常用的二阶模型,状态量为位置(x, y)、航向角θ和速度v。左转时θ按正方向变化,速度模长固定。这里有一个容易踩的坑:Matlab里的三角函数默认用弧度,而界面和人阅读习惯用度。我建议代码内部统一用弧度,只在画图和输出时换算成度,否则Dubins路径解算很容易出现诡异的角度结果。

为了把“资源约束”落到实处,我给每架无人机挂了一个资源结构体,里面包含剩余航程、弹药数量、探测半径、最小转弯半径四类字段。最小转弯半径是Dubins路径计算的核心参数,它跟机型和飞行速度绑定,预设过程中不要随意改,否则算出的路径会与实际速度曲线匹配不上,导致后面所有时间协同都是以错误路径长度为基础的。

2.2 目标搜索阶段的实现思路

搜索阶段看起来简单,其实是整套仿真中容易出bug的地方。通用方案是把任务区域栅格化,每架无人机沿自己负责的子区域做扫描飞行,机载传感器落在地图上的是一个以自身位置为圆心、探测半径R为半径的圆,只要目标出现在圆内,就标记为“已发现”。

这个阶段有两个值得注意的细节。第一是扫描覆盖率的计算:怎么判断任务区域已经被搜得差不多?我的做法是维护一个全局覆盖矩阵,每次位置刷新,把传感器覆盖范围内的栅格置为1,统计全局覆盖率。当覆盖率超过设定阈值(比如0.9)且始终没有发现目标时,可以判定“任务区域内无目标”,提前终止搜索流程,转入待命或返航。第二是搜索轨迹本身也要满足Dubins约束:很多初版代码会让无人机在格点之间走横平竖直的线段,结果路径长度和实际飞行时间严重失真,后面所有任务分配的时间预测都会跟着错。搜索路径建议直接用多段Dubins路径串联,步长设置为最小转弯半径的整数倍,这样既真实又方便计算能耗。

2.3 冲突避免:谁先避、怎么避

多机同时在场,避碰是必须处理的现实约束。我在这个程序里综合了两种方案:优先级避让和速度障碍法。

速度障碍法(VO)解决的是“相对运动碰撞判定”问题:把邻居无人机视为一个圆形障碍,若本机当前速度矢量与邻居相对速度组成的速度矢量落入碰撞圆锥范围内,就说明存在碰撞风险,需要调整速度方向。这个方法计算量小,对分布式场景很友好,缺点是密集队形下容易出现振荡。所以我额外引入一层优先级:按无人机编号设定固定优先级,低优先级在检测到碰撞风险时主动避让,高优先级保持主航迹。这样在两架同时接近时,不会出现双方互相让路导致死锁或原地绕圈。

我实测下来,这套组合在5到8架无人机的规模里表现很稳定,工程应用带来的额外绕行距离大约在5%到15%之间,完全可以接受。如果无人机数量超过10架,建议再引入通信约束下的编队内部排序逻辑,否则碰撞规避的路径调整会频繁触发,整体航程损耗会明显上升。

3. 候选集与任务分配的实现细节

3.1 候选集的构建逻辑

候选集的构建是整个分配层的入口,设计得好不好,直接决定后续分配算法的表现。我按四步过滤来做:

过滤层级判断条件主要作用
探测过滤无人机与目标直线距离 ≤ 探测半径保证目标对无人机可见,硬前提
资源过滤剩余航程覆盖往返路径估值,弹药数≥1剔除无法完成任务的无人机
可行性过滤Dubins路径长度 ≤ 剩余航程的80%排除“直线近、绕行远”的假候选
代价过滤综合代价低于设定阈值控制候选集规模,提升分配效率

补充两个容易忽略的点。

第一,可行性过滤里“Dubins路径长度”不能用直线距离代替。某些目标点虽然直线距离很近,但受威胁区边界限制或末端航向约束,实际路径需要绕很大一圈,资源消耗远超直线估算。如果不做这一步,候选集里会混入大量物理上完不成任务的数据,分配算法再优秀也没用,因为输入本身就是脏的。

第二,候选集的“守门”阈值设置。阈值太松,候选集规模变大,分配计算时间上升;阈值太紧,可能出现一个目标没有任何无人机可选,任务直接死锁。我建议先用固定距离作为第一版阈值,跑几轮仿真观察候选集的规模和分布,再逐步收紧代价上限。切记不要一开始就追求“完美候选集”,调参要一步一步来。

3.2 联盟组建:从候选集到攻击联盟

联盟组建的输入是候选集,输出是“哪个联盟负责攻击哪个目标、联盟内部每架无人机扮演什么角色”。对于中等威胁目标,通常2到3架无人机组成联盟就够了;高价值目标可能需要4架以上,并且要区分主攻、压制和侦察。

我采用的是“拍卖+局部交换”的两段式方案。拍卖阶段,每架无人机根据自身代价函数对所有可见目标出价,出价越低代表“更愿意去”,目标管理器统一收齐后做一次任务初分配。交换阶段,对已经形成的分配方案,遍历所有两两无人机组合,尝试交换任务,只要交换之后总代价下降就接受交换。这个办法比纯匈牙利算法更容易实现,也比纯贪婪分配更接近全局最优,而且Matlab环境下运行效率很高,几十毫秒能完成一轮。

两段式方案还有一个额外的好处:可替换性强。初分配算法可以换,交换策略也可以换,模块间耦合度低。如果你后面想改成合同网协议(CNP)或者粒子群优化做分配,不用动其他组件,只要保持输入输出接口一致就行。

3.3 资源分配的代价函数设计

资源分配的核心是设计一个代价函数,把所有零散约束统一量化到一个可比较的数值里。我在程序里用的简化版本如下:

J = w1 * t_flight / t_max + w2 * Q_exposure / Q_max + w3 * (1 - R / R0)

其中t_flight是从开始到执行完任务需要的飞行时间估计,Q_exposure是航迹在威胁区内的累计暴露量,R是当前剩余航程,R0是初始最大航程。括号内的部分表达的是:资源越少,该无人机出任务的机会成本越高,所以要被惩罚。w1、w2、w3是权重,需要在仿真前反复调整标定。

这里举一个实际例子。假设无人机A距离目标X有3000米,但去X的航迹要穿过一片高威胁区域;无人机B距离目标Y有4000米,威胁区暴露路径却很短。如果只按距离优先,无人机A会被派去攻击X,但算上威胁代价后,把A派往Y、B派往X反而总代价更低。这就是为什么代价函数不能只写单项——我在初版程序里就吃过这个亏,权重全都给了飞行时间,结果联盟路径总往威胁区里钻,看着飞行时间很短,实际生存性几乎为零。

调参的经验流程:先把三个代价项分别用各自最大值归一化到0到1区间,然后设定初始权重w1=0.4、w2=0.4、w3=0.2,跑一轮仿真观察平均暴露量和资源消耗情况,再按场景需求调整。这个环节没有捷径,只能依赖具体场景反复标定,但方法本身是通用的,换目标换机型都能套用。

4. Matlab仿真程序实操走一遍

4.1 初始化参数配置建议

先贴一份我跑通完整闭环用的初始参数配置。代码不长,但每个参数的设置逻辑我都会解释清楚。

% 仿真区域与目标配置 mapWidth = 10000; mapHeight = 10000; % 任务区域 10km x 10km targets = struct('pos', {[2000 3000], [7500 6500]}, ... 'threatLevel', {0.6, 1.0}, 'alive', {1, 1}); targetNum = 2; % 无人机初始化 uavNum = 6; uavState = zeros(uavNum, 4); % x, y, heading(rad), speed(m/s) uavTank = 3200 * ones(1, uavNum); % 剩余航程(米) uavWeapon = 2 * ones(1, uavNum); % 携带弹药数 uavDetectR = 800 * ones(1, uavNum); % 传感器探测半径(米) uavTurnR = 200; % 最小转弯半径(米) for i = 1:uavNum uavState(i, :) = [600 + (i-1)*300, 800, pi/4 + (i-1)*0.1, 50]; end

解释几个容易踩坑的参数。

剩余航程3200米,这个数值是按区域对角线长度估算的:10km×10km区域,无人机需要跨区搜索、威胁绕行和任务后返航,如果初始航程给太小,搜索阶段就会频繁“返航”,协同攻击根本组织不起来;如果给太大,资源约束等于摆设。我一般是先按区域对角线的1/3到1/2量级设置,再通过多次仿真观察资源利用率来校准。

弹药数量初始设为2,是为了让“资源随任务消耗”这个约束在仿真中充分体现。如果给10发,一直打不完,资源分配环节就很难暴露问题,也就没法学到真正有用的调参经验。

还有一个细节:我把最小转弯半径设为固定值200米,这对应特定速度区间下的机动能力。如果你修改速度,建议同步调整转弯半径,一套气动参数配一套运动参数,不要拆开乱配。

4.2 主循环与状态机设计

仿真主循环我用一个for循环驱动,步长取0.1秒,结束条件可以是仿真时间到、全部目标被摧毁并确认,或者场上无可执行任务。主循环内部按“感知—决策—执行”三个层面组织,大致结构如下:

for t = 0:dt:T_max % 感知层:更新目标状态与发现情况 discovered = updateDetection(uavState, targets, uavDetectR); % 决策层:周期性触发任务重新分配 if mod(t, reassignPeriod) < dt candSet = buildCandidateSet(uavState, targets, uavTank, uavWeapon); [tasks, formation] = allianceAllocation(candSet, targets); end % 执行层:每架无人机按任务目标生成并跟踪Dubins路径 for i = 1:uavNum if ~isempty(tasks(i).targetId) p = planDubinsPath(uavState(i,:), getTargetState(tasks(i).targetId), uavTurnR); uavState(i, :) = followPath(uavState(i,:), p, dt); else uavState(i,:) = searchMotion(uavState(i,:), uavTurnR, dt); end end % 冲突躲避:检测碰撞风险,调整个别的速度方向 avoidCommand = velocityObstacleAvoidance(uavState, uavDetectR * 0.6); uavState = applyAvoidance(uavState, avoidCommand, dt); % 命中判定与资源扣除 [targets, uavWeapon, uavTank] = attackCheck(tasks, uavState, targets, uavWeapon, uavTank); end

建议在每架无人机的状态里额外加一个missionMode字段,用来标记当前所处阶段:空闲IDLE、搜索SEARCH、接近APPROACH、攻击ATTACK、返航RETURN。这个状态字段的价值在于让各功能模块只在对应状态下执行,比如只有APPROACH状态才调用Dubins路径跟踪,只有ATTACK状态才做命中判定。这样既保证逻辑清晰,也方便调试时通过打印状态转移轨线快速定位问题。

关于重新分配周期,这里有一个经验值。我最初把reassignPeriod设为1秒,结果整个集群频繁切换任务,Dubins路径重算的调用频率极高,整体性能急剧下降。后来改成10秒一个周期,并给任务切换增加惩罚系数,才把分配抖动控制住。这个值跟任务区域大小和无人机速度有关,需要按场景实测。

4.3 Dubins路径计算的关键代码片段

Dubins路径的完整推导和实现细节,网上资料和开源代码都很丰富,我这边给一个能直接用的骨架结构。重点是理解四条经典候选路径(LSL、LSR、RSL、RSR)的长度比较逻辑。

function [path, len] = planDubinsPath(p0, theta0, p1, theta1, R) % p0, p1 为起终点坐标,theta0, theta1 为起止航向角(弧度) % R 为最小转弯半径 % 分别计算四条候选路径 LSL = computeDubinsSegment(p0, theta0, p1, theta1, R, 'LSL'); LSR = computeDubinsSegment(p0, theta0, p1, theta1, R, 'LSR'); RSL = computeDubinsSegment(p0, theta0, p1, theta1, R, 'RSL'); RSR = computeDubinsSegment(p0, theta0, p1, theta1, R, 'RSR'); [len, idx] = min([LSL.L, LSR.L, RSL.L, RSR.L]); allPaths = {LSL, LSR, RSL, RSR}; path = samplePath(allPaths{idx}, R); end

实际使用中有三个容易翻车的点。

第一,候选路径中可能出现某一段长度为负值的情况,代表该几何组合不可行,计算总长时直接置为inf,不要参与最小值比较,否则会选出一条物理上无法飞行的路径。第二,路径采样密度决定轨迹平滑程度,我一般取0.5米到1米间隔,采样太稀疏会导致后面的避碰控制很难收敛,路径跟踪误差会被持续放大。第三,所有角度运算尽量做归一化,把航向限制在[-π, π]或[0, 2π)区间,避免从359度绕到1度时产生异常的路径跳变。

5. 调试路上踩过的那些坑

5.1 候选集为空的死局

第一版程序跑起来后,经常出现一种尴尬情况:无人机在搜索模式里绕地图转了半天,一直构建不出候选集;等目标终于被发现时,部分无人机已经因为油量不足被判定返航,剩余无人机的候选集规模也很小,最终攻击效果很差。

排查后发现有两个原因。一是搜索路径的耗油估算偏乐观,我没有把Dubins绕行距离计入油耗,导致实际可执行任务的无人机数量比预期少;二是候选集里的“资源比例阈值”设得太高,相当于自动淘汰了大部分无人机。修复方式是先跑一遍不带攻击任务的纯搜索仿真,统计实际平均耗油率,再把阈值调整到适配值。同时还加了兜底逻辑:若候选集为空,不强制进入分配流程,而是让所有无人机继续按搜索模式飞行,等下一个分配周期再试,这样主循环不会空转或报错。

5.2 航向角导致路径算错

Dubins路径计算对航向角极其敏感,我遇到最典型的错误是角度单位混用。有一次把目标航向角用度数传进了用弧度写好的Dubins计算函数,结果所有候选路径长度都被算出天文数字,集群直接判定“不可达”,整轮攻击没有产生一次有效打击。排查花了大半天,最后靠逐个打印中间角度变量才定位。

另一个更隐蔽的问题是对角度边界处理不当。无人机航向从180度附近往181度变化时,数值小幅变化但实际方向跨越了π边界。如果你在差分或归一化时用了不连续的角度表示,速度障碍法里的速度矢量方向会瞬间乱掉,导致无人机突然转向。我的习惯是写一个统一的角度归一化工具函数,所有模块都走同一个接口,不直接操作原始角度值,这个习惯确实帮我省了很多事。

5.3 可视化与性能优化的取舍

Matlab仿真带着实时动画跑,5到6架无人机时还勉强能接受,但如果一直开着drawnow和figure刷新,迭代到10架以上时帧率会掉得厉害,调试效率大大降低。我的建议是把“仿真计算”和“可视化展示”彻底分开:模式1只跑数据,保存每个时间步的无人机状态和目标状态,不画图;模式2加载数据后统一绘制轨迹、覆盖范围和事件时间线。实测下来这个改动节约了至少三分之一的时间,定位逻辑问题的时候不用再盯着动态图反复看。

关于记录数据,我的格式是每步存一个结构体数组,包含时间戳、每架无人机的位置、当前状态、剩余资源和命中事件列表。这样在做复盘和调参时可以直接对历史数据做后处理,比如统计平均暴露量、路径利用率、任务完成时间等指标。

5.4 分配结果抖动的处理

分配抖动是我调试后期遇到的最后一个老大难问题。任务重新分配周期设得太短时,编队会在两个候选目标之间反复横跳,一会儿全力去目标A,一会儿又切换去目标B。这不仅白白消耗油量,还让Dubins路径重算频率急剧上升,整个集群行为看起来就跟无头苍蝇一样。

我把reassignPeriod从1秒逐步调到10秒,并给任务切换增加惩罚系数:只有新方案的总代价比当前方案低一定比例才允许切换。我试过10%、15%、20%三个阈值,最终在10%时取得了不错的平衡,既能保证分配结果及时更新,又不会因为微小代价波动导致频繁切换。这个值也需要按具体任务规模调整,但“切换惩罚”这个思路本身是值得保留的。

一个额外的扩展建议

如果你打算把这套程序往更复杂的方向扩展,我强烈的建议是先保持模块接口稳定。候选集构建、联盟分配、冲突避免、路径规划都封装成独立函数,替换算法时只改内部实现,不改变调用方式。后续想加通信约束、异构无人机编队、或者目标动态重定位,都是在这套骨架上打补丁的活,改动成本会低很多。

我自己的体会是,集群协同仿真最值钱的部分其实不是某一个算法多么惊艳,而是你为让整套算法闭环跑起来而做的那些工程化处理,包括参数标定、异常兜底、性能取舍和模块解耦。这套逻辑要是吃透了,换语言、换场景都能复用,下一次遇到新的集群问题,你会比其他人更快地戳中要害。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进