简介东南大学参加机器人世界杯救援仿真国际赛的代码包面向人工智能、机器人及灾难救援仿真领域的学习者和研究者集中呈现多智能体协同、路径规划、环境感知、实时决策等核心算法的工程实现。压缩包共918个文件约6.18MB以Java源码和编译后的class文件为主配合HTML开发文档、配置文件和JAR依赖涵盖从核心逻辑到运行环境配置的完整工程结构便于阅读、调试与二次开发。目前已有1115人学习浏览。代码按智能体、世界模型、任务管理等模块分包读者可清晰追踪环境建模、通信协作、任务分配及搜救策略优化的完整链路其中涉及的路径搜索、马尔可夫决策、多角色协作等思路不仅可用于机器人世界杯救援赛备赛对智能搜救、无人系统协同等实际应用也有迁移参考价值。该代码包还保留了版本提交历史信息有助于理解项目的演进过程。 东南大学队伍在Robocup救援仿真国际赛里靠代码拿了不错的成绩这事在圈子里传得挺广。很多人好奇这套代码到底怎么写的、用到了哪些算法、怎么在比赛里跑赢其他队伍。我花了不少时间研究过这套系统今天就把这里面的门道掰开揉碎讲清楚。这套代码解决的核心问题是在一座模拟地震后的城市里多个救援单元消防车、救护车、警车如何协同工作在有限时间内救出最多的人。听起来像游戏AI但实际上是个非常典型的多智能体协同规划问题涉及路径规划、任务分配、通信协议、实时决策这些硬核内容。1. 项目定位与核心场景1.1 Robocup救援仿真到底在模拟什么Robocup救援仿真Rescue Simulation是机器人世界杯的一个正式赛项模拟的是地震后的城市搜救场景。整个仿真环境里有一座虚拟城市建筑会起火、道路会被废墟堵塞平民被困在建筑里等待救援。3支队伍各司其职消防队负责灭火、救护队负责救治和转运伤员、警队负责清理道路障碍。3个对应的指挥中心负责调度。每一局比赛的时间是按仿真周期走的通常几百个周期后结束最终得分取决于救出的平民数量、城市损毁程度和灭火效果。这个得分机制直接决定了代码的优化方向——所有智能体都必须围绕“更多存活平民”这个目标做决策。这套代码能走到国际赛层面说明它在几个关键环节上都做了扎实工作。我结合东南大学队伍公开的技术报告和代码风格把一个完整可用的救援仿真智能体系统需要具备的模块、数据结构、算法流程都整理了出来下面每一个环节都是在实战中真正决定胜负的点。1.2 为什么这个项目的代码含金量高很多人以为比赛分数靠调参实际上到了国际赛阶段比的几乎全是系统架构和算法效率。一个intelligent agent在一局比赛里要做上千次决策每次决策要考虑当前位置、周围火势、队友状态、通信获取的全局信息等十几个维度。代码的模块化程度、计算效率、决策质量直接决定了比赛结果。用这套赛事来检验本科生的工程能力非常全面。它既考验你对经典算法的掌握A*、Dijkstra、聚类、匈牙利匹配又考验你在分布式环境下的系统设计能力还考验你对实时性的把控——每个智能体的think时间被严格限制代码跑得慢就是送分。2. 系统框架与代码组织思路2.1 仿真平台的主要模块在动手写代码之前先把仿真平台本身搞清楚。整个Robocup救援仿真平台由几个独立进程组成Kernel负责整个世界的仿真推进GIS模块提供地图数据各个Agent通过socket与Kernel通信。跑起来之后Kernel每过一个时间步就会向各Agent发送当前世界状态Agent决策完再把动作指令发回去。这套系统的关键点是世界状态的数据结构。地图是一个带权图节点代表道路交叉口或建筑入口边代表可通行的道路。每个建筑有当前燃烧程度、结构完整性、内部平民数量等属性。所有的Agent消防车、救护车、警车也都位于图中的节点上。理解这个图结构后面的路径规划代码才能写得顺手。东南大学队伍在系统架构上做得比较好的一点是把“感知—决策—执行”三个环节拆成了独立模块。感知模块统一解析Kernel发来的世界状态转成内部对象模型决策模块只依赖这些对象模型做规划执行模块把决策结果转回仿真协议要求的数据格式。这样拆的好处是换地图、换场景、甚至换比赛版本时只需改协议解析部分算法模块可以原封不动地复用。2.2 参赛代码的模块划分一个可用的救援仿真智能体系统代码目录结构大致长这样src/ ├── agent/ # 智能体入口与主循环 │ ├── fire_brigade.py │ ├── ambulance_team.py │ ├── police_force.py │ └── station.py ├── world/ # 世界模型与地图处理 │ ├── map_parser.py # 地图解析 │ ├── building.py │ ├── road.py │ └── world_state.py ├── algo/ # 核心算法库 │ ├── path_planning.py # A* / Dijkstra等 │ ├── task_alloc.py # 任务分配 │ ├── cluster.py # 聚类 │ └── fire_spread.py # 火势预测 ├── comm/ # 智能体间通信 │ └── message.py └── utils/ # 工具函数每个Agent的核心都是一个think()方法这个方法在每一个仿真周期被Kernel调用一次返回当前周期要执行的动作。整个代码的复杂度都集中在这个方法里。如果你自己从零写这个系统我建议第一个版本先不做任何复杂算法就把最基础的“感知—去最近目标—执行”跑通再逐步迭代加内容。比赛代码不是一口气写完的是几十个版本堆出来的。3. 核心代码实现与算法细节3.1 路径规划从A*到分层搜索救援仿真里所有Agent的移动都依赖路径规划。地图是一个带权无向图每个节点的权值代表道路的通行代价。正常情况下A*算法就能搞定点到点寻路。但问题在于道路可能被堵塞火灾可能让某些道路无法通行所以路径规划必须是动态的。东南大学队伍在路径规划上做了分层处理。第一层是全局规划层基于静态地图不考虑火势和堵塞预计算关键节点之间的最优路径存成一张查找表。第二层是局部规划层在Agent实际移动过程中如果发现前方道路不可通行被废墟堵了或者火势过猛就在局部范围内做重规划绕开障碍继续向全局目标前进。这个分层策略很聪明。全局查表避免了每次决策都跑一遍A*计算量大幅下降。局部重规划又把动态避障的计算限制在很小的范围内实时性也有保障。核心A代码实现的关键点在于启发式函数的选择。在地图这种网格/节点图里用欧几里得距离做启发式是常见选择但更高效的做法是用预计算的“所有点到目标点的最短距离”做启发式这样A就能保证每走一步都严格朝最优方向前进。def astar(graph, start, goal, blocked_edges): open_set {start} came_from {} g_score {start: 0} f_score {start: heuristic(start, goal)} while open_set: current min(open_set, keylambda x: f_score.get(x, float(inf))) if current goal: return reconstruct_path(came_from, current) open_set.remove(current) for neighbor in graph[current]: if (current, neighbor) in blocked_edges: continue tentative_g g_score[current] graph[current][neighbor] if tentative_g g_score.get(neighbor, float(inf)): came_from[neighbor] current g_score[neighbor] tentative_g f_score[neighbor] tentative_g heuristic(neighbor, goal) if neighbor not in open_set: open_set.add(neighbor) return None这里有个真实的坑仿真环境里的道路是双向的但堵塞状态可能是单向的。比如东向西能通行西向东因为废墟过不去。在地图模型里每一条边必须区分两个方向的可通行状态否则路径规划会频繁规划出一条走不通的路。3.2 火势蔓延预测与灭火优先级消防队的灭火策略是救援仿真里最复杂的一块。火势在每个时间周期都会变化受建筑材料、风力、相邻建筑燃烧状态影响。如果只是看到火就去灭大概率会东奔西跑却收效甚微。东南大学队伍的方案是先预测后灭火。火势预测的常用做法是在离线阶段用历史比赛数据统计出火灾蔓延的概率模型。比如建筑A着火了建筑B在距离A 10米以内那么B在下一周期起火的概率是多大。这个统计模型跑起来之后每一局比赛开始就能对全城建筑做一次火灾风险评估把高风险的建筑提前标记为“消防重点保护对象”。灭火优先级不是看哪个建筑火最大、烧得最猛而是看哪个建筑对“保护平民生命”影响最大。消防队的工作逻辑应该是优先灭那些可能威胁到有平民被困建筑的火优先灭那些火势临界点低、一烧就全完的建筑。比赛中常见的错误是消防队全员扑向最大火点。在实际仿真里大火点往往周围已经没多少可救的东西了反而是那些小火点如果不管它过几个周期就会蔓延到重要建筑。所以代码里对每个火点都要计算一个综合价值评分按评分排序灭火目标。def score_fire_target(building, world_state): # 基础分建筑是否起火、火势大小 score building.fire_value * 1.0 # 重点加分建筑里有没有平民 if building.civilian_count 0: score 100 * building.civilian_count # 风险分周围建筑是否高价值 for neighbor in world_state.get_neighbors(building): if neighbor.is_important: score 50 # 惩罚分距离消防队当前有多远 score - distance_to_nearest_fire_brigade(building) * 0.1 return score这段代码体现的是多目标决策的思想——灭火不只是追求“把火扑灭”而是追求“在有限时间内保护最多的人”。比赛里很多队伍就是栽在这一层逻辑上只会见火就灭不知道权衡。3.3 任务分配救护队的救援调度救护队面临的问题和消防队不同。平民受伤后被发现的时机是随机的救护车需要跑到废墟建筑旁边进行挖掘和救治然后把救出来的平民送到医院。这里有两个约束很关键一是每台救护车同一时间只能处理一个任务二是医院的收治能力有限而且离得越远运送时间越长。这就成了一个典型的在线任务分配问题。东南大学队伍用的是“市场机制”方案每发现一个待救援的受困平民就作为一个“任务”发布出来每辆空闲的救护车根据自己的位置和这个任务的距离计算“完成任务所需时间”。引入一个简单的竞价机制时间最短的救护车获得任务分配权。这个方案好在哪里它不需要全局统一调度每个Agent自己做决策天然适合分布式执行。如果只有一个中心节点做调度一旦通信出问题或者中心节点计算超时整个救护体系就瘫了。市场机制的容错性更好而且实现起来也不复杂。实际比赛里救护队的通信范围是有限制的不是所有救护车都能实时获取全城所有任务信息。这时候就要用到通信模块去转发任务信息让每辆救护车手里有一份尽可能完整的“待办任务列表”。3.4 通信模块与信息共享Robocup救援仿真的通信模型做得非常真实智能体之间的通信有距离限制而且通信带宽有限。这意味着Agent不能像在本地单机调试时那样随时获取全局信息。通信协议的设计直接决定了队伍的“信息优势”。一种常见的做法是每个Agent周期性广播自己的“所见所闻”——比如发现的新火灾位置、新伤员位置、堵塞道路信息。各指挥中心汇总后在通信范围内整体广播一份“全局简报”。这样每个Agent能拿到一份可能过期但足够完整的世界地图。代码里的消息数据结构可以这么设计interface RescueMessage { type: string; // fire | civilian | road_block | status agentId: number; timestamp: number; // 发现消息的时刻 position: { x: number, y: number }; priority: number; // 消息优先级 }要特别注意消息的时效性。每个Agent收到一条关于火灾的消息时必须判断这个消息是什么时候发出的。如果消息已经是20个周期之前的那么对应的火势大小可能已经完全变了不能直接拿来当决策依据只能作为参考。我在调试中就遇到过很多次Agent照着旧消息跑过去到地方火已经灭了或者已经烧完了。通信模块做得好整个团队的协同效率能提升一个量级。做不好队伍就是一群瞎跑的个体看起来都在动实际一盘散沙。4. 实战排错与经验清单4.1 常见问题与排查方法救援仿真代码调试的复杂度比普通项目高很多因为每次运行结果都有随机性同样的代码跑两遍结果可能差异不小。这里整理几类高频问题的排查思路。模型解析失败仿真平台升级后地理数据格式可能变更导致地图解析报错。解决办法是拿到地图文件后先写一段独立的加载验证脚本解析完打印节点数、边数、建筑数和平台文档核对不匹配就检查字段偏移和格式定义。Agent卡住不动通常是路径规划返回了空路径或者Agent在目标点附近反复调整位置但始终无法满足“到达”判定条件。排查思路是在关键决策点打印日志把目标点、当前位置、路径结果输出出来对比看Agent是不是在绕圈。很多情况下是因为修复堵塞道路的判定条件写得太严比如要求完全到达节点才算到达但实际地图里两段路之间有小偏移导致永远到不了。灭火效率低不是算法问题通常是火势预测模型跑错了数据。检查一下火势增长模型的参数是不是适配当前地图的建筑类型。不同地图的建筑材质差异很大木质建筑和混凝土建筑的火势蔓延速度完全不是一个量级。通信数据量超限仿真平台对每个Agent每个周期能发送的数据量有上限超过上限会被丢弃。代码里把所有信息都塞进广播的做法在数据量小的时候看不出问题一到大城市地图就频繁丢包。解决办法是信息分优先级低优先级信息可以延迟发送高优先级信息比如新发现的火灾必须立刻优先发送。我用一张表把这几个问题整理出来方便你对照排查现象可能原因排查方式地图加载失败协议解析不匹配单测解析脚本核对节点/边数量Agent原地打转到达判定过严或路径为空打印路径和位置日志跟踪目标点灭火效率低火势预测参数不匹配检查建筑类型对应参数表通信丢包严重数据量超限检查发送频率和数据大小上限救护车任务堆积任务分配策略过期检查任务列表的更新机制4.2 提升比赛成绩的几个关键优化节点除了把基本功能跑通比赛成绩的差异其实是在几个关键优化点上拉开的。第一决策频率的动态调整。仿真环境每个周期调用一次think()但很多周期里Agent根本没有迫切的决策需求比如救护车在去医院的路上这时候不需要每周期都执行复杂的重规划。把路径跟踪这类低频操作和火灾发现这类高频响应分开处理能显著降低CPU负载给真正需要计算的决策步骤留出余量。第二预计算地图关键指标。地图加载完后把所有建筑的连通性、医院的位置分布、消防站到各区域的预估距离等指标一次性算好存在内存里。运行过程中反复计算这些值是典型的性能浪费。第三模拟推演。高端队伍会在决策前跑一轮快速模拟比如消防队规划灭火路径时把当前火势复制一份模拟接下来10个周期的火势变化估算出“如果我这时去灭火能保住哪些建筑、救出多少人”。这个想法的计算量比较大但可以把代码中很多拍脑袋的启发式规则换成有数据支撑的决策。第四日志和复盘工具。比赛级别的代码必须有一组好的可视化日志工具。每次跑完比赛把各个Agent的轨迹、火灾变化、通信消息全部输出再做偏差分析。没有这么一套工具算法改没改好全靠猜迭代效率极低。4.3 从代码层面看团队协作比赛代码通常是团队协作的产物东南大学那套代码也不例外。我看了他们公开的技术分享感受最深的一点是代码风格的统一程度很高。变量命名语义清晰核心模块之间有明确的接口定义算法部分基本不依赖具体的Agent类型。这样带来的好处是多个人可以并行开发消防队逻辑和救护队逻辑互不干扰最后合到一起时因为共用同一套世界模型和路径规划库代码合并冲突非常少。单人开发的团队很容易忽略这一点先把一个人的代码跑通了再让队友往里面塞功能结果就是主开发者的代码越来越臃肿别人看不懂也改不了。我的建议是开始写代码的第一天就把模块边界和接口文档定义好哪怕后续接口会调整也比没有接口随口乱调强得多。从工程角度看这套比赛代码的价值不只是比赛本身。智能体实时决策、分布式任务分配、动态路径规划这些能力放到自动驾驶调度、物流机器人协同、智能仓储管理这些真实场景里核心逻辑都是相通的。搞明白这一套代码的写法相当于把多智能体系统的一整套实战经验摸了一遍。比赛成绩只是结果真正值钱的是你在写这些代码过程中建立的系统思维和调试能力。本文还有配套的精品资源点击获取