简介:基于无人机协同对抗策略的MATLAB仿真资源,面向无人机作战仿真与智能决策方向的初学者和研究者,尤其适合作为期末大作业或课程设计的参考。资源共27个文件,核心为23个.m脚本,涵盖红蓝双方位置刷新、拦截与突破判定、成功攻击判断等关键模块,同时包含.mat仿真数据、mlapp图形界面及pdf时间统计说明,压缩包约221KB,轻量易用。代码入口清晰,从main.m可运行完整对抗流程,各函数职责分明,模块间耦合度低,便于按需修改算法或替换策略。已有218人学习下载,无论用来理解协同对抗流程、复现实验场景,还是在此基础上扩展新的无人机博弈策略,这份源码和配套数据都能提供扎实的起点和直接的参考。
1. 无人机协同对抗策略仿真:为什么先讲刷新函数再谈主循环
接手这套源码时,我的第一反应是打开 main.m 找主逻辑,结果发现真正决定对抗走向的,是 RefreshRedPosition3.m、InterceptSuccess.m 和 AvaliableArea.m 这几个看起来零散的文件。基于无人机协同对抗策略的 Matlab 源码,把红方攻击编队与蓝方拦截防线放在同一个离散时间步里迭代:红方每帧刷新位置并尝试突破,蓝方同步刷新拦截节点,并在距离阈值内触发拦截成功。和纯运动学仿真不同,这套工程的价值在于把“协同对抗”拆成了可以独立替换的状态刷新函数,换一个刷新函数就相当于换一套无人机战术。适合正在做 Matlab 智能体博弈、期末大作业或毕设需要完整可跑工程的人。我的建议是从带 notUseNow 后缀的废弃文件读起,那段迭代痕迹比主代码更容易讲清设计取舍。
2. 对抗状态机与红蓝刷新机制:从文件命名反推战术流程
2.1 文件清单就是一张状态机
这套源码没有用 Simulink,全部由 .m 脚本、一个 matlab.mat 数据文件和一个 app1.mlapp 组成。文件命名遵循“角色+动作+版本”的约定,稍微整理一下就能还原整个对抗流程:
| 文件名 | 角色 | 从命名推断的职责 |
|---|---|---|
| RefreshRedPosition1.m / 2 / 3 | 红方 | 按不同策略刷新红方编队位置 |
| RefreshBluePosition1.m / 2 | 蓝方 | 刷新蓝方拦截单位位置 |
| InterceptSuccess.m | 判定 | 判断蓝方是否在有效距离内拦截红方 |
| BreakthroughSuccess.m | 判定 | 判断红方是否越过蓝方防线 |
| SuccessAttack.m | 判定 | 判断红方是否完成对目标的有效打击 |
| AvaliableArea.m | 区域约束 | 判断位置是否落在可用区域内 |
| GetPoint.m | 采样 | 在边界内生成随机初始点 |
| Rotate.m | 运动 | 旋转速度向量,负责航向更新 |
| node.m | 数据结构 | 定义单个智能体节点,存放位置、速度、状态 |
| R2B.m | 关系计算 | 计算红方相对蓝方的距离和方位 |
| DrawAreaPicture.m | 可视化 | 绘制当前战场态势 |
| 时间统计.pdf | 文档 | 记录仿真耗时与统计口径 |
main.m 是入口,foretest2.m、foretest3.m 是批量测试脚本,test.m 是冒烟测试,.gitignore 说明作者早期就按 Git 工程管理,所以保留 notUseNow 文件是刻意的版本管理习惯。这种命名对后来者的价值在于:可以通过 diff 两个版本看到策略迭代方向。比如 RefreshRedPosition1_notUseNow.m 和 RefreshRedPosition3.m 之间,最可能的变化就是“是否把蓝方位置作为刷新输入”,这决定了红方是直线突防还是协同避让。
所以理解这套源码的正确路径,不是逐行读数学公式,而是先厘清谁先动、谁后动、判定优先级。在这个工程里,红方是进攻方,蓝方是拦截方;红方要突破防线,蓝方要在防线前把红方“换算成”拦截成功事件。
2.2 主循环里的四步刷新
把 main.m 的结构压缩一下,可以看到一个非常典型的对抗主循环:
% 简化后的对抗主循环:先更新红方,再更新蓝方,最后做判定 for t = 1:T_max redPos = RefreshRedPosition3(redPos, bluePos, param); bluePos = RefreshBluePosition2(bluePos, redPos, param); if InterceptSuccess(redPos, bluePos, param) interceptCount = interceptCount + 1; resList(t) = 0; % 0 表示拦截成功 break; end if BreakthroughSuccess(redPos, param) breakCount = breakCount + 1; resList(t) = 1; % 1 表示突破成功 break; end if ~AvaliableArea(redPos, param.boundary) break; % 红方飞出可用区域,按对抗失败处理 end end这段循环体现了典型的异时博弈顺序:回合开始先由红方刷新,蓝方再根据红方的新位置进行拦截响应。如果把两行刷新位置对调,就变成蓝方先动的“先手拦截”,统计结果会显著不同。param 里常见字段有 dT、速度上限、拦截半径和区域边界;resList 保存每个回合的判定结果,后续时间统计和成功率统计都依赖它。
提示:break 跳出循环前只能写一次 resList(t),以防止同一回合被拦截和突破同时触发。这里把 InterceptSuccess 放在 BreakthroughSuccess 之前,意味着拦截优先级更高,与物理直觉一致:只要蓝方在红方越过防线前进入拦截半径,就算拦截成功。
2.3 R2B 和 Rotate:坐标关系里的方向问题
R2B 这个函数名容易被误读成“红蓝双方距离”,实际上从输入输出看,它更接近一个 Red-to-Blue 相对关系工具。常见做法是计算每个红方节点到最近蓝方节点的距离和方位角,作为红方刷新策略的输入,相当于把蓝方防线“感知”压缩成一组标量。
function [range, bearing, idx] = R2B(redPos, bluePos) % 返回每个红方节点到最近蓝方节点的距离、方位与编号 n = size(redPos, 1); range = zeros(n, 1); bearing = zeros(n, 1); idx = zeros(n, 1); for i = 1:n delta = bluePos - redPos(i, :); d = vecnorm(delta, 2, 2); [range(i), idx(i)] = min(d); bearing(i) = atan2(delta(idx(i), 2), delta(idx(i), 1)); end endvecnorm计算每行向量的二范数,min返回最近距离及对应蓝方节点索引;bearing用atan2返回值在 [-pi, pi] 区间,单位是弧度。这里最容易出错的是delta(idx(i), 2)对应 y 方向,一旦把 x 和 y 对调,后面 Rotate.m 转出来的航向会整体偏转 90°,红方集群会出现集体绕圈的现象。
Rotate.m 对应运动层,通常实现为二维旋转矩阵:
function vVec = Rotate(vVec, angle) % 将速度向量旋转 angle 弧度 R = [cos(angle), -sin(angle); sin(angle), cos(angle)]; vVec = (R * vVec(:)).'; end旋转矩阵的约定是逆时针为正。如果你的屏幕坐标 y 轴向下,需要把 sin 项取反,否则看到的轨迹是镜像的。这个问题在最后一章的单步调试里会暴露得非常明显,建议先用一组已知坐标验证 R2B 和 Rotate 的配合结果,再跑完整对抗。
提示:把 R2B 返回的 bearing 打印出来,再用 Rotate 旋转,对比旋转前后的 x、y 增量,可以快速判断坐标正方向是否匹配。
3. 核心模块实现:可用区域生成与单步博弈判定
3.1 AvaliableArea 与 GetPoint:边界约束下的随机初始化
AvaliableArea.m 这个文件名拼写少了一个 a,但既然函数名已经定了,调用处必须保持一致。它的作用是排除越界点,因为红蓝双方不能出现在任意位置。GetPoint.m 则负责在边界内生成随机点。两个函数配合解决“初始位置如何不重叠、不越界”的问题。
function flag = AvaliableArea(pos, boundary) % 判断 pos 是否落在 boundary 内 % boundary 支持矩形 [xmin xmax; ymin ymax] 和多边形顶点两种格式 if size(boundary, 1) == 2 flag = pos(:, 1) >= boundary(1, 1) & pos(:, 1) <= boundary(1, 2) & ... pos(:, 2) >= boundary(2, 1) & pos(:, 2) <= boundary(2, 2); else flag = inpolygon(pos(:, 1), pos(:, 2), boundary(:, 1), boundary(:, 2)); end end这里的boundary如果只有两行,就按矩形处理;如果有多行,就按多边形处理,交给inpolygon。pos是 N 行 2 列的坐标矩阵,返回的逻辑向量与每行坐标一一对应。实际工程里这个函数会被反复调用,建议把边界检查放在每帧刷新的第一句,越界点直接标记为失败,避免后续atan2、norm对异常坐标产生无效值。
GetPoint.m 的常见实现是均匀随机采样:
function pts = GetPoint(N, boundary, margin) % 在边界内生成 N 个点,margin 保证点距边界有一定距离 xmin = boundary(1, 1) + margin; xmax = boundary(1, 2) - margin; ymin = boundary(2, 1) + margin; ymax = boundary(2, 2) - margin; pts = [xmin + (xmax - xmin) * rand(N, 1), ... ymin + (ymax - ymin) * rand(N, 1)]; endrand产生的是 [0, 1] 均匀分布随机数,映射到[xmin, xmax]就得到边界内的均匀点。margin建议取 0.5 到 1 个机身长度,避免初始位置贴边导致第一帧就飞出区域。红方初始位置可以用GetPoint(5, boundary, 2.0)生成 5 架无人机,蓝方用GetPoint(6, boundary, 1.0)生成 6 个拦截节点,数量不同可以模拟数量不对称的对抗场景。
3.2 成功判定函数的阈值设计
判定参数是仿真里最容易拍脑袋的地方。拦截半径interceptR太小会导致红方高速掠过时“穿模”,太大则红方很难活过前 20 个回合。InterceptSuccess.m 的典型实现是求所有红蓝距离的最小值,再与阈值比较:
function flag = InterceptSuccess(redPos, bluePos, param) % 判断是否有红方节点落入蓝方拦截半径 dMin = inf; for i = 1:size(redPos, 1) dMin = min(dMin, min(vecnorm(bluePos - redPos(i, :), 2, 2))); end flag = dMin <= param.interceptR; end循环里bluePos - redPos(i, :)把蓝方坐标平移到以当前红方为原点的坐标系,vecnorm的第二参量 2 表示二范数,第三参量 2 表示按行计算。这样写不依赖统计工具箱,换到基础 Matlab 环境也能跑。如果红蓝数量增大到几十个,可以先用pdist2(redPos, bluePos)一次性算出成对距离矩阵,再取全局最小值,代码更短但需要确认环境支持。
BreakthroughSuccess.m 的判定则简单得多:
function flag = BreakthroughSuccess(redPos, param) % 红方任一节点越过防线 x 坐标即视为突破成功 flag = any(redPos(:, 1) >= param.defLineX); endredPos(:, 1)取出所有红方的 x 坐标,any表示只要有一架越过防线就算突破成功。这里的defLineX应该略小于区域右边界,给蓝方留出纵深。更稳的做法是加连续帧确认:连续 3 帧都越过防线才返回成功,防止红方在防线附近来回振荡造成误判。
3.3 main.m 到 app1.mlapp:参数集中配,态势实时画
main.m 适合命令行批量跑,app1.mlapp 适合答辩演示和单步调试。建议把仿真参数全部收敛到 param 结构体,避免散落在各处魔法数字:
clear; clc; close all; boundary = [0 100; 0 100]; % 仿真区域 100 x 100 param.dT = 0.1; % 仿真步长 param.vRed = 8.0; % 红方最大速度 param.vBlue = 9.5; % 蓝方最大速度 param.interceptR = 3.0; % 拦截判定半径 param.defLineX = 70.0; % 蓝方防线 x 坐标 param.T_max = 300; % 最大仿真步数 redPos = GetPoint(5, boundary, 2.0); % 5 架红方无人机 bluePos = GetPoint(6, boundary, 1.0); % 6 个蓝方拦截节点 for t = 1:param.T_max redPos = RefreshRedPosition3(redPos, bluePos, param); bluePos = RefreshBluePosition2(bluePos, redPos, param); if InterceptSuccess(redPos, bluePos, param), break; end if BreakthroughSuccess(redPos, param), break; end if ~AvaliableArea(redPos, boundary), break; end end DrawAreaPicture(redPos, bluePos, boundary);| 参数 | 含义 | 建议初始值 | 调整方向 |
|---|---|---|---|
| dT | 仿真步长 | 0.1 | 步长越小越精细,但耗时线性增加 |
| vRed | 红方最大速度 | 8.0 | 提高后突破更容易,但轨迹更抖动 |
| vBlue | 蓝方最大速度 | 9.5 | 蓝方比红方快才能形成有效拦截 |
| interceptR | 拦截判定半径 | 3.0 | 过小穿模,过大红方难以生存 |
| defLineX | 防线 x 坐标 | 70.0 | 靠近红方出生点,对抗时间更短 |
foretest2.m 和 foretest3.m 是调参脚本,适合批量修改param.interceptR观察成功率变化;test.m 是冒烟测试,用来验证初始化位置是否合法、判定函数是否返回。把参数集中在结构体的好处是,GUI 和命令行跑的是同一套配置,不会出现“界面跑的和脚本跑的不是一个版本”的问题。
4. 对抗效能统计:从时间统计.pdf 到批量仿真验证
4.1 时间统计 pdf 在统计什么
时间统计.pdf 从这个项目的文件组成看,大概率记录的是两类内容:一是一局完整对抗中各个函数的耗时占比,二是多局仿真的平均时长。Matlab 里做这类统计最直接的方式是tic/toc:
tCost = zeros(param.T_max, 3); for t = 1:param.T_max tic; redPos = RefreshRedPosition3(redPos, bluePos, param); tCost(t, 1) = toc; tic; bluePos = RefreshBluePosition2(bluePos, redPos, param); tCost(t, 2) = toc; tic; InterceptSuccess(redPos, bluePos, param); tCost(t, 3) = toc; end disp(mean(tCost, 1));mean(tCost, 1)得到每类操作的平均耗时,可以看到瓶颈到底在位置刷新还是判定逻辑。如果发现 RefreshRedPosition3 占比异常高,优先检查循环里是否重复计算 R2B 或者重复分配大矩阵;不要一上来就怀疑 Matlab 本身慢。对于秒级耗时,timeit比tic/toc更稳定,因为它会多次运行取中位数,适合对单次函数做精细评估。
4.2 批量仿真与成功率口径
单局结果说明不了策略好坏。批量跑的时候,要把“初始随机、过程随机”控制住:
N = 200; success = zeros(N, 1); breakTime = zeros(N, 1); for r = 1:N [ok, tUsed] = runOnce(param); success(r) = ok; breakTime(r) = tUsed; end breakRate = mean(success); % 突破成功率 avgTime = mean(breakTime(success == 1)); % 成功突破的平均耗时这里runOnce是把 main.m 的主体封装成“参数进、结果出”的函数,返回是否突破和耗时。想用parfor加速时要注意随机数问题:每个worker 会继承不同的随机流,但如果GetPoint在循环内部调用rng重置,所有局就会生成相同初始点,统计结果完全失真。建议只设置一次全局随机流,或者让GetPoint接受可选的随机种子参数。
统计口径建议按下表对齐:
| 指标 | 定义 | 使用位置 |
|---|---|---|
| 拦截率 | 拦截成功次数 / 总仿真局数 | 评估蓝方策略强度 |
| 突破成功率 | 突破成功次数 / 总仿真局数 | 评估红方协同策略 |
| 平均对抗时长 | 每局命中的 t 值之和 / 局数 | 判断对抗是否过于一边倒 |
| 单帧最大耗时 | 一个刷新函数的最长耗时 | 性能优化参考 |
4.3 三版红方刷新策略的对照实验
同一个工程里同时存在 RefreshRedPosition1、2、3,本身就是设计好的对照实验。通过函数句柄切换,可以快速比较策略差异:
strategyList = {@RefreshRedPosition1, @RefreshRedPosition2, @RefreshRedPosition3}; for s = 1:3 param.refreshRed = strategyList{s}; result(s) = runBatch(param, 50); endparam.refreshRed存的是函数句柄,主循环里调用param.refreshRed(redPos, bluePos, param)即可。三版策略可以从命名和 notUseNow 文件推断出大致差异:
| 版本 | 行为特征 | 典型效果 |
|---|---|---|
| RefreshRedPosition1 | 直线趋向目标,不做避让 | 突破率最高,但蓝方容易预判 |
| RefreshRedPosition2 | 加入随机扰动 | 突破率下降,红方轨迹更不可预测 |
| RefreshRedPosition3 | 根据蓝方位置调整航向后再刷新 | 协同特征最明显,对抗时间最长 |
如果第三版引入了蓝方位置作为输入,对照实验时需要把旧版函数的调用接口统一成“红方、蓝方、参数”三元组。Matlab 的函数句柄天然支持这种多态调用,这也是这套源码适合二次开发的原因之一。
4.4 结果落盘
批量跑完一定要落盘,否则关掉 Matlab 就白跑:
save('runResult.mat', 'success', 'breakTime', 'param');matlab.mat是源码自带的数据文件,保存的是作者跑过的原始数据,建议不要覆盖。自己跑的结果存成 runResult.mat 或带时间戳的文件名,比如runResult_20250101.mat。这样后续做参数敏感性分析时,还能回头查某一组参数的原始数据,而不是重新跑几百局。
5. 排错与调试技巧:废弃文件、坐标旋转与 GUI 单步验证
5.1 从 notUseNow 文件反查设计迭代
带有 _notUseNow 后缀的文件不是垃圾,而是作者的迭代记录。比如 RefreshBluePosition2_notUseNow.m 与 RefreshBluePosition2.m 之间的 diff,能看到蓝方拦截策略是否从“固定点拦截”改成了“预测红方位置拦截”。命令行里用对比工具直接看两个文件,比看任何注释都直观。保留废弃文件同时保留 .gitignore,说明这套源码的版本管理层次是清楚的。我自己的习惯是给废弃文件加_notUseNow后缀,而不是删除,因为旧版本往往藏着新方案里没有的边界处理逻辑。
5.2 航向旋转的象限修正
Rotate.m 和 R2B.m 配合时最常见的坑是角度象限错误。atan2(dy, dx)返回的角度范围是 (-pi, pi],如果旋转矩阵方向不对,按这个角度旋转后的速度向量可能指向完全相反的方向。一个兼容性更好的写法是给 Rotate 增加一个方向参数:
function vVec = Rotate(vVec, angle, yawSign) % yawSign > 0 使用逆时针旋转,否则使用顺时针旋转 if nargin < 3 || yawSign > 0 R = [cos(angle), -sin(angle); sin(angle), cos(angle)]; else R = [cos(angle), sin(angle); -sin(angle), cos(angle)]; end vVec = (R * vVec(:)).'; endnargin < 3确保老调用方式不报错,新代码可以显式传入yawSign。调试时先用单点坐标验证:给一个 (1,0) 速度向量旋转 90°,看结果是 (0,1) 还是 (0,-1)。只要这一步对了,后续的航线就不会整体镜像。
5.3 app1.mlapp 单步验证
app1.mlapp 适合做单步演示。在 GUI 里加一个“单步”按钮,回调里只执行一轮刷新并立即绘图:
function stepButtonPushed(app, event) app.redPos = RefreshRedPosition3(app.redPos, app.bluePos, app.param); app.bluePos = RefreshBluePosition2(app.bluePos, app.redPos, app.param); plot(app.UIAxes, app.redPos(:, 1), app.redPos(:, 2), 'r*'); hold(app.UIAxes, 'on'); plot(app.UIAxes, app.bluePos(:, 1), app.bluePos(:, 2), 'bo'); hold(app.UIAxes, 'off'); drawnow limitrate; enddrawnow limitrate限制重绘频率,比裸drawnow流畅得多,尤其在无人机数量增多后效果明显。把单步按钮和 5.2 里的yawSign开关放在一起用,调一次参数就能立刻看到集群队形是散还是聚,这是验证协同对抗策略最直接的方式。
本文还有配套的精品资源,点击获取