简介:《东南大学2014年校内赛Robocup3D仿真代码详解》是一份面向机器人足球仿真研究者、参赛学生和人工智能初学者的完整代码包,围绕RoboCup3D三维仿真环境,覆盖多智能体协作、运动控制、球路预测、射门与防守策略、视觉感知等模块,可帮助读者理解真实比赛中的智能决策、系统集成与调试方法。压缩包共332个文件,以100个.h头文件、87个.cpp源文件、55个.rsg场景配置和22个.hpp文件为主,辅以说明文档、脚本及备份文件,整体约307KB,目录结构清晰,方便按模块查阅;同时附带README、Doxyfile等辅助文件,便于快速定位代码结构。目前已有303人学习下载。通过研读代码可以获取东南大学参赛队伍在物理引擎交互、路径规划、通信协调和性能调优方面的具体实现与排错思路,是学习RoboCup3D仿真开发和多智能体系统的难得实践案例。
1. seu2013.tar.gz 是什么:一份瞄准 RoboCup 3D 赛场的 2013 年老代码包
你拿到的这个名为 seu2013.tar.gz 的压缩包,解开之后是一支完整的 RoboCup 3D 仿真足球队。和很多人想的不一样,这种老包里面基本没有强化学习,也没有视觉算法,主体是用 C++ 写的感知解析、世界模型、行为状态机和运动控制。它解决的是两类真实需求:比赛队伍需要一个能快速跑起来的 baseline,研究者需要一个环境稳定、逻辑透明的对照系统。它适合正在备赛的学生团队,也适合想把多智能体决策落地到仿真环境里验证的工程师。2013 年的代码放到今天依然值得打开,是因为 RoboCup 3D 的仿真底座没变,变的只是角色的细腻程度,而这恰恰是拆老代码最能学到东西的地方。
2. 先搭 RoboCup 3D 仿真底座:SimSpark 服务端与依赖选型
2.1 为什么是 SimSpark:仿真服务端与智能体的关系
RoboCup 3D 比赛用的仿真服务器叫 SimSpark,它是一个独立的 Linux 程序,底层用 ODE 物理引擎模拟 21 个 NAO 仿人机器人站在球场上的场景。这里要搞清楚一个基本概念:智能体(agent)不是图形程序,它是通过 TCP 连接到一个端口与仿真服务器通信的普通进程。服务器每帧把每个机器人的感知信息推给 agent,包括球的位置、队友/对手的相对坐标、关节角度、加速度计和陀螺仪读数;agent 算完决策后把动作指令发回服务器,服务器再驱动物理学模型往前走一步。
这个架构决定了整个代码包的形态。你在 seu2013.tar.gz 里看到的 src 目录,本质上是纯逻辑代码,不依赖 3D 渲染,只在调试时用 Monitor 观看。理解这个分层很重要:很多新手把 agent 当成“游戏客户端”,去源码里找窗口、找贴图、找碰撞箱,结果一无所获。正确的心智模型是——SimSpark 是物理世界,agent 是大脑,Monotor 是眼睛,三者通过端口连接,各管一段。
2.2 服务端依赖安装与网络验证
先把底座立起来。老包时代的 RoboCup 3D 队伍几乎默认基于 Debian/Ubuntu 系发行版开发,所以第一步是把编译工具、物理引擎和可选的可视化库装齐。我一般会这样装:
sudo apt-get update sudo apt-get install -y build-essential autoconf automake libtool \ zlib1g-dev libboost-dev libode-dev \ freeglut3-dev qtbase5-dev python3这里有几个选型要点。libboost-dev 是老代码编译绕不开的依赖,标题里这个 2013 年的包大概率用了 boost 的智能指针和线程库,装不上或者版本太新都会在 configure 阶段直接报错。libode-dev 提供物理引擎,SimSpark 依赖它来算碰撞和关节约束,这个库缺失时服务端就算编译成功,运行也会在加载物理模块时崩掉。freeglut3-dev 和 qtbase5-dev 是给 Monitor 用的,如果你只在无图形环境的服务器上跑比赛,可以暂时不装,但调试时看不到画面,排查行为问题会非常吃力。
依赖装完后,先验证服务端能不能起来。注意 Robocup3D 的服务端命令和 2D 版本不一样,3D 常用 rcssserver3d:
rcssserver3d & sleep 3 ss -lntp | grep 3100默认端口常见是 3100,ss 命令能看到监听状态就说明服务端活了。如果这里没输出,先确认进程有没有起来,再检查 3100 端口是否被别的进程占了。我会在这一步顺手加一条:如果终端提示缺少动态库,用 ldd 看 rcssserver3d 依赖哪些 so,缺什么补什么,不要急着重装整个环境。
2.3 用 Monitor 确认仿真世界已经活了
服务端端口通了,只能说明 TCP 在监听,不代表物理世界在正常推进。验证方式很简单:把 Monitor 打开,看球场和机器人是否出现。
rcssmonitor3d &如果窗口能打开且看到两队机器人站在中圈两侧,仿真世界就走了第一步。这一步能过滤掉三类问题:显卡环境问题、动态库缺失、服务端物理模块加载失败。我见过不少团队卡在“端口通了但 Monitor 一片黑”,最后查到是 ODE 库版本不对导致物理世界初始化失败,agent 连上了也拿不到任何感知数据。所以这里的实操建议是:先让 Monitor 画出球场,再启动 agent,顺序不要反。
Monitor 连不上的另一个隐蔽原因是 agent 和 server 的主机名解析问题。如果你在本地跑,千万不要让 agent 去解析远端主机名,直接用 127.0.0.1 最稳。这一步跟后面的避坑章节会反复提到,本质上都是老代码把 localhost 解析失败的黑匣子问题。
3. 解开 seu2013.tar.gz:包内目录结构、主循环与角色分工
3.1 先看整体:一个 2013 年代队包的典型布局
把 tar 包解开之后,不要急着编译,先摊开看结构。这个年代的包通常能看到这样一副骨架,我按最常见布局给你列出来:
seu2013/ ├── configure.in ├── Makefile.am ├── src/ │ ├── main.cpp │ ├── worldmodel/ │ ├── behavior/ │ │ ├── role_keeper.cpp │ │ ├── role_defender.cpp │ │ ├── role_midfielder.cpp │ │ └── role_forward.cpp │ ├── motion/ │ └── comm/ ├── conf/ │ ├── config.rb │ └── rcssserver3d.conf ├── script/ │ └── start.sh └── data/先解释为什么是这个结构:RoboCup 3D 的 agent 是一个长期运行的进程,它要管理通信、维护世界模型、做决策、发动作,所以源码天然被切成 comm、worldmodel、behavior、motion 四个模块。comm 负责与服务器端口收发报文;worldmodel 负责把服务器推来的原始坐标转换成本方能理解的全局坐标系;behavior 是核心,决定这门满载意图的动作;motion 是最后一步,把行为翻译成 23 个关节的目标角度。
conf 目录值得第一个读。config.rb 这种文件在老包里通常配置了队伍名、智能体编号、服务器地址和端口。很多时候你编译报错、运行时连不上,问题根本不在源码,而在 config.rb 里的地址写成了团队内网 IP。读老代码的纪律是先看配置,再读行为逻辑。
3.2 主循环拆解:感知、世界模型与行为决策
理解了目录,再往下追一行代码:agent 的主循环长什么样。老包虽然是 C++ 写的,但逻辑骨架用伪代码看更清楚,我一般贴着源码抽成这个循环:
while running: perception = recv_from_server() # 接收原始感知报文 if perception is None: continue world_model.update(perception) # 把报文转成全局坐标与关节状态 role = assign_role(player_number) # 按编号区分守门员/后卫/中场/前锋 skill = behavior.select(role, world_model) # 用状态机挑出当前动作 action = motion.plan(skill) # 运动层算出关节目标角度或角速度 send_to_server(action) sleep(TICK)这段伪代码里,最值得细抠的是 world_model.update 这一步。2013 年的包的感知报文不是结构化 JSON,而是要手工按格式解析的文本流。解析入口一旦写错一个字段偏移,整个世界模型都会错位——球的位置可能落在场地外,队友坐标全部镜像,表现出来就是机器人像无头苍蝇。所以 readme 少可以,解析函数里必须逐行注释。
behavior.select 是另一个容易翻车的点:老包的状态机普遍用角色编号直接做 switch 分支,如果队形配置里把 1 号设置为守门员、5 号设置为前锋,但主循环里判定用的是物理编号,就会出现前锋守门、门将压上的混乱局面。拿到包之后,第一步先确认 assign_role 的编号逻辑和 conf 配置是否一致,这在后来调试时能省掉一晚上的怀疑。
3.3 参数改哪里:角色配置与球队战术的入口
看懂了主循环,接下来要回答的问题就是:我如果想改队形、改跑位、改防守强度,应该动哪里。老包通常把位置参数放在行为模块的构造函数里,而不是独立配置文件中。下面是角色分工表,对应常见的行为模块入口:
| 角色 | 站位区域 | 决策重心 | 常见配置入口 |
|---|---|---|---|
| 守门员 | 本方禁区线附近 | 面向球、封堵射门角度 | role_keeper.cpp 中的站位参数 |
| 后卫 | 本方半场 | 盯人、解围、回追 | role_defender.cpp 中的防守半径 |
| 中场 | 中圈区域 | 接应、横向转移 | role_midfielder.cpp 中的跑动区间 |
| 前锋 | 对方半场 | 前插、逼抢、射门 | role_forward.cpp 中的进攻深度 |
表格里的“防守半径”“进攻深度”这类参数,几乎都是几个浮点数常量。在 2013 年代码里,它们不是通过命令行传入,而是直接写在对应 cpp 文件的顶端。改一个数值,重新编译,再跑一场比赛看效果,这是最原始的调参闭环。
我自己的习惯是先不改运动控制,只改行为阈值。因为运动层控制的是膝盖、髋关节的力矩,一旦调错,机器人直接原地摔倒,误判成策略问题,会死死浪费调试时间。先动行为参数,等场上行为模式对了,再触碰 motion 层。
4. 从编译到上场:让老代码在本地跑通的最小流程
4.1 交叉编译:autotools 流程与三处必查项
这代老包编译走的是 autotools 流程,不是新式 CMake。进入源码根目录后,标准动作是这样:
cd seu2013 ./bootstrap # 如果没这个脚本,就用 autoreconf -i ./configure --prefix=$HOME/robo3d make -j4 ls src/ # 此时应出现 agent 可执行文件bootstrap 的作用是生成 configure 脚本,它内部会按顺序调用 aclocal、autoconf、automake。如果源码包里没有 bootstrap 文件,autoreconf -i 是等价替代品,不需要自己手工一步步敲。configure 在这里负责检查 boost、ODE、zlib 的版本和头文件路径,并把生成结果写进 config.h 和 Makefile。
编译过程有三处必查项。第一处:configure 输出里有没有 warning,特别是“Boost::thread not found”这类的提示,有就要回头处理依赖,不要硬着头皮 make。第二处:make 的时候报的错是不是集中在模板代码里,尤其是 boost::bind 相关——那是版本“代沟”信号,说明你的 develop 环境 boost 比 2013 年的包新了太多,后面避坑章会讲具体解法。第三处:make 完成后可执行文件是否真的在 src 目录下,有些包会把二进制输出在根目录的 build 文件夹里,找不到时用 find . -name "seu*" -type f 扫一遍。
4.2 启动顺序:先服务端,后智能体,最后验证
编译通过只是第一步,跑通一场比赛要严格按照物理世界的启动顺序来。我会用两个终端,按下面的顺序执行:
# 终端 1:仿真服务 rcssserver3d --port 3100 & # 终端 2:智能体 ./src/seu_agent --team SEU2013 --port 3100 &参数说明里有一个容易混淆之处:--team 指定队伍名,用于在 Monitor 上区分显示;--port 指连接服务端的端口。有些包不用命令行参数,而是读取 conf/config.rb,这时启动命令变成 ./src/seu_agent -f conf/config.rb。如果你输入的参数和代码里的实际定义不一致,agent 进程会报“未识别的选项”并退出,这不算 bug,是接口差异。
启动 agent 之后,正确的验证顺序是先看服务端终端有没有打印出“new agent connected”这一行。如果服务端没有任何反应,说明 agent 的请求根本没到端口;如果服务端打印了但 Monitor 上机器人不动,那问题在行为决策层,不在通信层。这两个方向不要搞混,否则排查起来会走很多弯路。
4.3 用一行日志确认智能体没有“装死”
很多 2013 年的老包默认不往屏幕打印日志,这就需要自己加。如果源码里已经有日志开关,我通常会这样过滤出关键心跳信息:
tail -f ~/robo3d/agent.log | grep -E "sensed|kick|pos"sensed 表示收到感知消息,kick 表示执行了踢球动作,pos 会带坐标输出。这三类关键字里,sensed 是首要观察对象。如果程序启动后 10 秒内一条 sensed 都没有,说明 agent 没有成功从服务器拿到感知数据,问题在通信;如果 sensed 在刷而 kick 从来没出现过,说明行为机没有走到踢球分支,问题在决策。
日志还有一个容易被忽略的用途:确认 agent 的决策周期稳定性。你可以统计连续两条 sensed 的时间差,如果在同一个物理环境里忽快忽慢,说明 agent 的决策代码在部分帧里做了不适合的计算量,比如每帧都全量遍历所有队友的坐标做距离排序。这种问题在模拟赛中不明显,但一旦跑 11 个 agent,CPU 瓶颈会直接让比赛变成幻灯片。
5. RoboCup 3D 老包避坑手册:环境、端口、模型与日志的 5 个高频问题
5.1 编译期:boost 版本“代沟”问题
现象:执行 make 时,编译器报错信息集中在 boost/ 头文件的模板展开处,比如 bind、shared_ptr、thread 相关的类型不匹配,错误行号已经不在你的源码里,而是深入 boost 头文件内部。
原因:2013 年的队伍代码是按当时的 boost 主版本写的,今天的主流发行版预装 boost 普遍比老代码新了若干个大版本。boost 的库接口演进不算保守,两个大版本之间常常把某些重载或内部适配删掉,老代码按旧写法调用就会在模板实例化阶段崩掉。
解决:不要一上来就改源码。先查当前 boost 版本,再确认老包头文件里 include 了哪些 boost 组件;如果可以,直接安装旧版兼容包最省事。改源码是最后手段,且只改调用不兼容的部分,不要动原有逻辑。我通常会保留新旧两套环境,用环境变量切换,而不是重装系统。
5.2 连接期:端口连不上与 hostname 解析
现象:agent 启动后没有任何报错,但日志里一直刷“connect timeout”或“connection refused”,服务端终端也看不到 agent 上线的记录。
原因:这个坑一半出在配置,一半出在解析。老包 config 里写的服务器地址很可能是一台已经不存在的主机名或内网 IP;另一个常见是服务端进程起来了,但实际监听端口不是 3100,被本地其他进程占住。
解决:先在服务端机器执行 ss -lntp 确认监听端口;再在 agent 机器上用 telnet 127.0.0.1 3100 试通,能通说明网络没问题,再把配置里的地址改成 127.0.0.1。如果 config 里写的是 localhost,建议统一改成 127.0.0.1,能避开一堆 IPv6/IPv4 解析不一致带来的隐性故障。
5.3 行为期:agent 上场就发呆
现象:Monitor 上机器人能站住,但既不追球,也不回位,像断电了一样;日志里 sensed 在刷,说明通信正常。
原因:通信正常但行为不触发,多半是世界模型没有正确更新。典型情况是感知解析函数拿到了数据,但坐标转换时用了过期的本地参数,导致球被换算到场地外十几米,行为机判定“球不可达”,于是进入待机分支。
解决:在世界模型更新后加一行临时打印,输出球的坐标和自身坐标,和 Monitor 画面里球的真实位置做对比。如果数值差得离谱,重点检查坐标系换算是绕 X 轴还是绕 Z 轴转的,方向反了会在数值上表现为镜像坐标。这种问题靠肉眼看源码很难发现,靠打印对比最直接。
5.4 运行期:物理仿真慢得像放幻灯片
现象:Monitor 里画面卡顿,agent 日志显示感知频率明显低于正常节拍,比赛节奏全乱。
原因:两个方向。一是物理仿真步长参数被改得过于精细,ODE 每步计算量过大;二是同时运行了过多 agent,而决策代码里有高复杂度的循环,把 CPU 打满。
解决:先把 agent 数量压到 2 个,单人机对空场测试,排除决策计算量问题。如果两个 agent 都流畅,就是仿真步长或 CPU 核数分配问题;如果还是卡,回到服务端配置把物理步长调回常见默认值,并确认没有开一堆后台进程抢 CPU。注意,这代老包的物理步长参数名在不同版本里不完全一致,搜配置文件里的 timeStep 或者 physics 相关项,改之前先备份。
5.5 监控期:Monitor 黑匣子与动态库路径
现象:rcssmonitor3d 能启动,但窗口要么一片黑,要么直接崩,服务端却一切正常。
原因:Monitor 是老包里最容易缺动态库的组件,它依赖的图形库和 agent 完全不同。另一个常见是你在远程服务器上通过 ssh 启动 Monitor,X 转发没开,图形窗口自然弹不出来。
解决:先确认运行环境有图形会话,远程连接要加 -X 参数并确保服务端有 xauth 配置。其次用 ldd 检查 Monitor 可执行文件缺哪些 so,缺哪个装哪个,不要整包重编。最后,如果 Monitor 版本和服务端版本不匹配也会黑屏,优先保证两者来自同一套编译产物。
6. 给老代码留着新活路:日志驱动的决策热区调优
让 2013 年的包不只是一件文物,最快的方式是给它加上日志驱动的调优手段。先说做法:打开行为模块里的日志开关,把每个智能体的坐标和决策状态每帧落盘;然后写一个简单的统计脚本,把坐标按区域分桶,输出热区分布。
# 假设日志行格式: pos 1.2 -0.3 0.42 import sys from collections import Counter bins = Counter() for line in sys.stdin: if "pos" not in line: continue parts = line.split() x, y = float(parts[1]), float(parts[2]) bins[(round(x / 2.0), round(y / 2.0))] += 1 for key, cnt in bins.most_common(20): print(f"{key}: {'#' * min(cnt, 60)}")这段脚本的逻辑很简单:以 2 米为格子边长,把场地切成分桶区域,统计机器人在每个区域里被感知到的次数。参数里最有意义的是分桶大小,2 米适合看整体队形覆盖,1 米适合看禁区内的逼抢密度。字段名 pos 后面的三个数字对应 x、y、z,按实际日志格式调整。
跑完一场之后,热区图能直接回答三个问题:守门员是不是常年缩在门线上一步不动?中场是不是只在一条横线上往返,从不进入对方半场?前锋是不是总在禁区弧顶就停住,从不插到小禁区?这些用肉眼看 Monitor 也能发现,但用热区统计可以看到量化比例——比如守门员 80% 的帧数集中在门前 2 米×2 米的格子内,说明站位过于保守;中场在对方半场出现不足 10%,说明攻防转换脱节。
拿热区结果回去改行为参数:把防守半径缩小、进攻深度加大,每个参数只改一次,再跑一场,重新生成热区,对比差异。这个闭环不需要换代码框架,不需要上新算法,但能让老包的行为在半天内肉眼可见地变积极。我现在的习惯是每场日志都归档成带时间戳的副本,因为旧代码最怕的不是算法不行,而是连续改了几版参数之后,忘了上一场到底是哪个阈值让队伍变成了防守队形。先把这一步做成习惯,你手里的 seu2013 也能当新基线用。希望帮到你。
本文还有配套的精品资源,点击获取