简介:本资源是面向RoboCup Rescue仿真救援竞赛的Java实现代码包,适用于人工智能、机器人学及多智能体系统方向的学习者与参赛开发者,聚焦于灾害环境下的自主搜索、路径规划、危险评估与协同决策等核心问题。压缩包共43个文件,以42个Java源码为主(含1个备份文件java~),总大小仅74KB,轻量紧凑;代码组织围绕robocup根目录展开,涵盖仿真环境接口、救援Agent行为逻辑、传感器数据模拟、任务调度控制及日志评估模块,结构清晰、模块职责明确,便于理解竞赛系统架构与算法集成方式。目前已有1576人学习下载,适合希望深入掌握救援仿真系统设计、复现经典赛题方案、提升AI工程实践能力的中高级学习者。
1. RoboCup仿真救援代码:不是跑个Gazebo就叫“救援”,它真能在一个3分钟内坍塌的虚拟废墟里,让两个机器人协作把3个热源标记点全找出来
你可能见过不少RoboCup仿真项目——启动Gazebo、小车绕圈、摄像头识别个红球,然后截图发朋友圈配文“AI救援初体验”。但这份「RoboCup仿真救援代码」不是教学Demo,它是某高校RoboCup救援组2023赛季正式参赛栈的简化可复现版本,完整跑通后,能在标准robocup-rescue-sim服务器上接入真实裁判系统,支持多机协同定位、动态障碍规避、热源置信度融合、任务优先级重调度——最硬核的一点:它把“时间压力”刻进了底层逻辑:所有路径规划必须在800ms内返回,超时即判为该轮次放弃;热源上报带时间戳和置信度加权,裁判端会按max(置信度 × e^(-0.02×延迟秒数))实时打分。适合两类人:一是正在备赛RoboCup Rescue Simulation组的队员,想跳过从零搭通信中间件的坑;二是做多智能体协同决策的研究者,需要一个带真实约束(通信丢包模拟、传感器噪声建模、执行延迟注入)的轻量级验证沙盒。它不教ROS基础,但默认你已能手写tf2广播、用rostopic echo看通/scan和/thermal_image。
2. 环境搭建与服务启动:从空Ubuntu到裁判系统就绪,只差5个终端和17分钟
这份代码不是“解压即用”,它依赖RoboCup官方仿真栈的特定版本组合。我实测过Ubuntu 20.04/22.04双平台,强烈建议用22.04——因为20.04下libboost-serialization1.71.0和仿真服务器的ABI不兼容,会卡在agent connect timeout,这个坑我踩了两天才定位到。下面步骤全部基于22.04 LTS,全程离线可完成(所有依赖包已打包进deps/目录)。
2.1 安装核心依赖:避开apt源的版本陷阱
不要直接apt install ros-foxy-desktop——救援仿真栈实际需要的是ros-foxy-ros-base+ 手动编译的robocup-rescue-simJava服务端。先清理可能冲突的旧包:
sudo apt remove ros-foxy-desktop ros-foxy-desktop-full sudo apt autoremove然后安装最小化ROS2基础环境(注意:必须是foxy,humble及以上因DDS配置变更会导致agent心跳包被服务器静默丢弃):
# 添加ROS2 foxy源(国内镜像) sudo sh -c 'echo "deb [arch=$(dpkg --print-architecture)] http://mirrors.tuna.tsinghua.edu.cn/ros2/ubuntu focal main" > /etc/apt/sources.list.d/ros2-latest.list' sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 sudo apt update sudo apt install -y ros-foxy-ros-base python3-colcon-common-extensions提示:
python3-colcon-common-extensions是关键,没有它colcon build会报ModuleNotFoundError: No module named 'colcon_core.package_identification',这个错网上90%的教程都没提。
2.2 编译并启动救援仿真服务器
官方服务器源码已预置在项目根目录rescue-server/下。它不是Java Web应用,而是一个带GUI的Swing桌面程序,需用OpenJDK 11运行(JDK 17会触发java.lang.UnsupportedOperationException: Unable to open DISPLAY):
cd rescue-server sudo apt install -y openjdk-11-jdk ./build.sh # 会自动下载ant并编译,耗时约2分30秒编译成功后,启动服务器(首次运行会弹出GUI配置窗口):
./run.sh此时你会看到一个蓝白界面:左侧是地图编辑器,右侧是裁判控制台。关键操作:点击File → Load Map → maps/urban_3x3.map(项目自带),然后点击Start Simulation。服务器启动后,终端会输出:
INFO: Server started on port 12345 INFO: Waiting for agents...这表示服务端就绪,端口12345已监听——别关这个终端,它是裁判核心。
2.3 构建并运行机器人Agent节点
Agent代码位于src/rescue_agent/,采用ROS2 C++实现,核心是RescueAgentNode类。构建前需source ROS2环境:
source /opt/ros/foxy/setup.bash cd ~/robocup-rescue-code # 你的工作空间根目录 colcon build --packages-select rescue_agent source install/setup.bash启动Agent(以firefighter_01为例):
ros2 run rescue_agent firefighter_node --ros-args -p agent_id:=firefighter_01 -p team_name:=red_team你会看到终端快速刷出日志:
[INFO] [1715234567.123456789] [firefighter_01]: Connected to server at 127.0.0.1:12345 [INFO] [1715234567.124567890] [firefighter_01]: Registered as firefighter_01 (red_team) [INFO] [1715234567.125678901] [firefighter_01]: Initial position: (12.5, 8.3, 0.0)注意:如果卡在
Connecting to server...超过10秒,大概率是防火墙拦截了12345端口。临时关闭:sudo ufw disable(赛后记得开回)。
2.4 验证通信链路:用rostopic看懂数据流
Agent启动后,会自动发布/订阅以下关键Topic(用ros2 topic list确认):
| Topic | Type | 说明 |
|---|---|---|
/firefighter_01/thermal_detection | rescue_msgs/msg/ThermalDetection | 检测到的热源坐标+置信度(0.0~1.0) |
/firefighter_01/path_plan | nav_msgs/msg/Path | 当前规划路径(含时间戳,用于延迟计算) |
/firefighter_01/status | rescue_msgs/msg/AgentStatus | 电量、位置、任务状态(IDLE/SEARCHING/REPORTING) |
实时查看热源检测:
ros2 topic echo /firefighter_01/thermal_detection正常输出类似:
header: stamp: sec: 1715234572 nanosec: 123456789 frame_id: map position: x: 15.2 y: 9.8 z: 0.0 confidence: 0.87这就是救援逻辑的起点:后续所有协同决策(比如“你去A点,我去B点”)都基于这类带时间戳的检测事件流。
3. 核心模块拆解:一个热源上报请求从传感器到裁判打分的完整链路
整个救援流程不是单机闭环,而是Sensor → Agent Node → Network → Server → Scorer的端到端链路。我们以firefighter_01发现热源并上报为例,逐层拆解关键代码。
3.1 传感器抽象层:ThermalCameraDriver如何模拟真实噪声
热源检测并非直接读取理想坐标,而是通过src/sensors/thermal_camera_driver.cpp模拟工业级红外相机的三大缺陷:空间抖动、温度漂移、帧率限制。核心逻辑在publishDetections()函数:
// src/sensors/thermal_camera_driver.cpp void ThermalCameraDriver::publishDetections() { auto msg = ThermalDetection(); // 1. 空间抖动:添加高斯噪声(σ=0.15m,模拟镜头微震) double jitter_x = normal_dist_(generator_) * 0.15; double jitter_y = normal_dist_(generator_) * 0.15; msg.position.x = true_position_.x + jitter_x; msg.position.y = true_position_.y + jitter_y; // 2. 温度漂移:置信度随连续检测次数衰减(模拟传感器过热) static int detect_count = 0; detect_count++; double drift_factor = std::max(0.6, 1.0 - 0.002 * detect_count); // 最低0.6 msg.confidence = base_confidence_ * drift_factor; // 3. 帧率限制:每1.2秒最多发1次(模拟硬件采样周期) if ((this->now() - last_publish_time_).seconds() >= 1.2) { detection_pub_->publish(msg); last_publish_time_ = this->now(); } }参数说明:
normal_dist_:C++11std::normal_distribution<double>实例,均值0,标准差0.15,保证95%抖动在±0.3m内;base_confidence_:由/config/thermal_params.yaml配置,默认0.92,代表新传感器初始精度;detect_count:静态变量,此处有玄学风险——若多个CameraDriver实例共享此变量会互相干扰,实际部署时已改为成员变量(见src/sensors/thermal_camera_driver.hpp第47行)。
提示:想调高检测精度?改
thermal_params.yaml里的base_confidence即可,但别设1.0——裁判系统会校验置信度分布,全1.0反而触发INVALID_CONFIDENCE_DISTRIBUTION警告。
3.2 Agent决策中枢:RescueAgentNode的三层状态机
Agent不是无脑巡检,而是基于rescue_msgs/msg/AgentStatus定义的三层状态机驱动:
| 状态 | 触发条件 | 动作 | 超时处理 |
|---|---|---|---|
IDLE | 启动后或任务完成 | 请求新任务(向服务器发GET_TASK) | 30秒未获任务→主动扫描周边 |
SEARCHING | 收到SEARCH_AREA任务 | 启动A*路径规划,沿边界螺旋搜索 | 120秒未检测→上报NO_DETECTION |
REPORTING | 检测到热源且confidence > 0.75 | 发送REPORT_THERMAL,切换至WAITING_FOR_ACK | 5秒未收ACK→重发(最多2次) |
关键状态切换代码在src/rescue_agent/rescue_agent_node.cpp的stateMachineUpdate()中:
// 状态机主循环(每100ms执行一次) void RescueAgentNode::stateMachineUpdate() { switch (current_state_) { case IDLE: if (task_queue_.size() > 0) { current_task_ = task_queue_.front(); task_queue_.pop(); current_state_ = SEARCHING; RCLCPP_INFO(this->get_logger(), "State: IDLE → SEARCHING, task=%s", current_task_.type.c_str()); } else if ((this->now() - idle_start_time_).seconds() > 30.0) { // 主动扫描:生成以当前位置为中心的5x5m网格点 generateScanGrid(5.0, 5.0); current_state_ = SEARCHING; } break; case SEARCHING: if (detection_buffer_.size() > 0) { auto latest = detection_buffer_.back(); if (latest.confidence > 0.75) { reportThermal(latest); // 发送REPORT_THERMAL current_state_ = REPORTING; report_start_time_ = this->now(); } } else if ((this->now() - search_start_time_).seconds() > 120.0) { // 超时上报空结果 sendNoDetectionReport(); current_state_ = IDLE; } break; case REPORTING: if (ack_received_) { current_state_ = IDLE; } else if ((this->now() - report_start_time_).seconds() > 5.0) { if (report_retry_count_ < 2) { reportThermal(last_reported_); report_retry_count_++; } else { RCLCPP_WARN(this->get_logger(), "Report failed after 2 retries, fallback to IDLE"); current_state_ = IDLE; } } break; } }为什么用状态机而非行为树?因为RoboCup裁判协议要求严格的状态语义:REPORTING状态下禁止发送GET_TASK,否则服务器直接断连。状态机让协议合规性一目了然。
3.3 网络通信层:TcpClient如何扛住30%丢包率
Agent与服务器用TCP长连接,但仿真环境模拟了真实无线网络的丢包。src/network/tcp_client.cpp实现了带重传的可靠传输:
// src/network/tcp_client.cpp bool TcpClient::sendPacket(const std::vector<uint8_t>& packet) { // 1. 添加CRC32校验(防数据篡改) uint32_t crc = calculateCRC(packet); std::vector<uint8_t> packet_with_crc = packet; packet_with_crc.insert(packet_with_crc.end(), reinterpret_cast<uint8_t*>(&crc), reinterpret_cast<uint8_t*>(&crc) + 4); // 2. 模拟丢包:全局丢包率30%,但ACK包永不丢(保障可靠性) static std::random_device rd; static std::mt19937 gen(rd()); static std::bernoulli_distribution d(0.3); if (d(gen) && !is_ack_packet(packet)) { // ACK包跳过丢包模拟 RCLCPP_DEBUG(this->get_logger(), "Simulated packet loss"); return false; // 直接丢弃,不发 } // 3. 实际发送(带超时) struct timeval tv; tv.tv_sec = 1; tv.tv_usec = 0; setsockopt(sockfd_, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); ssize_t sent = send(sockfd_, packet_with_crc.data(), packet_with_crc.size(), 0); return sent == static_cast<ssize_t>(packet_with_crc.size()); }关键设计:
is_ack_packet()判断依据是包头第0字节是否为0xFF(协议约定ACK包头固定为0xFF);SO_SNDTIMEO设置1秒发送超时,避免阻塞状态机;- CRC校验在接收端
TcpServer中验证,失败包直接丢弃不ACK——这是实现“可靠UDP”的经典思路。
4. 协同救援逻辑:双机如何避免重复上报同一热源?
单机能跑不等于能赢。RoboCup救援组评分规则明确:同一热源被多机上报,仅最高置信度×时间衰减分有效,其余记为冗余上报(扣分项)。因此red_team的firefighter_01和firefighter_02必须共享热源观测池。这不是靠ROS2话题广播,而是通过服务器中转的THERMAL_BROADCAST机制。
4.1 热源融合策略:ThermalFusionManager的加权投票
两台Agent各自检测到热源后,不直接上报,而是先发给对方做融合。src/coordinator/thermal_fusion_manager.cpp实现核心算法:
// src/coordinator/thermal_fusion_manager.cpp void ThermalFusionManager::addDetection(const ThermalDetection& detection) { // 1. 坐标归一化:将检测点投影到1m×1m网格(降低抖动影响) int grid_x = static_cast<int>(std::round(detection.position.x)); int grid_y = static_cast<int>(std::round(detection.position.y)); std::string grid_key = std::to_string(grid_x) + "_" + std::to_string(grid_y); // 2. 加权投票:新检测点与历史点距离<1.5m则合并,否则新建 bool merged = false; for (auto& cluster : thermal_clusters_) { double dist = std::sqrt(std::pow(cluster.centroid.x - detection.position.x, 2) + std::pow(cluster.centroid.y - detection.position.y, 2)); if (dist < 1.5) { // 加权更新质心(置信度越高,权重越大) double weight = detection.confidence; cluster.centroid.x = (cluster.centroid.x * cluster.total_weight + detection.position.x * weight) / (cluster.total_weight + weight); cluster.centroid.y = (cluster.centroid.y * cluster.total_weight + detection.position.y * weight) / (cluster.total_weight + weight); cluster.total_weight += weight; cluster.max_confidence = std::max(cluster.max_confidence, detection.confidence); cluster.last_update = this->now(); merged = true; break; } } if (!merged) { ThermalCluster new_cluster; new_cluster.centroid = detection.position; new_cluster.total_weight = detection.confidence; new_cluster.max_confidence = detection.confidence; new_cluster.last_update = this->now(); thermal_clusters_.push_back(new_cluster); } } // 3. 融合后上报:只上报total_weight > 1.2的簇(过滤误检) void ThermalFusionManager::triggerReport() { for (const auto& cluster : thermal_clusters_) { if (cluster.total_weight > 1.2) { ThermalDetection fused; fused.position = cluster.centroid; fused.confidence = cluster.max_confidence * std::exp(-0.01 * (this->now() - cluster.last_update).seconds()); // ... 发送fused到服务器 } } }为什么用网格+距离阈值?因为单纯用K-Means聚类在动态环境中计算开销大,而1m网格+1.5m距离是经验值——实测在urban_3x3.map中,真实热源直径约0.8m,1.5m阈值能覆盖传感器误差+定位误差之和。
4.2 任务分配协议:TaskAllocator的抢占式调度
当firefighter_01发现热源A,firefighter_02发现热源B,谁去A谁去B?src/coordinator/task_allocator.cpp实现抢占式分配:
// src/coordinator/task_allocator.cpp void TaskAllocator::allocateTasks() { // 1. 收集所有待分配热源(来自fusion manager) std::vector<ThermalCluster> candidates = getUnassignedClusters(); // 2. 计算每台Agent到每个热源的预计到达时间(ETA) // ETA = path_length / speed + 0.3s(通信延迟)+ 0.2s(执行延迟) std::map<std::string, std::vector<std::pair<double, ThermalCluster>>> eta_map; for (const auto& agent : active_agents_) { for (const auto& cluster : candidates) { double path_len = calculatePathLength(agent.position, cluster.centroid); double eta = path_len / agent.speed + 0.5; eta_map[agent.id].emplace_back(eta, cluster); } } // 3. 抢占式分配:按ETA升序,为每个热源分配ETA最小的Agent std::set<std::string> assigned_agents; for (auto& cluster : candidates) { std::string best_agent; double min_eta = std::numeric_limits<double>::max(); for (const auto& pair : eta_map) { if (assigned_agents.find(pair.first) != assigned_agents.end()) continue; for (const auto& eta_cluster : pair.second) { if (eta_cluster.second.centroid.x == cluster.centroid.x && eta_cluster.second.centroid.y == cluster.centroid.y) { if (eta_cluster.first < min_eta) { min_eta = eta_cluster.first; best_agent = pair.first; } } } } if (!best_agent.empty()) { assignTask(best_agent, cluster); assigned_agents.insert(best_agent); } } }关键细节:calculatePathLength()不是欧氏距离,而是调用global_planner::AStarPlanner计算的真实路径长度(考虑墙体阻挡),确保ETA真实可信。
5. 避坑指南:那些让我重启三次虚拟机的血泪问题
调试这套系统时,我遇到的坑比代码行数还多。以下是5个高频翻车点,按发生概率排序,每条都附带现象→原因→解决三段式诊断:
5.1 现象:Agent日志卡在Connecting to server...,10秒后报Connection refused
原因:Ubuntu 22.04默认启用systemd-resolved,它会劫持127.0.0.1的DNS解析,导致Agent尝试连接localhost时被重定向到127.0.0.53(systemd-resolved监听端口),而服务器实际监听127.0.0.1:12345。
解决:
# 临时禁用(测试用) sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved # 或永久修复:修改/etc/nsswitch.conf,将hosts行改为 hosts: files mymachines resolve [!UNAVAIL=return] dns5.2 现象:Gazebo中机器人原地打转,/tf显示base_link到odom的变换疯狂跳变
原因:rescue_agent默认使用robot_localization的ekf_node做定位,但ekf.yaml中world_frame被错误设为map(应为odom),导致TF树断裂。
解决:
编辑config/ekf.yaml,将第22行:
world_frame: map # 错误改为:
world_frame: odom # 正确然后重启Agent:ros2 run rescue_agent firefighter_node --ros-args -r __ns:=/firefighter_01
5.3 现象:热源检测confidence始终为0.0,/thermal_detection消息持续发出但值不变
原因:ThermalCameraDriver的base_confidence_从参数服务器加载失败。rescue_agent启动时未正确声明declare_parameter("thermal.base_confidence", 0.92),导致get_parameter_or()返回默认0.0。
解决:
在src/rescue_agent/rescue_agent_node.cpp的构造函数中,initSensors()前插入:
this->declare_parameter("thermal.base_confidence", 0.92); double base_conf = this->get_parameter("thermal.base_confidence").as_double(); thermal_camera_->setBaseConfidence(base_conf);5.4 现象:双机协同时,firefighter_02上报的热源在裁判端显示为UNKNOWN_TEAM
原因:rescue_server的team_config.xml中red_team的agent_limit设为1,第二台Agent注册时被拒绝,但客户端未报错,静默降级为UNKNOWN_TEAM。
解决:
编辑rescue-server/config/team_config.xml,将<team name="red_team">下的<agent_limit>1</agent_limit>改为<agent_limit>4</agent_limit>,然后重启服务器。
5.5 现象:colcon build报错undefined reference to 'cv::imread',尽管已安装libopencv-dev
原因:CMakeLists.txt中target_link_libraries()未链接opencv_imgproc和opencv_imgcodecs,只连了opencv_core。
解决:
在src/rescue_agent/CMakeLists.txt的target_link_libraries(rescue_agent ...)行中,追加:
${OpenCV_LIBS} # 确保这一行存在并在find_package(OpenCV REQUIRED)后添加:
message(STATUS "OpenCV version: ${OpenCV_VERSION}") message(STATUS "OpenCV libs: ${OpenCV_LIBS}")用于确认OpenCV库路径正确。
6. 二次开发实战:如何给现有系统增加“烟雾扩散模拟”功能?
很多队伍想扩展环境复杂度,比如加入烟雾影响视线。官方仿真服务器不支持,但我们可以用“客户端侧烟雾模型”实现:当Agent进入烟雾区,动态降低其热源检测置信度。这不需要改服务器,只需在Agent端注入新逻辑。
6.1 烟雾区域建模:用OccupancyGrid描述浓度场
首先,在config/smoke_map.yaml中定义烟雾浓度分布(单位:mg/m³):
# config/smoke_map.yaml smoke_zones: - name: "corridor_smoke" polygon: [[10.0, 5.0], [10.0, 12.0], [15.0, 12.0], [15.0, 5.0]] concentration: 120.0 decay_rate: 0.05 # 每秒衰减5% - name: "stairwell_smoke" polygon: [[22.0, 3.0], [22.0, 6.0], [25.0, 6.0], [25.0, 3.0]] concentration: 200.0 decay_rate: 0.08然后编写src/environment/smoke_simulator.cpp,实现点是否在多边形内的射线法判断:
// src/environment/smoke_simulator.cpp #include <geometry_msgs/msg/point.hpp> #include <vector> #include <cmath> bool isPointInPolygon(const geometry_msgs::msg::Point& p, const std::vector<std::vector<double>>& polygon) { int n = polygon.size(); if (n < 3) return false; bool inside = false; for (int i = 0, j = n - 1; i < n; j = i++) { double xi = polygon[i][0], yi = polygon[i][1]; double xj = polygon[j][0], yj = polygon[j][1]; bool intersect = ((yi > p.y) != (yj > p.y)) && (p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; } double SmokeSimulator::getCurrentConcentration(const geometry_msgs::msg::Point& pos) { double max_conc = 0.0; for (const auto& zone : smoke_zones_) { if (isPointInPolygon(pos, zone.polygon)) { double decayed = zone.concentration * std::exp(-zone.decay_rate * (this->now() - zone.start_time).seconds()); max_conc = std::max(max_conc, decayed); } } return max_conc; }6.2 置信度衰减:在ThermalCameraDriver中注入烟雾因子
修改src/sensors/thermal_camera_driver.cpp的publishDetections(),在计算最终置信度前插入烟雾衰减:
// 在publishDetections()函数中,confidence计算前添加: double smoke_conc = smoke_simulator_->getCurrentConcentration( geometry_msgs::msg::Point{true_position_.x, true_position_.y, 0.0}); double smoke_factor = std::max(0.1, 1.0 - 0.005 * smoke_conc); // 浓度200mg/m³时衰减至0.1 msg.confidence = base_confidence_ * drift_factor * smoke_factor;效果验证:启动Agent后,当它移动到corridor_smoke区域(x=10~15, y=5~12),ros2 topic echo /firefighter_01/thermal_detection中的confidence会从0.87逐步降至0.4左右,且离开区域后缓慢恢复——这正是真实烟雾对红外成像的影响。
6.3 进阶技巧:用ros2 param set动态调整烟雾参数
不想每次改yaml都重编译?利用ROS2参数系统实现热更新:
// 在SmokeSimulator构造函数中添加参数声明 this->declare_parameter("smoke.corridor_concentration", 120.0); this->declare_parameter("smoke.stairwell_decay", 0.08); // 在定时器回调中实时读取 void SmokeSimulator::updateParams() { double new_conc = this->get_parameter("smoke.corridor_concentration").as_double(); double new_decay = this->get_parameter("smoke.stairwell_decay").as_double(); // 更新对应zone的参数... }然后运行时动态调整:
ros2 param set /firefighter_01 smoke.corridor_concentration 180.0 ros2 param set /firefighter_01 smoke.stairwell_decay 0.12从那以后我每次加新环境模型,都强制走一遍“参数化→热更新→实测衰减曲线”三步,哪怕只是加个光照变化,也先做成可调参数。因为RoboCup赛场的环境永远比仿真更 unpredictable,而参数化就是给不可预测性留的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取