Skip to main content

机器人的Gazebo仿真操作流程

一、创建工作空间与功能包

在开始仿真之前,你需要先准备好一个"工作空间"——也就是你的机器人项目文件夹。这一步的任务是:在你的家目录下创建一个 ROS2 工作空间,并把准备好的功能包代码解压进去。

每一行命令在哪个目录下执行,我们都会标注清楚——跟着做就好。

1.1 工作空间长什么样

一个典型的 ROS2 工作空间,最终会长成这样。你的工作空间在家目录下~/gazebo_ws),核心是 src/ 目录下的 learning_gazebo_harmonic 功能包:

gazebo_ws/ ← 工作空间根目录 (~/gazebo_ws)
└── src/ ← 源代码目录:所有功能包都放在这里
└── learning_gazebo_harmonic/ ← 我们马上要导入的功能包
├── package.xml ← 功能包的身份信息(包名、版本、依赖)
├── setup.py ← Python 包的安装脚本
├── setup.cfg ← 安装配置
├── resource/ ← 标记目录(ROS2 内部使用)
├── learning_gazebo_harmonic/ ← Python 源码目录
│ └── __init__.py ← 让此目录成为一个 Python 包
├── config/ ← Gazebo-ROS2 桥接配置文件
├── launch/ ← 启动文件(.launch.py)
├── urdf/ ← 机器人模型文件(URDF / XACRO / SDF)
│ └── sensors/ ← 传感器模型(激光雷达、相机……)
├── worlds/ ← Gazebo 世界文件(地面、障碍物、迷宫……)
└── test/ ← 测试脚本

5 directories, 28 files(解压后你会看到这个数字)
你已经猜到了

这个功能包的所有代码已经提前写好了——你不需要从零开始。只需要把它解压到 src/ 目录下,然后编译即可。但在此之前,了解清楚目录结构会让你后面操作时心里有底。

1.2 第一步:在 ~ 下创建 gazebo_ws 工作空间

📁 当前位置:家目录 ~

打开终端,先确认你站在家目录:

cd ~
pwd # 应该输出 /home/你的用户名

然后一口气把工作空间的骨架搭好:

mkdir -p ~/gazebo_ws/src

解释一下:

  • ~ 是家目录的快捷写法,等于 /home/你的用户名
  • -p 意思是"如果上级目录不存在就一并创建",所以 ~/gazebo_ws~/gazebo_ws/src 会同时被建出来
  • src 是 source 的缩写——未来所有功能包都生活在这个目录里

验证一下是否建好了:

ls ~/gazebo_ws
# 应该能看到 src 目录

1.3 第二步:解压准备好的功能包

代码已经为你准备好了

功能包的完整代码在实验室文件服务器的 guest 共享文件夹中,文件名为 learning_gazebo_harmonic.zip。你不需要手动创建任何文件——只需要把它下载下来,解压到 src/ 目录下就行。

📁 当前位置:家目录 ~

首先,从文件服务器的 guest 共享文件夹中,把压缩包下载到你的家目录。具体方式取决于实验室的网络环境(可能是通过浏览器下载、scp 命令、或直接在文件管理器中拖拽)。下载完成后,确认压缩包已经在 ~ 下:

ls ~/learning_gazebo_harmonic.zip
# 应该能看到这个文件名

然后把压缩包从家目录复制到工作空间的 src/ 目录下:

cp ~/learning_gazebo_harmonic.zip ~/gazebo_ws/src/

然后解压它(在 src/ 目录下执行):

cd ~/gazebo_ws/src
unzip learning_gazebo_harmonic.zip

解压完成后,src/ 下会出现一个 learning_gazebo_harmonic/ 文件夹,里面有所有代码。压缩包就没用了,可以删掉:

rm ~/gazebo_ws/src/learning_gazebo_harmonic.zip

好,来看看你拿到了什么——用 tree 看一下功能包的目录结构:

tree ~/gazebo_ws/src/learning_gazebo_harmonic

输出如下(如果没有 tree,用 sudo apt install tree 命令安装该软件):

learning_gazebo_harmonic/
├── learning_gazebo_harmonic/
│ └── __init__.py
├── config/
│ ├── ros_gz_bridge_mbot.yaml
│ ├── ros_gz_bridge_mbot_camera.yaml
│ ├── ros_gz_bridge_mbot_lidar.yaml
│ └── ros_gz_bridge_mbot_rgbd.yaml
├── launch/
│ ├── load_mbot_camera_into_gazebo_harmonic.launch.py
│ ├── load_mbot_lidar_into_gazebo_harmonic.launch.py
│ ├── load_mbot_lidar_into_maze_gazebo_harmonic.launch.py
│ ├── load_mbot_rgbd_into_gazebo_harmonic.launch.py
│ ├── load_mbot_rgbd_into_maze_gazebo_harmonic.launch.py
│ └── load_urdf_into_gazebo_harmonic.launch.py
├── package.xml
├── resource/
│ └── learning_gazebo_harmonic
├── setup.cfg
├── setup.py
├── test/
│ ├── test_copyright.py
│ ├── test_flake8.py
│ └── test_pep257.py
├── urdf/
│ ├── mbot_base_gazebo_harmonic.xacro
│ ├── mbot_gazebo_harmonic.xacro
│ ├── mbot_with_camera_gazebo_harmonic.xacro
│ ├── mbot_with_lidar.sdf
│ ├── mbot_with_lidar_gazebo_harmonic.xacro
│ ├── mbot_with_rgbd_gazebo_harmonic.xacro
│ └── sensors/
│ ├── camera_gazebo_harmonic.xacro
│ ├── lidar_gazebo_harmonic.xacro
│ └── rgbd_gazebo_harmonic.xacro
└── worlds/
├── depot.sdf
├── empty.sdf
└── maze.sdf

5 directories, 28 files

1.4 认识每一部分:这个功能包里有什么

看到这么多文件别慌——不需要你全部理解。我们按「你马上要用到的」和「以后才会接触的」分成三类,先认识一下:

🔥 你马上要用到的

目录 / 文件里面有什么什么时候用
launch/6 个启动文件,一键启动不同的仿真场景下一节就要用
worlds/3 个世界文件:空地(empty.sdf)、仓库(depot.sdf)、迷宫(maze.sdf下一节就要用
urdf/机器人的模型文件(底盘 + 激光雷达 + 相机),用 URDF/XACRO 格式描述下一节就要用
config/4 个 YAML 桥接配置文件——让 Gazebo 里的传感器数据能被 ROS2 话题读取后面几节会用到

📦 运行支撑文件(不需要改,但要知道它们存在)

文件作用
package.xml功能包的"身份证"——记录包名、版本、依赖项
setup.py / setup.cfg告诉 ROS2 如何安装这个 Python 包
resource/learning_gazebo_harmonic标记文件——确认"这是一个功能包"
learning_gazebo_harmonic/__init__.py让这个文件夹成为一个 Python 包(空文件)

🧪 测试文件(目前不用管)

测试文件检查什么
test_copyright.py每个源文件是否有版权声明
test_flake8.py代码风格是否符合 PEP 8 规范
test_pep257.py文档字符串是否符合 PEP 257 规范
你已经拿到了完整功能包

你拿到的是一个完整可运行的功能包——所有文件夹和文件都已经就位了。对新手来说,这比自己从头创建更高效:你可以直接跳到编译和运行的步骤,先感受仿真是什么样的,之后再慢慢学怎么写代码。

1.5 验证:看看你建好了什么

📁 当前位置:~/gazebo_ws/

回到工作空间根目录,看一看整体的目录树:

cd ~/gazebo_ws
tree -L 3

你应该看到类似这样的输出(注意此时还没有 build/install/log/ 三个目录):

gazebo_ws/
└── src/
└── learning_gazebo_harmonic/
├── config/
├── launch/
├── learning_gazebo_harmonic/
├── package.xml
├── resource/
├── setup.cfg
├── setup.py
├── test/
├── urdf/
└── worlds/

如果没有 tree 命令也没关系,用 ls -R 能看到同样的信息:

ls -R ~/gazebo_ws/src
到这里就对了

到此为止,你的工作空间还没编译——build/install/log/ 三个目录都还没出现。这是正常的:它们会在你第一次运行 colcon build 时自动生成。暂时不用管它们。

1.6 小结

回顾一下你刚刚做的事——每一步所在的目录都不同:

步骤所在目录做了什么
~创建 gazebo_ws/src 目录骨架
~/gazebo_ws/src/复制 learning_gazebo_harmonic.zip 到此处并解压,删除压缩包
~/gazebo_ws/src/learning_gazebo_harmonic/浏览目录结构,认识 launch/worlds/urdf/config/ 四大目录
无需手动创建文件

和一般 ROS2 开发流程不同,这次你不需要手动创建任何目录或代码文件——所有代码和资源已经打包在压缩包里了。你只需要解压、编译、运行即可。

下一步,我们要编译这个工作空间,然后一键启动你的第一个 Gazebo 仿真世界。


二、功能包的编译和运行

现在你手里已经有了完整的代码,接下来要让它们真正"动"起来。这就是编译(build)运行(run)——把源代码变成可执行的程序,然后在 Gazebo 里看到一个能跑的机器人。

别跳步骤

这一节的每一步都有它存在的理由。按顺序来,不要跳。 跳了任何一步,后面的命令就可能报一些你看不懂的错误。

2.0 在开始之前:一次性环境配置

在编译和运行之前,有两个"一次性"的配置需要做。它们只需要做一次,做完后以后就不用再管了。

2.0.1 修复 Gazebo GUI 黑屏 / 无响应的问题

很多同学第一次运行 Gazebo 时,会遇到这样的问题:Gazebo 窗口弹出来了,但一直黑屏,然后弹出"GUI Is Not Responding"的提示。

这是因为 Gazebo 的服务端(server)和界面(GUI)之间靠 UDP 组播(multicast)通信,而 Ubuntu 的防火墙或网络配置有时候会阻止这种通信。

解决方法是告诉 Gazebo:"别用组播了,全部走本地回环(localhost)通信"——在你的 shell 配置文件里加一行环境变量。

第一步:确认你用的是 zsh 还是 bash

echo $SHELL
  • 如果输出是 /bin/zsh/usr/bin/zsh → 编辑 ~/.zshrc
  • 如果输出是 /bin/bash → 编辑 ~/.bashrc

第二步:把下面这行加到配置文件末尾

nano 打开配置文件(以 ~/.zshrc 为例):

nano ~/.zshrc

按方向键 ⬇ 滚到文件最末尾,另起一行,粘贴:

export GZ_IP=127.0.0.1
  • GZ_IP:告诉 Gazebo "用哪个 IP 地址通信"
  • 127.0.0.1:就是"本机自己"的意思(localhost)

然后保存退出:Ctrl+OEnterCtrl+X

第三步:让配置立即生效

source ~/.zshrc

或者直接关掉终端重新打开一个。

改完再开新终端都会自动生效

放到 ~/.zshrc / ~/.bashrc 里的配置,会在每次打开终端时自动加载。所以你只需要做这一次,以后不用再手动 export 了。

补充方案:ufw 防火墙规则

如果上面的方法还是不行(极少情况),可能是因为 ufw 防火墙拦截了组播包。可以再加两条规则放行:

sudo ufw allow in proto udp to 224.0.0.0/4
sudo ufw allow in proto udp from 224.0.0.0/4

224.0.0.0/4 是 UDP 组播地址段。放行后重试即可。大多数情况下 只需要 GZ_IP=127.0.0.1 就够了,不需要这一步。

2.0.2 初始化 rosdep(一次性)

rosdep 是 ROS 的依赖管理工具——它能根据 package.xml 里声明的依赖,自动找到你的系统还缺哪些软件包,然后帮你装上。

如果你之前从来没运行过 rosdep,需要先初始化一次:

sudo rosdep init
如果提示 already exists

如果看到 ERROR: default sources list file already exists,说明以前已经初始化过了。这是正常的——跳到下一步即可。

然后更新 rosdep 的软件包索引(确认它能查到最新的包信息):

rosdep update

这两个命令只需要做一次,以后新增功能包时直接 rosdep install 就行,不用再 init 和 update。

2.1 用 rosdep 安装缺失的依赖

📁 当前位置:~/gazebo_ws/

功能包的 package.xml 里声明了它依赖哪些外部包。第一次编译前,用 rosdep 检查并安装这些依赖:

cd ~/gazebo_ws
rosdep install --from-paths src --ignore-src -r -y

命令拆解(每个参数是什么意思):

参数含义
--from-paths srcsrc/ 目录下找所有功能包的 package.xml
--ignore-src忽略 src/ 里已有的源码包(不检查它们之间的依赖)
-r遇到出错的包不中断,继续检查下一个(robust)
-y遇到需要安装的包自动确认(省得一直按 y

正常情况下的输出:

如果所有依赖都已经装好了(比如老师帮你提前装好了 ROS2 和 Gazebo),你会看到:

#All required rosdeps installed successfully

这说明一切就绪,可以直接跳到下一步编译。


如果需要安装东西:

如果 rosdep 发现缺了什么,它会列出需要安装的包名,然后自动调用 apt-get 安装。你可能会看到类似这样的输出:

executing command [sudo apt-get install -y ros-kilted-ros-gz]

让它跑完就行。这可能需要几分钟,取决于网速。

rosdep 并不万能

rosdep 只能安装 ROS 官方仓库里有的 系统依赖。如果你的功能包依赖了某个需要另外安装的第三方软件(比如 Gazebo 本身),你需要自己提前装好。在我们的场景里,Gazebo 和 ROS2 已经预装了,所以 rosdep 主要用来补一些桥接包(如 ros_gz_bridge)。

2.2 编译工作空间

📁 当前位置:~/gazebo_ws/

编译(build)就是把 Python 源码和配置文件打包成 ROS2 能直接运行的格式。在 ROS2 里,编译工具叫 colcon

第一步:加载 ROS2 环境

在编译之前,你必须让当前终端"认识"ROS2。需要 source 一下系统安装的 ROS2:

source /opt/ros/kilted/setup.zsh
根据你的 ROS2 版本调整

如果你用的是其他 ROS2 版本(如 Jazzy、Humble),把 kilted 换成对应的版本代号即可。不确定自己是什么版本?运行 echo $ROS_DISTRO,输出什么就填什么。

第二步:开始编译

确认你在工作空间根目录,然后执行 colcon build

cd ~/gazebo_ws
colcon build

正常输出长什么样?

编译过程中你会看到类似这样的输出:

Starting >>> learning_gazebo_harmonic
Finished <<< learning_gazebo_harmonic [1.23s]

Summary: 1 package finished [1.56s]

关键看最后一行 Summary——只要没有 failed,就算成功了。


编译完成后,回到工作空间根目录,你会看到多了三个目录:

ls ~/gazebo_ws

输出:

build install log src
新目录干什么的
build/编译过程中产生的中间文件
install/编译好的最终产物——ROS2 真正会用到的东西都在这里
log/编译过程的日志

以后你写了自己的代码,改动了 Python 文件,就回来再运行一次 colcon build 如果只改了 launch 文件或配置文件,则不需要重新编译。

2.3 让终端认识我们的功能包

📁 当前位置:~/gazebo_ws/

编译完之后,你的功能包已经存在于 install/ 目录下了。但终端还不知道——你需要让它"认识"你刚刚编译好的东西:

source ~/gazebo_ws/install/setup.zsh

验证一下——看看 ROS2 能不能找到我们的功能包:

ros2 pkg list | grep learning_gazebo_harmonic

如果输出了 learning_gazebo_harmonic,说明一切正常。


🔁 source 这件事,后面还会反复用到。

规则很简单:每打开一个新终端,都要先 source 一次。 你会在后面的步骤里频繁看到这条命令。

2.4 在 Gazebo 中加载机器人模型

现在到了最激动人心的时刻——在 Gazebo 仿真世界里,把你的机器人"召唤"出来。

第一步:打开一个新终端,加载 ROS2 环境。

(如果上一个终端还在,且已经 source 过了——可以复用。如果不确定,就重新 source 一次。)

source /opt/ros/kilted/setup.zsh
source ~/gazebo_ws/install/setup.zsh

第二步:启动 Gazebo 仿真。

ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py

此时,我们将得到如下的 gazebo 仿真界面:

Screenshot From 2026-06-16 15-20-22.png

这条命令做了什么?拆开看:

部分含义
ros2 launchROS2 的启动指令——一键启动多个节点
learning_gazebo_harmonic功能包的名字
load_urdf_into_gazebo_harmonic.launch.py启动文件——它告诉 Gazebo:"用 urdf/ 里的机器人模型,在默认世界里启动"

你应该看到什么?

执行命令后:

  1. 终端里会滚动很多日志信息。看着吓人,其实大部分都是正常的。
  2. 稍等片刻(第一次可能要多等一会,Gazebo 需要下载一些模型),一个 Gazebo 窗口 会弹出来。
  3. 窗口中你会看到:
    • 一个 地面(灰色的平面)
    • 一个 差速驱动机器人(mbot),有两个大轮子和一个万向轮
    • 顶上可能有一个 激光雷达(蓝色柱体)

没有看到机器人?

如果 Gazebo 窗口出现了但机器人没有加载出来,检查终端日志里有没有红色的 [ERROR] 字样。最常见的原因:

  • 忘了 source → 回到 2.3 节,确认你执行了那两条 source 命令
  • 忘了编译 → 回到 2.2 节,确认 colcon build 成功完成
  • Gazebo 黑屏 → 回到 2.0.1 节,确认 GZ_IP=127.0.0.1 已配置

2.5 用键盘遥控机器人

机器人出现了,但它还呆呆地杵在那里。现在我们来"遥控"它。

第一步:安装遥控器包。

遥控机器人的工具叫 teleop_twist_keyboard(teleop = 远程操作,twist = 速度指令,keyboard = 键盘)。它可能还没装,我们先装一下:

sudo apt-get install -y ros-kilted-teleop-twist-keyboard

第二步:打开一个新终端,加载环境,启动遥控器。

source /opt/ros/kilted/setup.zsh
source ~/gazebo_ws/install/setup.zsh
ros2 run teleop_twist_keyboard teleop_twist_keyboard

你会看到这样的界面:

This node takes keypresses from the keyboard and publishes them
as Twist messages. It works best with a US keyboard layout.
---------------------------
Moving around:
u i o
j k l
m , .

For Holonomic mode (strafing), hold down the shift key:
---------------------------
U I O
J K L
M < >

t : up (+z)
b : down (-z)

anything else : stop

q/z : increase/decrease max speeds by 10%
w/x : increase/decrease only linear speed by 10%
e/c : increase/decrease only angular speed by 10%

CTRL-C to quit

第三步:把键盘焦点放在这个终端窗口上,开始遥控。

按键说明(只需要记住 5 个键):

按键效果
i🔼 前进
k⏹️ 停止
j↩️ 左转
l↪️ 右转
,🔽 后退

注意:是英文输入法下的按键。按一次 i,机器人开始往前跑;按 k,它停下来。机器人不会自动停——不按 k 的话它会一直跑。


🤖 试试看:按 i 让机器人跑起来,然后按 jl 让它转弯。同时盯着 Gazebo 窗口——你应该能看到机器人在仿真世界里移动!

2.6 如何正确退出

操作结束后,不要直接关窗口。按以下顺序退出:

  1. 先退出遥控器:在遥控器终端窗口里按 Ctrl+C
  2. 再关闭 Gazebo:在启动 Gazebo 的终端窗口里按 Ctrl+C

两次 Ctrl+C 按完后,Gazebo 窗口会自动关闭。

不要直接点 × 关掉 Gazebo 窗口

如果你直接点窗口右上角的 × 关掉 Gazebo,后台的 server 进程可能还在运行。这会导致下次启动时报一些奇怪的错误(比如端口被占用)。如果不小心这样做了,运行 pkill -9 gz 来彻底清理。


2.7 完整流程速查

以后你再做 Gazebo 仿真时,整个流程可以简化为以下几步——把它截图或抄下来贴在旁边:

# ① 加载环境
source /opt/ros/kilted/setup.zsh
source ~/gazebo_ws/install/setup.zsh

# ② 启动仿真
ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py

# ③ 【新终端】启动遥控器
source /opt/ros/kilted/setup.zsh
source ~/gazebo_ws/install/setup.zsh
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# ④ 玩够了以后,两个终端各按 Ctrl+C 退出

你刚刚完成的

恭喜!你已经完成了 ROS2 + Gazebo 仿真的完整闭环

  1. ✅ 创建工作空间,解压功能包代码
  2. ✅ rosdep 安装依赖
  3. ✅ colcon build 编译
  4. ✅ 在 Gazebo 中加载机器人模型
  5. ✅ 用键盘遥控机器人移动

这五步就是机器人仿真开发中最核心的工作流。之后无论你做什么项目——加传感器、换地图、写控制算法——基本流程都差不多。你已经把最难的第一关过了!


三、ROS2 工程代码解读

前面两节,你成功地在 Gazebo 里看到了一个能跑的机器人。但你可能会想:那些命令背后到底发生了什么?这些文件之间是什么关系?

这一节就来回答这些问题。我们会把功能包里的关键代码文件逐一打开来看,搞清楚它们各自在做什么,以及它们是怎么"串"在一起的。

别怕看代码

这一节不可避免要读一些代码(Python、XML)。但不用担心——每一段代码都会配有逐行解释。你不需要掌握语法细节,重点是理解"这段代码在整个系统里扮演什么角色"

3.1 两条命令背后发生了什么

在动手读代码之前,先回顾一下你在第二部分运行的两条命令,以及它们各自触发了什么:


指令 ① — 启动仿真
ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py

这条命令一口气启动了 4 个组件(在 ROS2 里叫"节点",node):

节点作用类比
Gazebo 仿真引擎启动 Gazebo 世界(加载 empty.sdf),负责物理计算、渲染舞台
实体生成器(Spawn)把机器人模型(URDF)"放入"Gazebo 世界中,指定初始位置把演员放到舞台中央
ros_gz_bridgeROS2 和 Gazebo 之间的"翻译官"——让两边的话题能互相听懂同声传译
robot_state_publisher读取机器人关节数据,发布 TF 坐标变换动作捕捉系统

指令 ② — 遥控机器人
ros2 run teleop_twist_keyboard teleop_twist_keyboard

这条命令只启动 1 个节点:

节点作用
teleop_twist_keyboard监听键盘按键,把按键翻译成速度指令(Twist 消息),发布到 /cmd_vel 话题

数据流是这样的:

⌨️ 键盘按键

teleop_twist_keyboard → 发布 /cmd_vel(ROS2 消息)

ros_gz_bridge → 翻译成 Gazebo 消息 → 发布到 Gazebo 的 /cmd_vel

DiffDrive 插件 → 控制左轮和右轮转动

🤖 机器人在 Gazebo 中移动

🔍 那么问题来了:ROS2 是怎么找到这些文件的?

你敲了 ros2 launch learning_gazebo_harmonic ...——ROS2 怎么知道 learning_gazebo_harmonic 这个包在哪里?又怎么知道 load_urdf_into_gazebo_harmonic.launch.py 这个文件在哪个目录?

答案藏在两个文件里:package.xmlsetup.py。它们不直接参与仿真运行,但决定了你的代码能不能被 ROS2 找到——它们是整个工程的"注册中心"。


🔗 指令 ① 的参数溯源 — ros2 launch

ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py

这条命令的每一个参数是怎么被 ROS2 解析的?我们来逐层拆解。

第一层:包名 learning_gazebo_harmonic 从哪来?

它来自 package.xml<name> 标签

<package format="3">
<name>learning_gazebo_harmonic</name> ← 就是这里!
<version>0.0.0</version>
<export>
<build_type>ament_python</build_type>
</export>
</package>
字段作用
<name>功能包的唯一 ID——ros2 launch learning_gazebo_harmonic 里的包名就来自这里
<build_type>ament_python</build_type>告诉 colcon "这是一个 Python 功能包,用 ament_python 的方式编译"
<test_depend>测试依赖——只在你运行 colcon test 时才需要,不影响正常运行

编译时(colcon build),ROS2 会把 package.xml 复制到 install/learning_gazebo_harmonic/share/learning_gazebo_harmonic/ 下,并在这个路径下建立一个索引——之后所有 ros2 命令都能通过这个索引找到你的包。

第二层:launch 文件名 load_urdf_into_gazebo_harmonic.launch.py 去哪找?

package.xml 只解决了"包在哪里"。但 launch 文件、URDF 模型文件、世界文件等等——它们各自又在哪个子目录?答案在 setup.pydata_files 字段:

data_files=[
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))),
(os.path.join('share', package_name, 'urdf'), glob(os.path.join('urdf', '*.*'))),
(os.path.join('share', package_name, 'urdf/sensors'), glob(os.path.join('urdf/sensors', '*.*'))),
(os.path.join('share', package_name, 'worlds'), glob(os.path.join('worlds', '*.*'))),
(os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.*'))),
],

这段代码的每一行都在说一句话:"编译时,把源目录 X 下的文件,复制到安装目录 Y 下"

源文件位置(src/ 下)安装位置(install/.../share/ 下)
launch/*.launch.pylearning_gazebo_harmonic/launch/
urdf/*.xacrolearning_gazebo_harmonic/urdf/
urdf/sensors/*.xacrolearning_gazebo_harmonic/urdf/sensors/
worlds/*.sdflearning_gazebo_harmonic/worlds/
config/*.yamllearning_gazebo_harmonic/config/

这就是 ros2 launch 能找到你 launch 文件的原因。 launch 文件在 launch/ 目录下写好后,编译时被 setup.py 搬运到 install/ 下的标准位置,ROS2 就能按固定规则找到它了。


🔗 指令 ② 的参数溯源 — ros2 run

ros2 run teleop_twist_keyboard teleop_twist_keyboard

这个包是从哪来的?

teleop_twist_keyboard 不是我们写的——它是在第二部分 2.5 节,通过 apt-get 安装的系统级 ROS2 包

sudo apt-get install -y ros-kilted-teleop-twist-keyboard

安装后,apt-get 把文件放到了系统目录 /opt/ros/kilted/ 下(而不是 ~/gazebo_ws/ 下)。但具体放到了哪些子目录呢?一个 ROS2 系统包安装后分布在三个位置:

/opt/ros/kilted/
├── share/teleop_twist_keyboard/
│ └── package.xml ← 元信息:ROS2 靠它发现这个包
├── lib/teleop_twist_keyboard/
│ └── teleop_twist_keyboard ← 可执行入口:ros2 run 实际调用的脚本
└── lib/python3.14/site-packages/
└── teleop_twist_keyboard.py ← 源代码:234 行 Python 实现
位置放什么类比
share/package.xml 等元数据图书馆的目录卡片——告诉你"这里有一本书"
lib/<包名>/entry point 可执行脚本书架上指向书位置的标签
lib/python3.x/site-packages/真正的 Python 源码书的内容本身

为什么你在 share/ 下只找到了 package.xml因为 share/ 本来就只放元信息。 真正的代码在 lib/ 下——你可以自己去看看:

# 看源码
cat /opt/ros/kilted/lib/python3.14/site-packages/teleop_twist_keyboard.py

# 看入口脚本
cat /opt/ros/kilted/lib/teleop_twist_keyboard/teleop_twist_keyboard

当你 source /opt/ros/kilted/setup.zsh 时,ROS2 就知道去这三个位置找东西了——share/ 发现包,lib/ 找到代码。

两种来源对比:

来源安装方式根目录如何被找到
learning_gazebo_harmonic自己写的,colcon build~/gazebo_ws/install/source ~/gazebo_ws/install/setup.zsh
teleop_twist_keyboardapt-get install 系统包/opt/ros/kilted/source /opt/ros/kilted/setup.zsh

ROS2 的搜索逻辑是:把所有 source 过的路径都加到一个列表里,按顺序查找。 所以你在第二部分每次都要先 source 这两条——一条让 ROS2 认识系统包,一条让 ROS2 认识你自己写的包。

拆解一下:

ros2 run <功能包名> <可执行文件名>
↑ ↑
package.xml setup.py 的
的 <name> entry_points
  • 第一个 teleop_twist_keyboard:功能包名,来自它的 package.xml<name>
  • 第二个 teleop_twist_keyboard:可执行文件名——这是怎么来的?

答案在 setup.pyentry_points 字段:

entry_points={
'console_scripts': [
'teleop_twist_keyboard = teleop_twist_keyboard.script:main',
# ↑ 可执行文件名 ↑ 实际 Python 函数位置
],
},

entry_pointsconsole_scripts 就是用来注册可执行程序的:

  • 等号左边:你敲的命令名(ros2 run xxx YYYYYY 部分)
  • 等号右边:实际的 Python 代码位置(包.模块:函数
本项目为什么 entry_points 是空的?

你可能会发现,我们 learning_gazebo_harmonicsetup.pyentry_points 是空的('console_scripts': []):

entry_points={
'console_scripts': [
],
},

这是因为本项目的所有运行入口都是 launch 文件,没有需要独立运行的 Python 脚本。如果以后你在这个包里写了自己的 Python 节点(比如一个控制算法),就需要在这里注册它。


补充:package.xml 里为什么没有运行依赖声明?

我们的 package.xml 里没有 <depend>ros_gz_sim</depend><depend>ros_gz_bridge</depend> 这样的运行依赖。这是因为当前环境已经预装了这些包,launch 文件和 URDF 直接引用它们即可。如果要发布这个功能包给别人用,就需要在 package.xml 中显式声明所有依赖,让 rosdep 能自动安装。


小结:从命令到代码的完整链路

ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py
│ │ │
│ package.xml 的 <name> setup.py data_files
│ 定义包的唯一 ID 把 launch/*.launch.py
│ 搬到 install/share/.../launch/

└──→ ros2 在 install/share/ 下搜索 learning_gazebo_harmonic
→ 找到 launch/load_urdf_into_gazebo_harmonic.launch.py
→ 读取并执行(启动 4 个节点)

现在,让我们打开这个 launch 文件,看看它里面具体写了什么。

3.2 launch 文件详解:load_urdf_into_gazebo_harmonic.launch.py

这是你运行的核心文件。让我们逐段打开来看。

文件位置: launch/load_urdf_into_gazebo_harmonic.launch.py

头部的 import 语句

import os
import xacro

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
作用
import osPython 自带的路径处理工具
import xacro.xacro 文件(带宏的 URDF)转换成纯 URDF 格式
get_package_share_directory"告诉我这个功能包被安装到了哪个目录"——即 install/learning_gazebo_harmonic/share/learning_gazebo_harmonic/
LaunchDescriptionlaunch 文件的"主结构"——所有要启动的东西都放进去
IncludeLaunchDescription复用另一个 launch 文件(这里用来启动 Gazebo 自带的启动文件)
Node定义一个 ROS2 节点——"启动某个功能包的某个可执行程序"

变量定义

package_name='learning_gazebo_harmonic'
pkg_path = os.path.join(get_package_share_directory(package_name))
xacro_file = os.path.join(pkg_path, 'urdf', 'mbot_gazebo_harmonic.xacro')
world_file = os.path.join(pkg_path, 'worlds', 'empty.sdf')
  • pkg_path:功能包的安装路径。os.path.join 把多个路径片段拼在一起(跨平台兼容)
  • xacro_file:指向机器人模型的 xacro 文件
  • world_file:指向 Gazebo 世界文件——这里用的是 empty.sdf(空地)

如果你想换一个世界(比如迷宫),把 empty.sdf 改成 maze.sdf 就行。

robot_description_config = xacro.process_file(xacro_file)

这一行调用 xacro 库,把 xacro 文件"展开"成纯 URDF 格式。xacro 文件里有很多宏(macro)——你可以把它们理解为"代码模板",展开后变成一长串完整的 URDF 描述。

启动 Gazebo

gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_ros_gz_sim, 'launch', 'gz_sim.launch.py')),
launch_arguments={'gz_args': '-r ' + world_file}.items(),
)
  • IncludeLaunchDescription:调用别人的 launch 文件。这里调用的是 ros_gz_sim 包里的 gz_sim.launch.py(Gazebo 自带的启动文件)
  • gz_args: '-r ' + world_file:告诉 Gazebo "启动后立即加载这个世界文件"(-r 是 run 的意思)

生成机器人

spawn_entity = Node(package='ros_gz_sim', executable='create',
arguments=['-topic', 'robot_description',
'-name', 'mbot',
'-x', '0.0', '-y', '0.0', '-z', '0.3', '-Y', '0.0'],
output='screen')
  • 调用 ros_gz_sim 包里的 create 程序
  • -topic robot_description:从 ROS2 话题上读取机器人模型(URDF 字符串)
  • -name mbot:给机器人取个名字
  • -x 0.0 -y 0.0 -z 0.3 -Y 0.0:初始位置(x=0, y=0, z=0.3m 离地面一点,偏航角=0)

robot_state_publisher

params = {'robot_description': robot_description_config.toxml(), 'use_sim_time': True}
node_robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[params]
)
  • robot_description:把展开后的 URDF 作为参数传进去——robot_state_publisher 需要知道机器人的结构才能计算坐标变换
  • use_sim_time: True:使用仿真时间(而不是真实时间)——这是仿真场景下必须设的

ros_gz_bridge

ros_gz_bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
parameters=[{
'config_file': os.path.join(pkg_path, 'config', 'ros_gz_bridge_mbot.yaml'),
}],
)
  • 启动 ros_gz_bridge,并告诉它"用 config/ros_gz_bridge_mbot.yaml 这个配置文件来决定桥接哪些话题"
  • 桥接配置文件的具体内容见 3.4 节

最后的"启动!"

return LaunchDescription([
gazebo, # ① 启动 Gazebo
spawn_entity, # ② 把机器人放进去
ros_gz_bridge, # ③ 启动话题桥接
node_robot_state_publisher, # ④ 启动状态发布器
])

LaunchDescription 像一个"发射清单"——把需要启动的 4 个节点放进去,ros2 launch 会把它们全部启动起来。

3.3 URDF 机器人建模文件

URDF 是什么?

URDF(Unified Robot Description Format,统一机器人描述格式)是 ROS 里用来描述机器人结构的 XML 格式。它用 <link> 标签描述"这个机器人由哪些零件组成",用 <joint> 标签描述"这些零件之间怎么连接"。

XACRO 是 URDF 的"升级版"——支持宏(macro)、变量(property)等高级特性,让模型文件更好维护。.xacro 文件最终会被"展开"成 .urdf 文件。

文件关系图

我们这个机器人的 URDF 模型由 5 个 XACRO 文件组成,它们之间有清晰的层级关系:

mbot_gazebo_harmonic.xacro ← 最简版本:只有底盘
├── mbot_base_gazebo_harmonic.xacro ← 底盘本体(轮子 + 万向轮 + 插件)

mbot_with_lidar_gazebo_harmonic.xacro ← 底盘 + 激光雷达
├── mbot_base_gazebo_harmonic.xacro ← (同上)
├── sensors/lidar_gazebo_harmonic.xacro ← 激光雷达传感器定义

mbot_with_camera_gazebo_harmonic.xacro ← 底盘 + 相机
mbot_with_rgbd_gazebo_harmonic.xacro ← 底盘 + RGB-D 深度相机

这种设计思想叫组件化——把公共部分(底盘)写一次,不同传感器各写一个文件,组合起来就能变出不同的机器人配置。就像搭乐高:底盘是"核心块",传感器是"配件"。

顶层文件:mbot_gazebo_harmonic.xacro

<?xml version="1.0"?>
<robot name="mbot" xmlns:xacro="http://www.ros.org/wiki/xacro">

<xacro:include filename="$(find learning_gazebo_harmonic)/urdf/mbot_base_gazebo_harmonic.xacro" />

<xacro:mbot_base_gazebo/>

</robot>
解释
xmlns:xacro声明"这个文件使用 xacro 语法"
<xacro:include>引入外部 xacro 文件——相当于 Python 的 import
<xacro:mbot_base_gazebo/>调用 mbot_base_gazebo_harmonic.xacro 里定义的名为 mbot_base_gazebo 的宏

这 3 行 XML 就定义了一个完整的机器人——因为真实的工作都在 mbot_base_gazebo_harmonic.xacro 里做好了。

底盘文件:mbot_base_gazebo_harmonic.xacro

这个文件是整个机器人定义的核心,分为以下几个部分:

(1)属性(变量)定义
<xacro:property name="base_mass" value="1" />
<xacro:property name="base_radius" value="0.20"/>
<xacro:property name="base_length" value="0.16"/>

<xacro:property name="wheel_radius" value="0.06"/>
<xacro:property name="wheel_joint_y" value="0.19"/>
<xacro:property name="wheel_joint_z" value="0.05"/>

这些是机器人的物理参数。像编程里的变量一样,定义一次,后面反复使用。如果你想改机器人尺寸(比如加大轮子),改这里的数字就行,不用满文件搜。

(2)颜色材质
<material name="yellow">
<color rgba="1 0.4 0 1"/>
</material>

RGBA 颜色:红(1) 绿(0.4) 蓝(0) 透明度(1)。底盘用黄色,轮子用灰色,万向轮用黑色。

(3)惯性矩阵宏
<xacro:macro name="sphere_inertial_matrix" params="m r">
<inertial>
<mass value="${m}" />
<inertia ixx="${2*m*r*r/5}" ixy="0" ixz="0"
iyy="${2*m*r*r/5}" iyz="0"
izz="${2*m*r*r/5}" />
</inertial>
</xacro:macro>
  • xacro:macro 定义一个"代码模板"——传入质量 m 和半径 r,自动算出球体的惯性矩阵
  • 惯性矩阵(inertia)是物理仿真的核心参数——Gazebo 用它来计算物体有多"难推转动"
  • ${2*m*r*r/5} 是球体惯性矩的公式(写死在宏里,不需要你手算)
(4)轮子宏
<xacro:macro name="wheel" params="prefix reflect">
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel_link"/>
<axis xyz="0 1 0"/>
</joint>

<link name="${prefix}_wheel_link">
<visual>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_length}"/>
</geometry>
<material name="gray" />
</visual>
<xacro:cylinder_inertial_matrix m="${wheel_mass}" r="${wheel_radius}" h="${wheel_length}" />
</link>

<gazebo reference="${prefix}_wheel_link">
<mu1>100000.0</mu1>
<mu2>100000.0</mu2>
</gazebo>
</xacro:macro>
  • ${prefix}${reflect} 是宏的参数——调用 wheel(prefix="left", reflect=1) 生成左轮,wheel(prefix="right", reflect=-1) 生成右轮
  • <joint type="continuous">:continuous 关节 = 可以无限旋转(适合轮子)
  • <axis xyz="0 1 0">:绕 Y 轴旋转(轮子横着转)
  • <gazebo> 标签里的 <mu1><mu2>摩擦系数——数字很大,轮子几乎不打滑
(5)底盘本体宏
<xacro:macro name="mbot_base_gazebo">
<!-- base_link: 圆盘形底盘 -->
<link name="base_link">...</link>

<!-- 两个驱动轮 -->
<xacro:wheel prefix="left" reflect="1"/>
<xacro:wheel prefix="right" reflect="-1"/>

<!-- 两个万向轮(支撑,不驱动) -->
<xacro:caster prefix="front" reflect="-1"/>
<xacro:caster prefix="back" reflect="1"/>

<!-- Gazebo 插件定义 → 见 3.5 节详解 -->
<gazebo>...</gazebo>
</xacro:macro>

机器人的结构树(从地面往上):

odom(里程计坐标系)
└── base_footprint(机器人在平面上的投影点)
└── base_link(底盘圆盘)
├── left_wheel_link(左轮)
├── right_wheel_link(右轮)
├── front_caster_link(前万向轮)
├── back_caster_link(后万向轮)
└── laser_link(激光雷达,如果加载了 lidar 版本)

传感器文件:激光雷达

mbot_with_lidar_gazebo_harmonic.xacro 在底盘基础上加了一个激光雷达:

<xacro:include filename="...mbot_base_gazebo_harmonic.xacro" />
<xacro:include filename=".../sensors/lidar_gazebo_harmonic.xacro" />

<joint name="lidar_joint" type="fixed">
<origin xyz="0 0 0.105" rpy="0 0 0" />
<parent link="base_link"/>
<child link="laser_link"/>
</joint>

<xacro:laser_lidar prefix="laser"/>
<xacro:mbot_base_gazebo/>
  • lidar_joint 是 fixed 类型关节——把激光雷达固定在底盘上方 0.105m 处(不可旋转)
  • <xacro:laser_lidar> 调用了传感器宏
  • <xacro:mbot_base_gazebo/> 调用底盘宏——组合出完整机器人

3.4 Gazebo 仿真世界的配置

世界文件:empty.sdf

文件位置: worlds/empty.sdf

这个世界文件定义了一个空旷的平面世界——只有地面、光照和物理引擎。核心内容:

<sdf version="1.6">
<world name="empty">
<physics name="1ms" type="ignored">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>

<plugin filename="ignition-gazebo-physics-system" .../>
<plugin filename="ignition-gazebo-user-commands-system" .../>
<plugin filename="ignition-gazebo-scene-broadcaster-system" .../>

<light type="directional" name="sun">...</light>

<model name="ground_plane">
<static>true</static>
<link name="link">
<collision>
<geometry><plane><size>100 100</size></plane></geometry>
</collision>
</link>
</model>
</world>
</sdf>
部分作用
<physics>物理引擎配置:仿真步长 0.001 秒,实时因子 1.0(仿真速度 = 真实速度)
<plugin> 标签加载 Gazebo 系统插件(物理、用户指令、场景广播)
<light>一个方向光(模拟太阳),让世界不是全黑的
<ground_plane>一个 100×100m 的静态地面——机器人不会穿下去

本功能包还有 depot.sdf(仓库场景)和 maze.sdf(迷宫场景),结构类似,只是多了障碍物模型。

桥接配置:ros_gz_bridge_mbot.yaml

文件位置: config/ros_gz_bridge_mbot.yaml

这是 ROS2 和 Gazebo 之间的"翻译字典"——告诉 ros_gz_bridge 哪些话题需要互相翻译:

# 键盘 → 机器人:ROS2 的 cmd_vel 翻译成 Gazebo 的 cmd_vel
- ros_topic_name: "/cmd_vel"
gz_topic_name: "/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ # ROS → Gazebo 方向

# 里程计:Gazebo 的 odometry 翻译成 ROS2 的 /odom
- ros_topic_name: "/odom"
gz_topic_name: "/model/mbot/odometry"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS # Gazebo → ROS 方向

# 关节状态:Gazebo 的 joint_state 翻译成 ROS2 的 /joint_states
- ros_topic_name: "/joint_states"
gz_topic_name: "/world/empty/model/mbot/joint_state"
ros_type_name: "sensor_msgs/msg/JointState"
gz_type_name: "gz.msgs.Model"
direction: GZ_TO_ROS

# TF 坐标变换
- ros_topic_name: "/tf"
gz_topic_name: "/model/mbot/pose"
ros_type_name: "tf2_msgs/msg/TFMessage"
gz_type_name: "gz.msgs.Pose_V"
direction: GZ_TO_ROS

每条桥接规则包含 5 个字段:

字段含义
ros_topic_nameROS2 这边的话题名
gz_topic_nameGazebo 那边的话题名
ros_type_nameROS2 消息类型(决定了消息里有哪些字段)
gz_type_nameGazebo 消息类型
direction翻译方向:ROS_TO_GZ(发到 Gazebo)或 GZ_TO_ROS(从 Gazebo 接收)

数据的流动方向:

ROS_TO_GZ →
ROS2 Gazebo
← GZ_TO_ROS

遥控指令 cmd_vel ────────→ 机器人速度控制
机器人位置 /odom ←──────── 里程计数据
关节角度 /joint_states ←── 关节状态
坐标变换 /tf ←─────────── 位姿信息

3.5 Gazebo 插件:让机器人活起来的关键

本节是新知识,仔细看

插件(plugin) 是 Gazebo 仿真中最核心的概念之一。如果你只学了 URDF 建模,你只能在 Rviz2 里看一个静止的机器人模型。让机器人在物理世界里动起来、传感器产生数据、关节受力反馈——这些都靠插件。

什么是插件?

打个比方:Gazebo 仿真引擎就像一台智能手机,而插件就是手机上安装的 App

类比本体能做
手机硬件Gazebo 引擎提供基础能力(渲染、物理计算)
App插件(Plugin)添加具体功能(控制轮子、读取传感器、发布数据)

没有插件,Gazebo 只是一个有重力的空舞台。有了插件,机器人才能感知、移动、与环境交互。

插件从哪来?

Gazebo(Harmonic 版本)自带了一批系统插件,编译好的二进制文件已经安装在系统里了。你不需要自己写插件或编译它们——直接在模型文件中引用就行。

查看系统已安装的插件:

# 看看 Gazebo 的插件目录里有什么
ls /usr/lib/x86_64-linux-gnu/gz-sim-*/plugins/

你会看到类似这样的输出:

libignition-gazebo-diff-drive-system.so ← 差速驱动
libignition-gazebo-sensors-system.so ← 传感器
libignition-gazebo-joint-state-publisher-system.so ← 关节状态发布
libignition-gazebo-odometry-publisher-system.so ← 里程计
libignition-gazebo-user-commands-system.so ← 用户指令
libignition-gazebo-scene-broadcaster-system.so ← 场景广播
libignition-gazebo-contact-system.so ← 碰撞检测
...

每个 .so 文件就是一个插件。 就像 .exe 是可执行程序,.so 是动态链接库——Gazebo 在运行时加载它们。

如何配置插件?

插件配置写在 URDF/XACRO 文件的 <gazebo> 标签内,或者 SDF 世界文件的 <plugin> 标签内。基本格式:

<plugin filename="插件文件名.so"
name="完整的插件类名">
<!-- 插件的配置参数 -->
</plugin>

本项目用到的插件详解

我们的机器人模型(mbot_base_gazebo_harmonic.xacro)中定义了一组插件,每个都扮演着不同角色:

① DiffDrive 差速驱动插件
<plugin filename="libignition-gazebo-diff-drive-system.so"
name="ignition::gazebo::systems::DiffDrive">
<update_rate>30</update_rate>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.38</wheel_separation>
<wheel_radius>0.06</wheel_radius>
<topic>cmd_vel</topic>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<publish_wheel_tf>true</publish_wheel_tf>
<odometry_topic>odom</odometry_topic>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_footprint</robot_base_frame>
</plugin>

这个插件做了什么?——它是整个仿真最关键的部分。

  • 它监听 /cmd_vel 话题(接收速度指令)
  • 收到 Twist 消息后,根据差速运动学公式,把线速度和角速度转换为左右轮各自的转速
  • 控制 left_wheel_jointright_wheel_joint 两个关节,驱动轮子转动
  • 同时发布里程计数据——告诉你机器人走了多远、转了多少度
参数含义
left_joint / right_joint指定左右驱动轮对应的关节名
wheel_separation左右轮间距(影响转弯半径的计算)
wheel_radius轮子半径(影响线速度的计算)
topic监听的话题名(默认 cmd_vel
publish_odom是否发布里程计

这就是为什么你按 i 键(teleop_twist_keyboard 发送 /cmd_vel),机器人就会动起来。

② Sensors 传感器插件
<plugin filename="ignition-gazebo-sensors-system"
name="ignition::gazebo::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>
  • 启用 Gazebo 的传感器系统——让激光雷达、相机等传感器能够"看见"东西
  • render_engine: ogre2:使用 OGRE2 渲染引擎来生成传感器数据
  • 没有这个插件,激光雷达不会产生点云数据
③ JointStatePublisher 关节状态发布器
<plugin filename="ignition-gazebo-joint-state-publisher-system"
name="ignition::gazebo::systems::JointStatePublisher"/>
  • 持续读取所有关节的当前位置(角度、速度)
  • 发布到 Gazebo 的 /world/empty/model/mbot/joint_state 话题
  • ros_gz_bridge 把这些数据桥接到 ROS2 的 /joint_states
  • robot_state_publisher 读取 /joint_states 来计算 TF 坐标变换
④ OdometryPublisher 里程计发布器
<plugin filename="ignition-gazebo-odometry-publisher-system"
name="ignition::gazebo::systems::OdometryPublisher">
<odom_frame>odom</odom_frame>
<robot_base_frame>base_footprint</robot_base_frame>
</plugin>
  • 计算并发布机器人的里程计(位置 + 速度)
  • 读取 odombase_footprint 之间的变换,算出机器人相对出发点的位移
⑤ UserCommands & SceneBroadcaster — 基础设施
<plugin filename="ignition-gazebo-user-commands-system"
name="ignition::gazebo::systems::UserCommands"/>
<plugin filename="ignition-gazebo-scene-broadcaster-system"
name="ignition::gazebo::systems::SceneBroadcaster"/>
  • UserCommands:允许通过命令行或 Gazebo GUI 发送指令(如暂停/恢复仿真)
  • SceneBroadcaster:广播场景状态变化(模型增删、位姿更新等)
  • 这些是"必备基础设施"——大部分世界文件都会包含它们

传感器的插件配置

激光雷达传感器也有自己的插件配置(lidar_gazebo_harmonic.xacro):

<gazebo reference="laser_link">
<sensor type="gpu_lidar" name="gpu_lidar">
<topic>lidar</topic>
<update_rate>20</update_rate>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<min_angle>-3.14</min_angle>
<max_angle>3.14</max_angle>
</horizontal>
</scan>
<range>
<min>0.08</min>
<max>10.0</max>
</range>
</ray>
<alwaysOn>1</alwaysOn>
<visualize>true</visualize>
</sensor>
</gazebo>
参数含义
type="gpu_lidar"GPU 加速的激光雷达(比 CPU 版更快)
update_rate: 20每秒扫描 20 次
samples: 360每圈 360 个采样点(1° 一个点)
min_angle / max_angle扫描角度范围(-π 到 π = 360°)
range: 0.08 ~ 10.0探测距离:8cm 到 10m
visualize: true在 Gazebo 窗口中显示激光射线(可视化调试)

3.6 小结:代码文件的完整版图

把前面所有的代码文件放在一张全景图上:

┌─────────────────────────────────────┐
│ ros2 launch 命令 │
│ learning_gazebo_harmonic │
│ load_urdf_into_gazebo_harmonic │
└──────────────┬──────────────────────┘
│ 读取
┌──────────────▼──────────────────────┐
│ launch/ │
│ load_urdf_into_gazebo_harmonic │
│ .launch.py │
│ ┌─────────────────────────────┐ │
│ │ 启动 4 个节点: │ │
│ │ ① Gazebo (加载 empty.sdf) │ │
│ │ ② Spawn (加载 mbot xacro) │ │
│ │ ③ ros_gz_bridge (YAML 配置) │ │
│ │ ④ robot_state_publisher │ │
│ └─────────────────────────────┘ │
└──┬────────┬──────────┬──────────────┘
│ │ │
┌────────────▼──┐ ┌──▼──────┐ ┌─▼───────────────┐
│ worlds/ │ │ urdf/ │ │ config/ │
│ empty.sdf │ │ *.xacro │ │ *bridge*.yaml │
│ (世界定义) │ │ (机器人) │ │ (话题桥接) │
└───────────────┘ └─────────┘ └─────────────────┘

┌──────────────┼──────────────┐
│ │ │
┌───────▼──────┐ ┌─────▼─────┐ ┌───────▼────────┐
│ mbot_base │ │ sensors/ │ │ mbot_with_ │
│ _gazebo_ │ │ lidar_ │ │ lidar_gazebo_ │
│harmonic.xacro│ │gazebo_ │ │ harmonic.xacro │
│ │ │harmonic. │ │ │
│ 底盘+插件 │ │xacro │ │ 组合:底盘 │
│ │ │ 激光雷达 │ │ + 激光雷达 │
└──────────────┘ └───────────┘ └────────────────┘

┌────────────┐ ┌────────────┐
│ setup.py │ │ package.xml│
│ 安装路径 │ │ 依赖声明 │
└────────────┘ └────────────┘

看懂这张图,你就理解了一个 ROS2 + Gazebo 仿真工程的全貌。

下一步,你可以试着:

  • empty.sdf 换成 maze.sdf,看看机器人在迷宫里跑的效果
  • 试试 mbot_with_lidar_gazebo_harmonic.xacro,加载带激光雷达的机器人
  • 打开 RViz2,订阅 /scan 话题,实时查看激光雷达的点云数据

这些就是后面的进阶内容了——你已经准备好了。