
简介压缩包是一套面向无人机编队、机器人集群及自动化系统研究者的二维多智能体避障MATLAB仿真程序重点演示多智能体在二维平面上通过信息交互与一致性协议实现协同避障与队形保持。包内共10个文件全部为m脚本文件总量约4KB涵盖主程序main.m、智能体状态绘图plot_agent.m、编队控制plot_flocking.m、邻接矩阵计算adjacency.m、障碍物交点计算get_jiaodian.m以及势函数与bump函数等便于直接运行、逐步调参与二次修改代码量精简适合作为课程设计、毕业设计或算法对比的入门模板。目前已有211人学习下载。通过阅读这套代码可以掌握多智能体一致性理论在避障场景中的落地写法理解障碍物检测、避障策略设计与路径规划的MATLAB实现思路还可结合描述中提到的A*、Dijkstra等经典方法做进一步扩展是快速上手多智能体协同避障实验的高性价比参考。1. 多智能体二维避障这份 zip 解决的是“会撞成一团”而不是“怎么不撞墙”做过多智能体避障复现的人大概都有这种经历单跑一个智能体绕障碍效果很漂亮曲线顺滑、没有碰撞可一旦把十几个智能体放进同一张二维地图各自奔向目标整个场面很快就乱套——有的两个智能体迎面相遇后原地左右横跳有的挤在目标点附近疯狂抖动有的干脆互相“锁死”谁也不让。这正是多智能体避障和单智能体避障最本质的差别不只要躲开墙壁和静态障碍还要躲开其他也在移动、也在避让的智能体。这份“二维_避障.zip”解决的就是这个核心问题在二维平面地图上让多个智能体各自完成避障与导航同时彼此互不冲突并附带了可直接运行的仿真主循环和可视化层。它不依赖重型框架就是一个能跑的 Python 仿真包适合正在啃路径规划、机器人避障、多智能体协同课题的人拿来复现、改参数、做算法对比。2. 二维避障的三种建模思路为什么速度空间法是主选2.1 反应式避障分离力与人工势场最早也最直观的做法是反应式避障。典型代表是 Boids 模型中的分离规则和人工势场法APF。这类方法的思路很直接每个智能体收到来自静态障碍和其他智能体的“斥力”同时被目标点产生的“引力”拉着走合力方向就是当前速度方向。代码实现起来很便宜一个循环遍历所有邻居按距离衰减计算斥力矢量就行。但反应式方法的毛病在密集场景下非常明显局部极小值。两个智能体面对面接近时斥力会把双方往侧面推可由于双方都在动态变化中很容易出现你往左我也往左、你往右我也往右的镜像振荡表现在画面上就是两边高速抖动、迟迟无法通过。更麻烦的是它没有“未来”的概念完全基于当前帧位置计算力对高速相对运动几乎没有预判能力。所以在多智能体代码里反应式方法通常只作为 baseline 存在用来对比“不避让会怎样”和“避让后好多少”真正承担避障任务的是下面这类方法。2.2 速度空间法VO、RVO 与 ORCA速度空间法的核心是把避障问题从“选择哪条路径”转成“选择哪个速度”。假设当前智能体是一个圆盘对方也是一个圆盘那么当两圆的相对速度指向某个方向时经过一定时间后必然会碰撞。把所有会导致碰撞的相对速度画出来就在速度空间里形成了一个“禁止区域”这就是速度障碍物Velocity ObstacleVO。每个智能体只需要从禁止区域之外挑一个速度就不会在未来某段时间内撞上对方。但 VO 有一个典型问题如果双方都各自从自己的禁止区域外选速度容易出现“选了同一个方向导致避不开”的振荡。RVO互惠速度障碍的改进是把禁止区域做了一次镜像折叠让双方各承担一半的避让责任从而缓解振荡。而 ORCAOptimal Reciprocal Collision Avoidance是把避让责任进一步变成线性约束对每个邻居都生成一个“半平面”要求最终速度落在所有半平面之外或边界上然后用线性规划找一个离期望速度最近且满足全部约束的速度。这也解释了为什么 ORCA 在分布式多智能体避障里这么流行每个智能体只需要知道周围几个邻居的位置和速度不需要中心仲裁每帧算一个线性规划就能输出速度实时性有保证。我拆这类代码包时第一件事就是确认核心算法是哪一类。如果模块里有“ORCA”或“RVO”字样的类那这份资源大概率走的是速度空间法路线。如果只看到斥力向量求和那避障能力上限就在那了需要你自己往里补 RVO 或 ORCA。2.3 规划式避障A* 与 DWA 在多智能体下的边界还有一类常见思路是规划式避障比如 A*、RRT 做全局路径规划DWA动态窗口法做局部速度规划。A* 这类全局规划在静态地图里效果很好可一旦地图里有几十个动态智能体整张地图的障碍状态每帧都在变重规划成本很快就会失控。DWA 在单个智能体小车上是成熟方案通过采样线速度和角速度窗口选出一条能避开障碍的轨迹但多智能体场景里每个其他智能体都是动态障碍物采样窗口一旦开大计算量爆炸式增长开小了又看不到远距离来的人。实际工程项目里我更倾向分层方案上层用 A* 或 RRT 规划一串航点负责绕开静态大障碍下层用 ORCA 或 DWA 做局部避障只处理近距离的动态目标。这份 zip 如果只提供了局部避障主循环那全局规划层通常需要自己补。判断方法也简单看 config 里是只给了起点终点还是给了完整航点列表。3. 把 zip 跑起来目录结构、主循环与核心模块拆解3.1 解压后先看什么常见文件分工这类仿真资源的结构通常高度相似核心思路都是“参数、状态、控制、可视化”四层分离。我拿到一份多智能体避障代码一般先按下面这个映射去找对应文件文件职责你该关注什么config.py所有可调参数集中配置智能体数量、地图尺寸、速度上限、安全半径agent.py单个智能体的状态维护与速度计算状态更新方式、避障算法入口obstacle.py静态障碍解析与加载障碍是圆还是多边形、怎么碰撞检测simulation.py仿真主循环更新顺序、时间步长、碰撞统计visualization.py渲染与回放是实时绘制还是逐帧存图main.py入口从哪里读配置、跑多少步别假定目录结构和你手头这份完全一致但拆解顺序是通用的先用文本打开 main.py 找主循环再看 agent.py 里速度是怎么算出来的最后回 config.py 看参数。这个顺序能帮你快速定位算法类型而不是被文件命名迷惑。3.2 主循环骨架三阶段状态更新多智能体避障仿真主循环看起来很简单但它决定了整个系统的稳定性。一个合格的主循环会刻意把“计算期望速度”和“更新位置”分成两个独立阶段我在下文会详细解释为什么必须这样分。先看骨架代码def run_simulation(env, agents, dt, max_steps): for step in range(max_steps): # 阶段一基于上一帧位置计算每个智能体的期望速度 for agent in agents: agent.compute_desired_velocity(env.goal_by_id[agent.id]) # 阶段二调用避障算法得到无碰撞速度 for agent in agents: agent.compute_avoidance_velocity(env.obstacles, agents) # 阶段三统一应用新速度推进位置 for agent in agents: agent.position agent.position agent.velocity * dt # 碰撞统计与可视化 env.check_collision(agents) env.visualize(step)这段代码里最需要注意的是三个阶段的分工。阶段一只算期望速度没有任何智能体位姿被修改阶段二输入的是上一帧所有智能体的位置输出的是避障后的速度到了阶段三才真正把速度作用到位置上。如果你在同一帧内边算边更新位置后处理的智能体就会“看到”别人已经移动了而自己还站在旧位置制造不公平的先后手优势。这种不对称会让整个仿真行为变形具体表现我在第 4 章会细讲。参数方面dt 通常取 0.1 秒这在前向欧拉积分里够用max_steps 对应仿真时长比如 50 秒仿真就是 500 步。需要说明的是这只是一个骨架实际项目中 compute_avoidance_velocity 内部才是核心它会读取所有邻居的位置和速度构建前面说的约束半平面并求解线性规划。3.3 从单智能体扩展到多智能体状态同步与邻居搜索单智能体避障的世界里只有“我和障碍物”两种角色逻辑是单向的我绕开墙、绕开桩桶就够了。多智能体避障把问题复杂化了一层每个智能体对彼此来说都是动态障碍物而且对方也在主动避让。这带来两个直接后果一是状态同步必须对称不能用“先更新的占便宜”二是邻居搜索不能忽略。邻居搜索是很容易拖垮性能的点。朴素实现是双层循环对所有智能体两两计算距离复杂度 O(n²)。智能体数量小于 30 时这个开销可以接受一旦超过 50单帧计算里的距离运算就会占掉大部分时间。我见过不少二次开发没有做邻居搜索优化直接把数量调到 100 以后整个仿真卡到肉眼可见。常见的优化方案是单元格网格cell grid或者 KD-Tree把地图划分成格子只查邻近格子里的智能体。如果你只是想复现并跑通保持 n≤30 就够了如果你想做更大规模的实验这一步值得优先改。3.4 场景参数表从默认值开始调我把这类项目里常见的关键参数整理成一张表这些参数之间不是独立的调的时候互相之间会牵制参数典型值调参方向地图尺寸20m × 20m地图越小智能体越需要提前减速智能体数量10 ~ 30大于 30 需要关注邻居搜索优化最大速度1.2 ~ 1.5 m/s单个智能体小车的典型巡航速度智能体半径0.3 ~ 0.5 m含安全裕量决定了碰撞判定距离timeHorizon0.5 ~ 2.0 s看多远的碰撞风险见第 5 章实验邻居半径3 ~ 5 m只响应这个范围内的其他智能体期望速度系数0.8 × 最大速度给避障动作留出速度余量dt0.1 s缩小可提高精度但会显著拉长仿真时间这里最容易犯的错是只调数量、不调地图尺寸。20 个智能体放在 20×20 的场地里还不算密但如果把地图缩到 10×10密度翻了四倍timeHorizon 还维持 2.0 秒智能体会“过度反应”任何邻居靠近一点就开始绕行整体呈现出类似布朗运动的无序状态。正确顺序是先定智能体半径和安全距离再根据地图大小推最大密度最后回头调 timeHorizon。4. 复现多智能体避障代码五个高频坑与排查顺序4.1 目标点附近的抖动与卡死现象多数智能体已经到达目标点附近但有个别智能体迟迟进不去在目标周围绕圈或左右横跳既不撞上也不到达。原因有两个层面。第一目标点可能被另一个智能体占据避障算法认为“目标位置存在碰撞风险”持续输出远离目标的速度导致永远无法收敛第二很多实现没有“到达半径”的概念目标点是一个精确坐标可智能体本身有半径位置更新又是离散的很难恰好落在目标点上。解决的办法我一般加两个机制一个是在 agent 上设 goal_radius当智能体中心到目标点的距离小于这个半径时直接判定到达并冻结状态另一个是死锁检测连续 20 帧位移都小于阈值时给该智能体加一个随机扰动速度或者让它暂时降低避障优先级给后来者让路。默认代码里如果只给了单智能体逻辑这两个机制大概率需要你自己补。4.2 调大速度后避障算法“来不及反应”现象把最大速度从 1.0 调到 2.0 之后原本运行正常的场景开始出现直接碰撞避障算法好像失灵了。原因不是算法坏了而是你的 timeHorizon 与新的速度不再匹配。timeHorizon 的含义是“只看未来这么长时间内的碰撞风险”当相对速度变大同样的 timeHorizon 下提前量就变小了相当于刹车距离算短了。如果把智能体假设为半径 r最大相对速度为 v_rel_max那么 timeHorizon 至少应该满足提前量能覆盖减速到安全速度所需的距离。经验公式是 timeHorizon ≥ r / v_rel_max考虑到转向和响应延迟实际取值我会再放大 1.5 到 2 倍。解决就是在 config 里联动调整。一个我常用的做法是写一个配置检查函数跑仿真前先算一遍最大相对速度和当前 timeHorizon 是否匹配不匹配直接抛警告。这比跑到一半看碰撞日志更早知道问题。4.3 可视化看起来没撞碰撞统计却很高现象渲染窗口里两个智能体擦肩而过肉眼几乎看不出碰撞但脚本统计的碰撞次数上百。原因在碰撞判定和渲染两套逻辑之间。渲染是按帧率推进的帧与帧之间的位置变化用插值或直接跳帧而碰撞判定往往使用的是“几何中心距离小于半径之和”的严格条件。一个像素的短暂重叠在渲染上可能只闪一帧肉眼根本捕捉不到但判定代码已经记录了一次碰撞。反过来也可能是真的发生了轻微位置重叠而可视化层的圆圈半径比碰撞半径画得小给人“没碰上”的错觉。解决的核心是碰撞检测必须基于仿真轨迹来做不能基于渲染帧来做。把仿真 dt 固定在 0.1 秒渲染层只负责展示每一帧的真实位置都记录到轨迹数组里碰撞统计全部用轨迹数组计算。这样可视化帧率不影响碰撞结果问题就不再出现。4.4 设了随机种子结果还是每次不一样现象在代码里设置了 random.seed(42) 甚至 np.random.seed(42)连续跑两次仿真智能体轨迹完全不同。原因很隐蔽很多项目里随机数来源不止一个。配置解析用了 Python random初始化用了 NumPy random如果哪个模块内部用的是 random.Random 独立实例那全局种子就管不住它。更常见的是你在导入其他库之后才设置种子而上游模块在导入阶段已经消耗了一批随机数。另外一个被忽视的点是如果用了多线程或多进程每个线程的随机序列来源不同这种环境下全局种子的作用本身就有限。解决方法是把随机源统一收口。我的习惯是在 main.py 第一行就设置种子并且所有的随机采样都通过同一个 np.random.RandomState(seed) 实例来完成。与此同时对所有智能体按 id 排序后再初始化避免字典遍历顺序造成初始化顺序不固定。这两步做完同一种子基本就能复现相同轨迹了。4.5 更新顺序带来的“玄学”不稳定现象同一份代码、同一组参数只是往地图里加了一个障碍整个多智能体行为模式发生明显改变甚至出现系统性地朝某个方向偏移的现象。原因可能不是障碍本身而是更新顺序被改变了。如果主循环是“边算边更新位置”那么处于不同更新顺序位置的智能体看到的邻居状态就不是同时刻的后更新的智能体占便宜能看到别人已经移动后的位置先更新的则看到的是旧位置。这种不对称让整个系统产生偏向性加一个障碍物会改变智能体的更新顺序于是行为就变了。这类问题因为现象不直观很容易被当成玄学实际上它就是状态同步没做好。解决方法是把主循环改成第 3 章的三阶段结构所有智能体先在旧状态下计算期望速度和避障速度再用这些速度统一更新位置。这个改动逻辑简单但能消除大量看似随机的不稳定性。我拆过不少多智能体代码凡是出现“换个障碍就大变样”的现象八成都能在更新顺序上找到问题。5. 把 ORCA 参数调明白从仿真到小车路径规划的三个关键实验5.1 timeHorizon 与邻居半径的配合时间窗 timeHorizon 决定了智能体“提前多久”感知碰撞风险邻居半径决定了“把谁”纳入避障计算。这两个参数不是独立的我建议把邻居半径当作计算量控制开关把 timeHorizon 当作行为激进程度控制开关。具体来说邻居半径扩大到 8 米时每个智能体要计算的邻居数明显变多线性规划求解次数也上涨但如果把半径缩小到 2 米智能体对远处逼近的威胁完全无感高速场景下容易“临到眼前才发现”。我用一组对比实验验证过的规律是半径不变的前提下timeHorizon 从 2.5 秒降到 0.5 秒集群整体通行时间会缩短因为智能体不再提前绕远但碰撞风险同步上升。反之把 timeHorizon 拉到 4 秒智能体会为了规避几秒后才会发生的碰撞而大幅绕路整体效率明显下降。调参顺序我建议这样走先固定半径和安全距离再把 timeHorizon 从 0.5 秒起步逐步增加观察碰撞次数和完成时间找到“碰撞为零且完成时间不再明显下降”的临界点然后在这个时间点附近缩小邻居半径看碰撞次数是否仍然稳定。这个方法比同时调两个参数更可控你永远知道是哪个参数造成了行为改变。5.2 速度采样分辨率的双刃剑效应有些二次开发版本不会走 ORCA 线性规划而是把速度空间离散成网格逐格打分选最优。这时候就会多出一个采样分辨率参数。采样步长取太大比如最大速度的 1/5安全速度区间可能被漏过去选到违规速度导致碰撞采样步长取太小比如 1/100两个相邻步长对应的速度差异极小智能体每一帧都可能在若干近似速度之间高频切换轨迹反而抖动计算开销也大。我通常把采样步长设在最大速度的 1/20 到 1/50 之间具体看场景密度稀疏场景取 1/20密集场景取 1/40。还有一个补充技巧对“上一帧速度”采样更细对“远离上一帧速度”的方向采样粗一些。因为智能体的实际速度不会突变靠近当前速度的安全解才有意义这个非均匀采样能在不增加采样总量的前提下提高分辨率比单纯把步长改小更划算。5.3 三个评估指标从“没撞”到“效率最优”跑通代码只是第一步对比算法优劣时只看“有没有碰撞”远远不够。仿真里不碰撞是基本要求真正拉开差距的是效率。我一般同时统计三个量。一是碰撞次数这个直接看日志。二是死锁时间占比具体统计所有智能体速度模长低于某个阈值比如最大速度的 5%的帧数占比该值过高说明集群陷入拥堵。三是平均通行时间从起点到目标点附近的实际耗时这能反映避障算法是否过度保守、绕了不必要的远路。还有一个值得关注的指标是速度保持率即期望速度方向上能保持的速度比例。移植到动态避障小车路径规划场景时这个指标直接关联能耗和通行效率速度保持率低说明小车频繁减速、频繁调整方向电机效率必然受影响。5.4 一个可行的改进密度自适应参数调节固定参数在多智能体避障里最尴尬的地方在于场景密度是动态变化的。地图入口处很挤、开阔区域很空同一个 timeHorizon 在两个区域的表现天差地别。我做过的一个小改进是根据局部邻居数量动态调整 timeHorizon公式大概是这样density_factor local_neighbor_count / max_allowed_neighbors time_horizon_eff max(time_horizon_min, time_horizon_base * (1.0 - 0.3 * density_factor))当局部邻居多时timeHorizon 收缩避免智能体在拥挤区域过度反应当周围空旷时timeHorizon 恢复保持提前感知能力。实现时有两点要注意local_neighbor_count 必须按帧更新而且 time_horizon_eff 要做一阶低通平滑直接跳变会突然改变约束域反而引发不稳定的避障行为。我还会对 time_horizon_eff 设置下限防止高密度下收缩到完全失去预判能力。如果你后续想换成多智能体深度强化学习MADRL这套参数框架也能直接当 reward 设计和动作空间边界用至少给你省掉一轮从零建模的时间。6. 一个验证技巧用碰撞热力图抓出“假避障”“假避障”是我自己定的说法可视化全程看不出明显碰撞碰撞日志也勉强在及格线可一旦把轨迹数据摊开分析某些固定区域会反复出现高频率的近距离接触说明算法在那个区域系统性失效而不是偶发。这类问题在避障代码里最坑因为表面上一切正常实际上算法在特定位置长期处于“贴边行驶”状态。我习惯把地图栅格化成二维数组比如 40×40 或者 50×50 的网格然后统计每个网格内发生近距离接触的频率。判断标准很简单任意两个智能体中心的距离小于安全半径与一个小余量之和就在对应网格计数加一。等仿真跑完把计数矩阵画成热力图哪些区域是算法失效点一目了然。def build_heatmap(trajectories, safe_dist, grid_size): heat np.zeros((grid_size, grid_size)) for t in range(len(trajectories[0])): for i in range(len(trajectories)): for j in range(i 1, len(trajectories)): dist np.linalg.norm(trajectories[i][t] - trajectories[j][t]) if dist safe_dist: x int(trajectories[i][t, 0] / map_width * grid_size) y int(trajectories[i][t, 1] / map_height * grid_size) heat[x, y] 1 return heat这段代码接受所有智能体的轨迹序列、安全距离和栅格大小输出一个计数矩阵。坐标换算时注意把连续米制坐标映射到离散网格下标越界的坐标要截断否则索引直接崩。读图的时候重点看热点是否集中在某些结构性位置如果热点密集在目标点附近往往是目标点被占导致的区域拥堵如果热点分布在地图中部狭长地带大概率是该区域存在相对的“瓶颈”效应需要放宽邻居半径或缩小智能体半径来缓解。如果热点随机散布在地图各处那更可能是参数整体不匹配先回头检查 timeHorizon 和速度上限。有一次我调完一组参数可视化跑下来全程没撞自我感觉非常良好。直到烧完热力图脚本看到地图左下角一块固定区域的计数高得离谱才发现所有智能体在那里都会挤出一条极小间距的通道只是每次擦肩都很“极限”从渲染窗口看根本注意不到。从那以后我每次调完参数都强制自己先跑一遍热力图脚本确认没有暗藏热点之后再开可视化希望帮到你。本文还有配套的精品资源点击获取