简介:南邮2012年RoboCup 3D冠军队伍的可执行代码包,面向机器人仿真足球与多智能体系统研究者,可用于还原当年冠军队的底层决策、模型参数与战术脚本。压缩包约29.13MB,共175个文件,以76个rsg场景/模型配置、5个rb逻辑脚本、4个sh启动辅助脚本为主,同时包含apollo3d可执行程序、libruby依赖库以及点球攻防角色文件,并保留了svn版本元数据,便于追溯工程结构。已有898人学习下载。代码中的penalty-attacker、penalty-keeper文件承载了点球战术实现,结合rsg环境定义和rb脚本,可分析球员在Apollo3D平台上的运动控制、队形维持与攻防转换思路。下载后可对照目录结构快速定位关键配置与代码,复现实验场景或修改参数进行二次开发。对备战RoboCup比赛、研究多智能体协作或实时决策算法的开发者有直接参考价值。
1. 2012 RoboCup 3D 冠军代码:南邮 Apollo3D 的可执行包,值不值得跑
2012 年 RoboCup 3D 仿真组冠军,南邮 Apollo3D 的可执行代码包,听起来是十几年前的老古董,但真把它跑起来的人都会承认:这包比多数论文附录值钱。它不是一个演示 demo,而是一整套能在 SimSpark 仿真环境里完成感知、决策、通信、动作输出的完整 agent 系统。你不需要自己从零搭机器人模型,也不用纠结 UDP 通信协议,拿到手编译完直接能看到 11 个 agent 上场踢球。适合两类人:做多智能体决策的研究生,想抄作业但不想抄论文框架;以及刚接触 RoboCup 3D、想弄明白「冠军队伍到底比普通队伍强在哪」的入门者。这篇笔记会把我的复现过程和踩过的坑一次说清。
2. SimSpark 之外的决策主体:Apollo3D 代码包的模块结构与运行机制
2.1 先理解 RoboCup 3D 里 agent 到底在干什么
很多第一次碰 RoboCup 3D 的人会误以为这是视觉机器人比赛——agent 要处理摄像头图像、做目标检测。实际上完全不是。SimSpark 仿真器每 40ms 推一帧状态,包括球的位置、速度、自身关节角度、队友和对手的相对坐标,全部以带噪声的球坐标形式通过 TCP 推给 agent。agent 不需要「看」图像,它做的是状态估计、行为决策和动作输出。南邮这套代码的核心竞争力,不在于感知层有多花哨,而在于把有限信息用到了什么程度。
这个区别决定了你读代码的方式。冠军包里不会有什么卷积神经网络、目标跟踪算法,它最值钱的部分是两层逻辑:一是把带噪声的感知数据整理成可信状态估计——球在哪个区域、能不能够到、多久会滚出界;二是基于这个估计做团队决策——谁去抢、谁回防、什么时候传球。这两层逻辑全都写在 C++ 里,结构清晰,适合逐模块拆。
2.2 代码包模块划分:决策与动作分离的经典结构
Apollo3D 的可执行包拿到手后,代码树大致按功能分成五个模块,这个划分几乎是当年 RoboCup 3D 强队的标准结构,也是后来很多队伍抄作业的模板。
| 模块 | 对应代码目录 | 职责 | 关键接口 |
|---|---|---|---|
| 主控循环 | core/ | 维护 agent 生命周期,循环收发 server 消息 | agentLoop() |
| 感知建模 | perception/ | 处理球坐标、关节角、噪声过滤 | updateWorldModel() |
| 决策层 | strategy/ | 角色分配、站位、行为选择 | selectAction() |
| 动作层 | motion/ | 行走、踢腿、转身的关节轨迹生成 | executeAction() |
| 通信层 | communication/ | 队内消息的编码与解析 | sendMessage() |
主控循环是整个 agent 的心脏,它做的事情严格遵循仿真器节奏:每收到一帧 server 消息,就调用一次感知更新、一次决策、一次动作输出。不要小看这个看似简单的循环,决策周期是否稳定直接影响球队表现。如果某个周期里决策耗时超过 40ms,agent 就会「卡帧」,表现为动作迟滞、踢球乏力。
我拿到代码后的第一个习惯是先把core/里的主循环找出来读一遍,确认它是单线程顺序执行还是开了多线程。2012 年的代码基本都是单线程,因为当年的比赛规则不允许 agent 之间共享内存,多线程只会增加决策延迟。Apollo3D 的代码在这一点上非常干净,所有耗时操作都放在决策层之外,主循环里只保留状态更新和动作下发。
3. 把冠军代码跑起来:环境、参数与启动顺序
3.1 环境准备:老代码要配老工具链
2012 年的代码最大的坑不在代码本身,而在编译器。那个年代常见的 gcc 4.6、4.8 和 boost 1.4x 系列,放到 Ubuntu 20.04 上用 gcc 9 编译,基本会炸出一片boost/shared_ptr.hpp不存在或者std::auto_ptr已删除的报错。我的做法是老老实实装 Ubuntu 12.04 或 14.04 虚拟机,用系统自带的 gcc 4.8 和 boost 1.54。现代化的发行版能编译成功的概率不是没有,但折腾时间远超装个老系统。
装好系统后,先装物理引擎 ODE。SimSpark 依赖 ODE 做刚体动力学仿真,很多拿到代码包的人忘了这一层,直接编译 agent 本体,结果跑起来的时候服务端崩溃。完整的环境准备命令如下:
# Ubuntu 12.04/14.04 下安装基础依赖 sudo apt-get update sudo apt-get install -y build-essential cmake git sudo apt-get install -y libboost-all-dev libode-dev sudo apt-get install -y libdevil-dev libglew-dev freeglut3-dev # 验证 ODE 库是否可用 ldconfig -p | grep ode这里的libode-dev是关键。如果在ldconfig输出里看不到libode.so,SimSpark 服务端启动时会直接报动态库缺失。另外注意libdevil和libglew是 SimSpark 渲染需要的依赖,虽然比赛时不需要可视化界面,但服务端默认会尝试加载渲染模块,少了它们进程会异常退出。
3.2 启动流程:先起仿真服务端,再挂 agent
代码编译通过后,启动顺序有讲究。第一步必须先把 SimSpark 服务端跑起来,agent 再作为客户端去连。反过来的话,agent 启动后找不到服务器端口,会立即退出——这个问题看起来弱智,但我在复现时确实踩过。具体命令如下:
# 终端一:启动 SimSpark 3D 仿真服务端 cd simspark/build ./spark & # 等两秒确认服务端就绪 sleep 2 # 终端二:启动第一个 agent(角色 0) cd apollo3d/bin ./apollo3d --host localhost --port 3100 --teamname Apollo --role 0这里的参数含义要说清楚:--host指定服务器地址,本机就是localhost;--port是服务端监听端口,RoboCup 3D 标准比赛端口是 3100;--teamname必须和你对手的队伍名区分,两个队同名会被服务端拒绝;--role是球员角色编号,从 0 到 10 共 11 个角色。角色编号不是简单的位置编号,它对应决策层里预设的站位策略,后面会细说。
手动一个个起 11 个 agent 太累,我一般写个小脚本批量挂:
#!/bin/bash # 批量启动 11 个 agent,角色 0 到 10 TEAM="Apollo" HOST="localhost" PORT=3100 for i in $(seq 0 10); do ./apollo3d --host $HOST --port $PORT --teamname $TEAM --role $i & # 错开启动时间,避免 TCP 连接风暴 sleep 0.3 done echo "11 个 agent 已全部启动"注意脚本里的sleep 0.3是我加的缓冲。当年我图省事直接循环不 sleep,结果 11 个进程同时发起 TCP 连接,服务端 socket 队列来不及响应,前几个 agent 连接失败直接宕掉。错开启动后这个问题消失。
3.3 验证跑通:怎么看 agent 是否「活着」
跑通不等于跑对。agent 连接上服务端后,控制台并不会打印「连接成功」这种友好提示,你需要主动设置日志级别。Apollo3D 的代码一般支持--verbose参数,开启后会在标准输出打印每一帧的关键信息:
# 带详细日志启动单个 agent,观察决策日志 ./apollo3d --host localhost --port 3100 --teamname Apollo --role 0 --verbose 2>&1 | tee first_run.log # 查看日志里是否出现周期性心跳 grep -E "cycle|think|sense" first_run.log | head -20正常情况下,日志里应该能看到周期性递增的 cycle 编号,说明 agent 的主循环在持续运行。如果你只看到启动信息、没有任何 cycle 输出,大概率是感知模块卡死——agent 连接了但没有收到 server 的状态帧。这时候去服务端终端看有没有报错,常见的是「agent registration failed」,说明--teamname或端口设置有问题。
另外一个判断 agent 状态的方法是看比赛监控画面。SimSpark 服务端默认带有 monitor 端口 3200,用官方提供的 RoboViz 连接后,你能直观看到 11 个 agent 分别站在什么位置。更实用的验证是让两个 agent 互相传球——如果日志里出现通信消息,说明 TCP 链路和队内通信都正常,代码包算是真正跑起来了。
4. 拆解冠军队的踢球思路:从感知建模到动作输出
4.1 感知与状态估计:带噪声数据怎么变成可信判断
SimSpark 推给 agent 的球坐标不是真实值,而是附加了高斯噪声的观测值。这意味着 agent 每一帧看到的球位置都和实际位置有偏差,直接拿这个值去做踢球动作,大概率踢空。Apollo3D 的感知模块处理这个问题的思路,是维护一个「球状态缓存」,对最近 N 帧的观测做加权融合。
核心逻辑可以抽象成下面的伪代码结构:
// 球状态估计:对多帧观测做加权平均 class BallTracker { public: void update(Vector3f obs_pos, float obs_noise) { // 权重 = 1 / 噪声方差,噪声越大权重越低 float weight = 1.0f / (obs_noise * obs_noise); // 指数加权移动平均 filtered_pos = filtered_pos * (1 - alpha) + obs_pos * alpha; } Vector3f predictPosition(float time_ahead) { // 用最近两帧的位移估算速度,外推未来位置 Vector3f velocity = (filtered_pos - prev_pos) / dt; return filtered_pos + velocity * time_ahead; } private: Vector3f filtered_pos; Vector3f prev_pos; float alpha; // 滤波系数,0.2~0.4 之间 };这段逻辑里alpha是关键参数。设大了,滤波响应快但噪声也大,球来回抖动,决策层会频繁改判;设小了,估计平滑但滞后明显,球都滚到跟前了模型还停在两帧前的位置。Apollo3D 的做法是动态调整:球距离远时用大 alpha 快速更新,球靠近时用小 alpha 平滑预测。这种对距离的自适应处理,是冠军队伍和普通队伍拉开差距的细节之一。
踢球动作的生成还要用到predictPosition的结果——不是踢球当前的球,而是踢球到达时需要的位置。这个时间提前量一般取 0.2 到 0.4 秒,对应 5 到 10 个仿真周期。提前量设太大会踢空,设太小会踢到球屁股上导致方向偏。我当时反复调这个参数,最终固定在 0.3 秒左右,命中率最高。
4.2 站位决策:双二分场区域划分
Apollo3D 的团队站位不是简单的「前锋、中场、后卫」三条线,而是把己方半场分成多个功能区域,每个角色根据球的位置在区域间滑动。这是一种介于固定站位和全自由跑动之间的折中策略——既保留了阵型纪律,又不会出现 11 个 agent 挤成一团抢球的混乱局面。
核心决策伪代码如下:
// 角色站位决策:根据球的位置计算目标点位 Vector3f StrategicPosition(int role, Vector3f ball_pos) { // 区域基准点,每个角色有一个主区域 Vector3f home = homePositions[role]; // 球在哪,角色就向球所在的区域方向做有限偏移 float dx = clamp(ball_pos.x - home.x, -2.0f, 2.0f); float dy = clamp(ball_pos.y - home.y, -1.5f, 1.5f); // 偏移量限制在区域边界内,防止全员追球 Vector3f target = home + Vector3f(dx, dy, 0); // 门将特殊处理:始终守在球门线附近 if (role == 0) { target.x = clamp(target.x, -16.0f, -14.5f); } return target; }这个逻辑里最关键的是clamp操作。当年我手痒把这个限制去掉,想让前锋全场追球,结果发现 agent 全部跑到球附近,后场空门大开,对面随便一个长传就打穿了。冠军代码的站位哲学是「保持阵型的稳定性优先于追求局部抢球」,偏移量限制正是这个哲学的体现。
4.3 踢球动作:关节轨迹与力度参数的配合
踢球是所有动作里最考验调参的部分。SimSpark 的 Nao 模型腿部有 6 个关节,踢球动作本质上是给这 6 个关节设计一组时间序列的目标角度。Apollo3D 的动作层提供多种踢球动作,按力度和方向区分。
| 动作名 | 触发条件 | 力度档位 | 球速范围(m/s) | 适用场景 |
|---|---|---|---|---|
kickForward | 球在正前方 0.3m 内 | 3 档 | 1.5 ~ 4.0 | 正面推射 |
kickSide | 球在侧前方 | 2 档 | 1.0 ~ 2.5 | 边路转移 |
kickHigh | 球在脚下且被包围 | 5 档 | 3.0 ~ 6.0 | 大脚解围 |
kickPass | 队友在 5~8m 内 | 3 档 | 2.0 ~ 3.5 | 短传配合 |
调踢球力度时你会发现一个反直觉的现象:力度设大,球速不一定快。原因是 Nao 模型有脚部碰撞体积,摆动过快时脚会先碰到球侧面,把球碰歪而不是踢正。Apollo3D 的处理是给每个力度档位配一组关节轨迹,而不是简单地把所有关节角度乘以放大系数。你拿到代码后,如果想自己调踢球,建议一次只改一个力度档位对应的轨迹时间参数,不要动角度参数,这样定位问题最快。
4.4 队内通信:什么时候说话比说什么更重要
通信模块是 2012 年冠军队和当年大部分队伍拉开差距的地方。规则允许 agent 之间通过 server 转发消息,但每周期有消息数量和长度的限制。Apollo3D 的策略很克制:不是每帧都广播,而是在三个时机才说话——球权变化时、自己准备射门时、防线即将被打穿时。
这种做法直接降低了通信带宽占用和决策冲突。很多新手队伍喜欢每个 agent 每帧都广播自己的位置,结果消息风暴导致 server 转发延迟,决策信息反而更滞后。读这套代码时,重点关注通信层里的「消息优先级」字段,它决定了同一周期内多条消息谁先被处理,这个设计思路对做多智能体协作的人很有借鉴价值。
5. 避坑指南:2012 老代码从编译到上场的六个翻车点
5.1 现象:源码在新系统上编译报错,boost/shared_ptr.hpp文件不存在
原因:代码在 2012 年开发,依赖 boost 1.4x 系列,而现代 Ubuntu 自带 boost 1.7x+,头文件路径和 API 都有变化。shared_ptr从 boost 移入 std 标准库后,老代码的#include <boost/shared_ptr.hpp>直接失效。
解决:安装老版本工具链,或者做兼容处理。我的建议是别在编译器上较劲,直接装 Ubuntu 14.04 虚拟机,apt-get install libboost1.54-dev,把所有依赖对齐到当年的版本。如果你坚持用新系统,可以尝试全局替换boost::shared_ptr为std::shared_ptr并删掉对应 include,但代码量大时工作量不小。
5.2 现象:agent 启动后立即退出,终端无任何报错
原因:TCP 连接失败被静默处理了。Apollo3D 的代码在连接失败时只写系统日志,不往标准输出打印,不仔细看根本发现不了。最常见的情况是服务端压根没起来,或者 agent 启动早于服务端。
解决:启动顺序强制固定为「先 spark,再 agent」。写启动脚本时加上健康检查,在 agent 启动前先探测 3100 端口是否可达:
# 端口探测,通了再启动 agent for i in {1..10}; do if nc -z localhost 3100 >/dev/null 2>&1; then echo "server ready" break fi sleep 1 done这个nc -z探测是我后来加的习惯动作,避免了无数次「看起来在跑、实际没连上」的尴尬。
5.3 现象:服务端启动时报错,提示缺少libode.so或libglew.so
原因:SimSpark 编译时链接了 ODE 物理库和渲染依赖库,运行时会动态加载。如果只编译了 SimSpark 本体、没装对应的运行时库,服务端就会因为找不到共享库而崩溃。
解决:回到第 3.1 节的依赖安装步骤,确认libode-dev、libglew-dev都装好了。验收方法是执行ldconfig -p | grep -E "ode|glew",如果没有任何输出,说明库根本没装上,别急着跑程序。
5.4 现象:11 个 agent 全部挤在场地中央,阵型散架
原因:--role参数没有正确传递,全部 agent 用了默认角色 0。角色 0 是门将站位,多个门将同时上场时,决策层会把他们都锁定在球门区附近,看起来就是「挤成一团」。
解决:检查批量启动脚本,确认--role从 0 到 10 逐个递增。还有一种隐蔽情况是脚本里变量名写错,导致每次循环传的都是同一个值——我当时就栽在这上面,排查了半天才发现$i写成了$TEAM。
5.5 现象:球就在 agent 正前方,agent 却不做踢球动作
原因:感知模块的动态 alpha 滤波参数没有在比赛模式下生效。代码默认可能是「测试模式」,该模式下球的位置直接用观测值不做预测,而踢球动作触发条件需要预测位置在 0.3m 范围内。测试模式下预测值和观测值之间存在系统性偏差,导致触发条件永远不满足。
解决:在启动命令里加--matchmode参数,把代码切到正式比赛模式。如果你不确定自己手里的版本支持哪些参数,直接跑一遍帮助命令./apollo3d --help看全量列表。这一步能避免一整天毫无意义的查错。
5.6 现象:运行一段时间后 agent 集体掉线,服务端日志出现「connection reset」
原因:这是最玄学的问题之一,通常和 CPU 负载有关。决策循环耗时超过 40ms 仿真步长时,agent 对服务端的心跳响应超时,被判定为连接失效。单核虚拟机跑 11 个 agent 特别容易出现这个问题。
解决:给 agent 进程设置较高优先级,或者减少同时运行的 agent 数量先做单 agent 调试。我后来的习惯是先在--role 0单 agent 模式下跑通逻辑,再逐步增加 agent 数量,而不是一上来就挂满 11 个。这排错效率高很多。
6. 进阶验证:改一个参数,看冠军行为怎么变
代码跑通只是第一步,从「能跑」到「看得懂冠军思路」需要主动做实验。我最推荐的切入点是 4.3 节提到的kickPass动作的力度档位。找到动作层里定义档位的代码,把第三档力度从默认值改成一半,然后启动两支队伍对打——不用对手代码,复制你自己球队的二进制改个--teamname就行——观察传球成功率的变化。
你会很直观地看到:力度减半后,传球距离明显变短,中场的横传开始频繁被断。这说明 2012 年的力度参数是针对 Nao 模型踩过点的最优值,不是拍脑袋定的。再把predictPosition里的time_ahead从 0.3 改成 0.1,你会发现踢球命中率断崖式下跌——因为从决定踢到脚碰到球之间隔了多个仿真周期,球已经跑出一段距离了。这种「改参数看现象」的验证方式,比对着论文读框架强得多。
把验证结果记成实验日志是个好习惯。我当年把每个参数改完后的比赛结果截图、日志片段、胜率变化都存下来,一部分是给自己积累调参经验,一部分是写论文时需要真实数据支撑。现在回头看,这些日志本身就是一份宝贵的技术档案。
从那以后,我每次拿到老的代码包,都会强迫自己先跑一遍基线比赛、记录原始行为数据,再做任何改动。血泪经验告诉我,没有基线的调参就是瞎调,改坏了你都不知道改出的是什么。希望这份 Apollo3D 的复现笔记帮到你,跑通它,再拆掉它,最后你会看到一整套值得抄的决策框架。
本文还有配套的精品资源,点击获取