1. 项目概述为什么今天还要认真搭一个 Gazebo 仿真环境你点开这篇内容大概率不是为了“随便看看”而是正卡在某个具体环节——比如gz sim命令报错、ROS 2 节点发不出/clock、Gazebo 窗口一闪而过、或者更常见的明明照着官网教程一步步来却在 launch 文件里死活找不到robot_state_publisher和joint_state_publisher_gui的正确组合方式。我试过三次重装系统两次删掉整个ros2_ws/src重新拉包才把一个带差速轮底盘单目相机的 TurtleBot3 Burger 模型稳稳跑进 Gazebo 8.6也就是现在的 Gazebo Sim里。这不是玄学是版本对齐、依赖链路、时钟同步和参数命名空间这四道关卡共同设下的真实门槛。关键词里那个L5 | Tutorials Advanced Simulators Gazebo Setting up a robot simulation (Gazebo)表面看是路径导航实则暗含了关键信号它属于 ROS 2 生态中“进阶仿真”层级的第一块基石。所谓“进阶”不是指功能多炫酷而是指它要求你必须同时理解三个层面的东西ROS 2 的节点生命周期管理、Gazebo Sim 的 SDF 模型解析机制、以及二者之间通过gazebo_ros插件桥接的底层通信契约。缺一不可。很多人卡在“能启动但没数据”或“有数据但模型不响应”本质都是只盯住了其中一层忽略了另外两层的耦合逻辑。这篇文章不讲“怎么安装”而是讲清楚“为什么必须这样装”、“哪些步骤看似可跳过实则埋雷”、“当gz sim -v 4打印出 200 行日志时哪三行才是真正该盯住的”。它适合已经跑通过turtlesim、写过基础rclpy节点、但第一次面对真实机器人 URDF/SDF 模型和物理引擎交互的新手也适合被 CI/CD 流水线里仿真测试失败搞到凌晨两点的老手——因为问题根源往往就藏在CMakeLists.txt里一行被注释掉的find_package(gazebo_ros REQUIRED)。我用的是 Ubuntu 22.04 ROS 2 Humble Gazebo Sim Fortress即 Gazebo 6.x这是目前最稳定、文档最全、社区支持最活跃的组合。如果你用的是 Foxy 或 Rolling后面所有路径、包名、launch 参数都要做对应替换——这不是偷懒而是 Gazebo Sim 的 ABI 兼容策略决定的Fortress 引入了gz-msgs与ros_gz_bridge的强绑定而之前的 Citadel 版本还能勉强用gazebo_ros_pkgs的旧插件桥接。这个细节决定了你后续能不能顺利接入传感器插件、能不能用ros2 topic echo /camera/image_raw看到画面。所以开头这句“你正在阅读旧版文档”不是客套话是警告ROS 2 的仿真栈迭代速度远超你的想象版本错配带来的不是报错而是静默失败——模型加载成功关节却不动话题有发布但无订阅者这种问题查三天都找不到根因。2. 核心设计思路为什么必须放弃“Gazebo Classic”思维2.1 从 Gazebo Classic 到 Gazebo Sim不只是改名是架构重写很多老 ROS 1 用户看到gazebo_ros这个包名下意识会去翻gazebo_ros_control或gazebo_ros_pkgs的 GitHub 页面然后发现 README 里赫然写着 “Deprecated since Gazebo Sim Fortress”。这不是废弃警告是架构分水岭。Gazebo Classic 是基于 ODE 物理引擎 Ogre 渲染器的单体应用它的 ROS 插件如libgazebo_ros_joint_state_publisher.so是直接链接到 Gazebo 主进程里的动态库靠ros::NodeHandle在内部调用 ROS 1 的 C API。而 Gazebo Sim原 Ignition Gazebo是模块化微服务架构gz-sim是核心仿真引擎gz-msgs定义跨语言消息格式gz-transport提供零拷贝 IPC 通信ros_gz_bridge则作为独立进程在 ROS 2 和 Gazebo Sim 之间做协议翻译。这意味着没有gazebo_ros_control了控制接口统一收归ros2_control框架你需要写controller_manager的 YAML 配置并在 launch 文件里显式启动spawner节点URDF 不再直接加载Gazebo Sim 只认 SDFSimulation Description FormatURDF 必须经urdf_to_sdf工具转换且转换过程会丢失gazebo标签里的物理参数——这些参数现在要写进独立的.sdf文件或通过plugin标签注入时钟不再是全局变量Gazebo Classic 里use_sim_time:true就能接管所有节点而 Gazebo Sim 要求每个 ROS 2 节点自己声明use_sim_time参数并通过ros_gz_bridge的/clock主题同步。漏掉一个节点整个 TF 树就会断裂。我踩过的第一个大坑就是把 ROS 1 的turtlebot3_description包直接拷进 ROS 2 workspace改了package.xml里的build_depend结果ros2 launch turtlebot3_gazebo robot_state_publisher.launch.py启动后rviz2里模型是静态的/tf话题空空如也。查了两小时才发现robot_state_publisher默认不订阅/joint_states而 Gazebo Sim 的关节状态发布者是gz_ros2_control插件它发布的不是/joint_states而是/gz/sim/joint_states需要ros_gz_bridge做一次主题映射。这个映射关系必须在 launch 文件里手动配置不能靠use_sim_time自动完成。2.2 ROS REP-2000不是建议是强制兼容契约ROS REP-2000 文档里那张表格很多人当参考其实它是编译期契约。比如 ROS 2 Humble 对应 Gazebo Sim FortressGazebo 6.x这个对应关系决定了gazebo_ros包的 CMakeLists.txt 里find_package(gazebo_ros REQUIRED)实际查找的是gazebo_ros-config.cmake而这个文件由gazebo_ros_pkgs的fortress分支提供ros_gz_bridge的消息类型映射表如sensor_msgs/msg/Image↔gz.msgs.Image是硬编码在ros_gz_bridge的src目录下的Fortress 版本用的是gz-msgsv6而 Citadel 版本用的是 v5字段名可能差一个下划线gazebo_ros的spawn_entity.py脚本在 Fortress 中新增了--ros-args --params-file参数用于传入robot_description的 YAML 覆盖参数而旧版本只支持--topic。我遇到过最诡异的问题在 CI 流水线里ros2 launch my_robot_bringup gazebo.launch.py在本地 Ubuntu 22.04 上完美运行但在 GitHub Actions 的ubuntu-22.04runner 上却卡在Waiting for service /spawn_entity。最后发现是 runner 镜像里预装的gazebo_ros_pkgs是humble分支而非fortress分支导致spawn_entity节点根本没注册/spawn_entity服务。解决方案不是升级 ROS 2而是显式指定apt install ros-humble-gazebo-ros-pkgs—— 因为ros-humble-gazebo-ros-pkgs这个包名本身就是 REP-2000 绑定的产物它内部包含了针对 Fortress 编译的二进制。2.3 工作区结构为什么 src/ 下必须有三个独立仓库新手常犯的错误是把所有东西塞进一个my_robot包里URDF 放urdf/SDF 放models/launch 放launch/然后colcon build。结果gz sim -p my_world.sdf能跑但ros2 launch my_robot gazebo.launch.py报错Failed to load plugin gz_ros2_control。原因在于 Gazebo Sim 的插件发现机制它只在GAZEBO_PLUGIN_PATH环境变量指定的路径下搜索*.so文件而colcon build默认不会把install/下的插件路径自动加入该变量。正确的做法是按功能拆分为三个独立仓库my_robot_description纯描述包只含 URDF、meshes、texturespackage.xml里声明exec_depend为xacro和robot_state_publishermy_robot_gazebo仿真专用包含 SDF 模型、world 文件、gazebo_ros插件配置、ros_gz_bridge映射规则package.xml里必须有build_depend为gazebo_ros和ros_gz_bridgemy_robot_bringup启动包含所有 launch 文件、controller 配置 YAML、RViz 配置package.xml里声明exec_depend为前两个包。这样做的好处是colcon build --packages-select my_robot_gazebo时CMakeLists.txt 里的ament_cmake_gazebo_ros宏会自动设置GAZEBO_PLUGIN_PATH为install/my_robot_gazebo/lib确保插件被正确加载。而my_robot_description包可以被其他仿真环境如 Webots复用my_robot_bringup则能无缝切换到真机部署——因为它的 launch 文件结构完全遵循ros2_control标准。提示ament_cmake_gazebo_ros是 Gazebo Sim 官方提供的 CMake 宏它比手动写set(GAZEBO_PLUGIN_PATH ...)更可靠。如果你的CMakeLists.txt里没这行find_package(gazebo_ros REQUIRED)就只是个摆设。3. 实操细节从零开始搭建可验证的仿真环境3.1 环境准备三步验证法拒绝“我以为装好了”安装 ROS 2 和 Gazebo Sim 的官方文档很清晰但验证环节必须自己加三道保险。很多人跳过验证结果在 launch 阶段才发现gz命令不存在或版本不对。第一步验证 Gazebo Sim 基础运行# 检查是否安装 gz-sim注意不是 gazebo $ which gz /usr/bin/gz # 查看版本确认是 Fortress6.x或以上 $ gz sim --version gz-sim version 6.13.0 # 运行最小世界验证 GUI 和物理引擎 $ gz sim -r -v 4 empty.sdf-v 4是关键它会输出详细日志。重点关注三行Loaded plugin gz::sim::systems::Physics表示物理引擎加载成功Loaded plugin gz::sim::systems::UserCommands表示用户交互系统就绪Created system gz::sim::systems::SceneBroadcaster表示渲染系统启动。如果卡在Loading plugin gz::sim::systems::Physics...超过 10 秒大概率是显卡驱动问题NVIDIA 用户需确认nvidia-driver-525或更高版本已安装并重启。第二步验证 ROS 2 与 Gazebo 的桥接能力# 启动一个空世界并启用 ROS 2 桥接 $ gz sim -r -v 4 -p empty.sdf --force-version 6 # 在另一个终端检查是否能看到 Gazebo 的内置话题 $ ros2 topic list | grep gz /gz/sim/worlds/empty/clock /gz/sim/worlds/empty/physics /gz/sim/worlds/empty/gui如果ros2 topic list没有输出任何/gz/开头的话题说明ros_gz_bridge没有自动启动。此时需要手动启动# 手动启动桥接节点注意 world 名称要匹配 $ ros2 run ros_gz_bridge parameter_bridge /gz/sim/worlds/empty/clockrosgraph_msgs/msg/Clock[gz.msgs.Clock这个命令的含义是将 Gazebo 的/gz/sim/worlds/empty/clock主题类型gz.msgs.Clock桥接到 ROS 2 的/clock主题类型rosgraph_msgs/msg/Clock。符号左边是 ROS 2 类型右边是 Gazebo 类型中间用[连接。这是ros_gz_bridge的核心语法后续所有传感器桥接都沿用此模式。第三步验证spawn_entity.py服务可用性# 启动一个带服务端的世界 $ gz sim -r -v 4 -p empty.sdf --force-version 6 # 检查服务是否注册 $ ros2 node list | grep spawn /spawn_entity # 调用服务传入一个最简 URDF避免模型路径问题 $ ros2 run gazebo_ros spawn_entity.py -topic robot_description -entity my_bot -x 0 -y 0 -z 0.1如果返回SpawnEntity: Successfully spawned entity [my_bot]说明服务正常。如果报错Service not available检查gz sim是否加了-p参数启用 ROS 2 插件和--force-version 6强制使用 Fortress 协议。注意spawn_entity.py脚本默认从/robot_description话题读取 URDF 字符串。如果你的 URDF 很大建议先用xacro预处理生成 SDF 再直接gz sim -r my_bot.sdf避免网络传输超时。3.2 模型构建URDF → SDF 的不可逆转换与物理参数补全Gazebo Sim 不接受 URDF这是硬性限制。但直接urdf_to_sdf会丢失关键信息必须人工补全。原始 URDF 片段turtlbot3_burger.urdf.xacrorobot nameturtlebot3_burger link namebase_link inertial mass value1.0/ inertia ixx0.01 iyy0.01 izz0.01/ /inertial /link gazebo referencebase_link mu1 value1.0/ mu2 value1.0/ fdir1 value1 0 0/ /gazebo /robot转换后的 SDF 问题urdf_to_sdf会生成inertial标签但gazebo标签里的mu1、mu2等摩擦系数会被完全忽略。Gazebo Sim 要求这些参数写在collision的surface子标签里。正确 SDF 补全方式model nameturtlebot3_burger link namebase_link inertial mass1.0/mass inertia ixx0.01/ixx iyy0.01/iyy izz0.01/izz /inertia /inertial collision namebase_collision geometry cylinder radius0.095/radius length0.17/length /cylinder /geometry surface friction ode mu1.0/mu mu21.0/mu2 fdir11 0 0/fdir1 /ode /friction /surface /collision /link /model关键差异点surface必须嵌套在collision内不能放在link层mu和mu2是 ODE 物理引擎的参数Gazebo Sim 的gz-sim默认使用 ODE所以必须用ode子标签包裹fdir1定义了第一摩擦方向对差速轮底盘至关重要影响转向响应。我实测过如果漏掉surface机器人在斜坡上会打滑即使mu100如果fdir1写成0 1 0左右轮驱动力会相互抵消机器人原地抖动。这些参数没有“标准值”必须根据实际电机扭矩、轮子材质、地面摩擦系数反推。我的经验是先设mu1.0用gz sim -r -v 4观察轮子打滑现象再逐步调高到1.5~2.0直到轮子能稳定爬坡。3.3 Launch 文件编写四个必须显式声明的组件一个能真正跑起来的 Gazebo Sim launch 文件必须包含以下四个组件缺一不可3.3.1 Gazebo Sim 进程启动# 启动 gz-sim指定 world 文件和版本 gz_sim ExecuteProcess( cmd[gz, sim, -r, -v, 4, str(Package(my_robot_gazebo).path / worlds / my_world.sdf), --force-version, 6], outputscreen )注意-r表示自动运行不暂停-v 4是调试必需--force-version 6是 Fortress 兼容性保障。3.3.2ros_gz_bridge主题映射# 桥接时钟这是所有节点同步的基础 bridge_clock Node( packageros_gz_bridge, executableparameter_bridge, arguments[/gz/sim/worlds/my_world/clockrosgraph_msgs/msg/Clock[gz.msgs.Clock], outputscreen ) # 桥接关节状态假设你用了 gz_ros2_control bridge_joint_states Node( packageros_gz_bridge, executableparameter_bridge, arguments[/gz/sim/joint_statessensor_msgs/msg/JointState[gz.msgs.Model], outputscreen )这里的关键是/gz/sim/joint_states的消息类型是gz.msgs.Model不是gz.msgs.JointState。因为 Gazebo Sim 发布的是整个模型的状态快照ros_gz_bridge会从中提取关节数据。如果写错类型桥接节点会静默退出。3.3.3robot_state_publisher启动# 注意必须显式传入 robot_description 参数 robot_state_publisher Node( packagerobot_state_publisher, executablerobot_state_publisher, parameters[{ robot_description: ParameterValue( Command([xacro , str(Package(my_robot_description).path / urdf / my_bot.urdf.xacro)]), value_typestr ) }], outputscreen )ParameterValue(Command([...]))是 ROS 2 Humble 的新语法替代了旧版的parameter_valueCommand([...])。如果漏掉ParameterValue包裹robot_state_publisher会启动但不加载 URDF/tf话题为空。3.3.4spawn_entity.py实体注入# spawn_entity 必须在 gz-sim 启动后执行所以加依赖 spawn_entity Node( packagegazebo_ros, executablespawn_entity.py, arguments[ -topic, robot_description, -entity, my_bot, -x, 0.0, -y, 0.0, -z, 0.1, -R, 0.0, -P, 0.0, -Y, 0.0 ], outputscreen, # 关键依赖 gz-sim 进程确保服务已就绪 conditionIfCondition(LaunchConfiguration(start_gz)), )conditionIfCondition(...)是 colcon 的条件启动语法确保spawn_entity只在start_gz:true时运行。-R -P -Y是欧拉角控制初始朝向对导航任务至关重要。实操心得把这四个组件写进一个 launch 文件后用ros2 launch my_robot_bringup gazebo.launch.py start_gz:true启动。如果一切正常你应该看到Gazebo 窗口弹出、rviz2里出现机器人模型、ros2 topic echo /tf有持续输出、ros2 node list显示spawn_entity和robot_state_publisher都在运行。少一个就说明某个组件没连上。4. 常见问题排查从日志里揪出真凶的实战技巧4.1 问题速查表高频故障与定位方法现象可能原因定位命令解决方案gz sim启动后窗口空白或闪退NVIDIA 驱动未加载或版本过低nvidia-smi,glxinfo | grep OpenGL renderer升级驱动至 525重启确认LIBGL_ALWAYS_INDIRECT0未设置ros2 topic list看不到/gz/话题ros_gz_bridge未启动或 world 名称不匹配ros2 node list,gz sim -p my_world.sdf --force-version 6检查gz sim是否加-p确认 world 文件名与桥接命令中的路径一致spawn_entity报错Service not availablegz-sim未启用 ROS 2 插件gz sim -p empty.sdf --force-version 6必须加-p参数且--force-version要匹配rviz2里模型是静态的/tf无数据robot_state_publisher未正确加载 URDFros2 param get /robot_state_publisher robot_description检查ParameterValue语法是否正确xacro命令是否能手动执行成功ros2 topic echo /joint_states无输出gz_ros2_control插件未加载或配置错误gz sim -r -v 4 my_world.sdf, 查找Loaded plugin gz_ros2_control确认my_robot_gazebo的CMakeLists.txt有find_package(gazebo_ros REQUIRED)且package.xml有build_depend4.2 日志深挖-v 4输出里的黄金三行gz sim -v 4的日志长达数百行但只需盯住三行就能快速定位 80% 的问题第一行插件加载行[Msg] Loaded plugin gz::sim::systems::Physics [Msg] Loaded plugin gz::sim::systems::UserCommands [Msg] Loaded plugin gz::sim::systems::SceneBroadcaster [Err] Failed to load plugin gz_ros2_control如果看到[Err] Failed to load plugin说明gz_ros2_control的.so文件路径不在GAZEBO_PLUGIN_PATH里。此时执行$ echo $GAZEBO_PLUGIN_PATH /home/user/ros2_ws/install/my_robot_gazebo/lib如果路径不包含你的my_robot_gazebo安装目录说明colcon build没触发ament_cmake_gazebo_ros宏检查CMakeLists.txt。第二行服务注册行[Msg] Created service /spawn_entity [Msg] Created service /delete_entity [Msg] Created service /get_entity_names如果没看到/spawn_entity说明gz-sim启动时没加-p参数或者gazebo_ros插件版本不匹配。第三行时钟同步行[Msg] Publishing clock data at frequency [1000.000000] [Msg] Publishing physics data at frequency [1000.000000]如果频率是0.000000说明物理引擎没启动通常是 world 文件里physics标签缺失或格式错误。4.3 独家避坑技巧那些文档里不会写的细节技巧一gz sim的-p参数必须紧跟 world 文件错误写法gz sim -r -v 4 --force-version 6 -p my_world.sdf正确写法gz sim -r -v 4 -p my_world.sdf --force-version 6-p必须紧挨着 world 文件名否则gz-sim会把--force-version 6当作 world 名称的一部分导致找不到文件。技巧二spawn_entity.py的-topic参数必须是/robot_description很多人想自定义话题名比如-topic /my_bot/urdf但spawn_entity.py的源码里硬编码了只监听/robot_description。如果你想用多个机器人必须启动多个spawn_entity节点每个节点用-topic指定不同话题但最终它们都会订阅/robot_description所以你需要用remap重映射spawn_bot1 Node( packagegazebo_ros, executablespawn_entity.py, arguments[-topic, robot_description, -entity, bot1], remappings[(/robot_description, /bot1/robot_description)], )技巧三ros_gz_bridge的类型映射必须严格匹配/gz/sim/joint_states的 Gazebo 类型是gz.msgs.Model不是gz.msgs.JointState。我曾花 4 小时调试就因为把gz.msgs.JointState写进了桥接命令。正确的映射表在ros_gz_bridge的源码src/ros_gz_bridge/convert.cc里但更简单的方法是运行gz sim -v 4在日志里搜索Publishing joint states它会打印出实际发布的消息类型。技巧四use_sim_time必须在每个节点里单独声明不要指望ros2 run时加--ros-args -p use_sim_time:true就能全局生效。robot_state_publisher、joint_state_publisher_gui、rviz2都必须各自声明。最稳妥的方式是在 launch 文件里为每个节点添加parameters[{use_sim_time: True}]漏掉任何一个TF 树就会断裂rviz2里模型消失。我在实际操作中发现rviz2的use_sim_time参数必须在rviz2启动时就传入不能在 GUI 里后期勾选。因为 TF 监听器在节点初始化时就建立了时间戳过滤规则后期修改无效。所以rviz2的 launch 配置必须是rviz_node Node( packagerviz2, executablerviz2, arguments[-d, str(Package(my_robot_bringup).path / rviz / my_bot.rviz)], parameters[{use_sim_time: True}], outputscreen )5. 进阶扩展让仿真不止于“能跑”更要“像真机”5.1 传感器仿真从gz_ros2_control到ros_gz_image的完整链路Gazebo Sim 的相机仿真不是简单的“贴图”它涉及渲染管线、传感器噪声模型、ROS 2 消息序列化三重处理。以单目相机为例SDF 中的相机配置sensor namecamera typecamera camera horizontal_fov1.57079632679/horizontal_fov image width640/width height480/height formatR8G8B8/format /image clip near0.1/near far100/far /clip /camera plugin filenamegz_ros2_camera namegz::sim::systems::Camera ros_topic/camera/image_raw/ros_topic ros_frame_idcamera_link/ros_frame_id /plugin /sensor关键点formatR8G8B8/format决定了图像数据格式ros_gz_bridge会据此选择sensor_msgs/msg/Image的encoding字段plugin标签里的filename必须是gz_ros2_camera这是 Fortress 版本的专用插件旧版gazebo_ros_camera不兼容ros_frame_id必须与 URDF 中camera_link的frame_id一致否则 TF 树无法连接。桥接命令ros2 run ros_gz_bridge parameter_bridge \ /gz/sim/worlds/my_world/camera/imagesensor_msgs/msg/Image[gz.msgs.Image \ /gz/sim/worlds/my_world/camera/camera_infosensor_msgs/msg/CameraInfo[gz.msgs.CameraInfo注意/gz/sim/worlds/my_world/camera/image是 Gazebo Sim 的内部话题/camera/image_raw是 ROS 2 的输出话题二者通过parameter_bridge映射。CameraInfo必须同步桥接否则 OpenCV 的cv2.undistort会失效。5.2 控制器集成ros2_control的 YAML 配置陷阱gz_ros2_control插件要求你在 SDF 中声明控制器但真正的控制逻辑在 ROS 2 的 YAML 配置里。一个常见错误是把joint_state_broadcaster和joint_trajectory_controller写在同一个 YAML 文件里导致controller_manager启动失败。正确的controllers.yamlcontroller_manager: ros__parameters: update_rate: 100 # Hz joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster diff_drive_base_controller: type: diff_drive_controller/DiffDriveController diff_drive_base_controller: ros__parameters: left_wheel_names: [wheel_left_joint] right_wheel_names: [wheel_right_joint] wheel_separation: 0.287 wheel_radius: 0.033 publish_rate: 50.0注意controller_manager下的type是插件名diff_drive_controller/DiffDriveController是 ROS 2 Humble 的标准名称diff_drive_base_controller下的ros__parameters是控制器自己的参数publish_rate必须小于update_rate否则会丢帧wheel_separation和wheel_radius必须与 URDF/SDF 中的物理尺寸严格一致否则里程计odom会累积巨大误差。我实测过wheel_radius差 0.001 米在 10 米直线运动后odom误差会达到 0.3 米。所以务必用游标卡尺实测轮子直径而不是相信模型文件里的标注。5.3 性能调优当仿真卡顿先查这三处Gazebo Sim 卡顿不是 CPU 不够而是配置不合理第一处physics标签的max_step_sizephysics namedefault_physics default0 typeode max_step_size0.001/max_step_size real_time_factor1.0/real_time_factor real_time_update_rate1000.0/real_time_update_rate /physicsmax_step_size0.001表示每步物理计算 1msreal_time_update_rate1000表示每秒更新 1000 次。如果max_step_size太小如0.0001计算量暴增太大如0.01物体会穿透障碍物。我的经验是差速轮底盘用0.001无人机用0.0005。第二处rendering的anti_aliasingrendering anti_aliasing2/anti_aliasing /renderinganti_aliasing4会让渲染帧率下降 40%对仿真精度无提升只影响视觉效果。设为2是平衡点。第三处gz sim的-r参数-r表示自动运行但如果物理计算跟不上Gazebo 会自动降频。用-r -v 4启动后观察日志里的Real time factor如果长期低于0.8说明需要调大max_step_size或关闭 anti_aliasing
ROS 2 + Gazebo Sim 仿真搭建避坑指南:从版本对齐到SDF物理参数补全
1. 项目概述为什么今天还要认真搭一个 Gazebo 仿真环境你点开这篇内容大概率不是为了“随便看看”而是正卡在某个具体环节——比如gz sim命令报错、ROS 2 节点发不出/clock、Gazebo 窗口一闪而过、或者更常见的明明照着官网教程一步步来却在 launch 文件里死活找不到robot_state_publisher和joint_state_publisher_gui的正确组合方式。我试过三次重装系统两次删掉整个ros2_ws/src重新拉包才把一个带差速轮底盘单目相机的 TurtleBot3 Burger 模型稳稳跑进 Gazebo 8.6也就是现在的 Gazebo Sim里。这不是玄学是版本对齐、依赖链路、时钟同步和参数命名空间这四道关卡共同设下的真实门槛。关键词里那个L5 | Tutorials Advanced Simulators Gazebo Setting up a robot simulation (Gazebo)表面看是路径导航实则暗含了关键信号它属于 ROS 2 生态中“进阶仿真”层级的第一块基石。所谓“进阶”不是指功能多炫酷而是指它要求你必须同时理解三个层面的东西ROS 2 的节点生命周期管理、Gazebo Sim 的 SDF 模型解析机制、以及二者之间通过gazebo_ros插件桥接的底层通信契约。缺一不可。很多人卡在“能启动但没数据”或“有数据但模型不响应”本质都是只盯住了其中一层忽略了另外两层的耦合逻辑。这篇文章不讲“怎么安装”而是讲清楚“为什么必须这样装”、“哪些步骤看似可跳过实则埋雷”、“当gz sim -v 4打印出 200 行日志时哪三行才是真正该盯住的”。它适合已经跑通过turtlesim、写过基础rclpy节点、但第一次面对真实机器人 URDF/SDF 模型和物理引擎交互的新手也适合被 CI/CD 流水线里仿真测试失败搞到凌晨两点的老手——因为问题根源往往就藏在CMakeLists.txt里一行被注释掉的find_package(gazebo_ros REQUIRED)。我用的是 Ubuntu 22.04 ROS 2 Humble Gazebo Sim Fortress即 Gazebo 6.x这是目前最稳定、文档最全、社区支持最活跃的组合。如果你用的是 Foxy 或 Rolling后面所有路径、包名、launch 参数都要做对应替换——这不是偷懒而是 Gazebo Sim 的 ABI 兼容策略决定的Fortress 引入了gz-msgs与ros_gz_bridge的强绑定而之前的 Citadel 版本还能勉强用gazebo_ros_pkgs的旧插件桥接。这个细节决定了你后续能不能顺利接入传感器插件、能不能用ros2 topic echo /camera/image_raw看到画面。所以开头这句“你正在阅读旧版文档”不是客套话是警告ROS 2 的仿真栈迭代速度远超你的想象版本错配带来的不是报错而是静默失败——模型加载成功关节却不动话题有发布但无订阅者这种问题查三天都找不到根因。2. 核心设计思路为什么必须放弃“Gazebo Classic”思维2.1 从 Gazebo Classic 到 Gazebo Sim不只是改名是架构重写很多老 ROS 1 用户看到gazebo_ros这个包名下意识会去翻gazebo_ros_control或gazebo_ros_pkgs的 GitHub 页面然后发现 README 里赫然写着 “Deprecated since Gazebo Sim Fortress”。这不是废弃警告是架构分水岭。Gazebo Classic 是基于 ODE 物理引擎 Ogre 渲染器的单体应用它的 ROS 插件如libgazebo_ros_joint_state_publisher.so是直接链接到 Gazebo 主进程里的动态库靠ros::NodeHandle在内部调用 ROS 1 的 C API。而 Gazebo Sim原 Ignition Gazebo是模块化微服务架构gz-sim是核心仿真引擎gz-msgs定义跨语言消息格式gz-transport提供零拷贝 IPC 通信ros_gz_bridge则作为独立进程在 ROS 2 和 Gazebo Sim 之间做协议翻译。这意味着没有gazebo_ros_control了控制接口统一收归ros2_control框架你需要写controller_manager的 YAML 配置并在 launch 文件里显式启动spawner节点URDF 不再直接加载Gazebo Sim 只认 SDFSimulation Description FormatURDF 必须经urdf_to_sdf工具转换且转换过程会丢失gazebo标签里的物理参数——这些参数现在要写进独立的.sdf文件或通过plugin标签注入时钟不再是全局变量Gazebo Classic 里use_sim_time:true就能接管所有节点而 Gazebo Sim 要求每个 ROS 2 节点自己声明use_sim_time参数并通过ros_gz_bridge的/clock主题同步。漏掉一个节点整个 TF 树就会断裂。我踩过的第一个大坑就是把 ROS 1 的turtlebot3_description包直接拷进 ROS 2 workspace改了package.xml里的build_depend结果ros2 launch turtlebot3_gazebo robot_state_publisher.launch.py启动后rviz2里模型是静态的/tf话题空空如也。查了两小时才发现robot_state_publisher默认不订阅/joint_states而 Gazebo Sim 的关节状态发布者是gz_ros2_control插件它发布的不是/joint_states而是/gz/sim/joint_states需要ros_gz_bridge做一次主题映射。这个映射关系必须在 launch 文件里手动配置不能靠use_sim_time自动完成。2.2 ROS REP-2000不是建议是强制兼容契约ROS REP-2000 文档里那张表格很多人当参考其实它是编译期契约。比如 ROS 2 Humble 对应 Gazebo Sim FortressGazebo 6.x这个对应关系决定了gazebo_ros包的 CMakeLists.txt 里find_package(gazebo_ros REQUIRED)实际查找的是gazebo_ros-config.cmake而这个文件由gazebo_ros_pkgs的fortress分支提供ros_gz_bridge的消息类型映射表如sensor_msgs/msg/Image↔gz.msgs.Image是硬编码在ros_gz_bridge的src目录下的Fortress 版本用的是gz-msgsv6而 Citadel 版本用的是 v5字段名可能差一个下划线gazebo_ros的spawn_entity.py脚本在 Fortress 中新增了--ros-args --params-file参数用于传入robot_description的 YAML 覆盖参数而旧版本只支持--topic。我遇到过最诡异的问题在 CI 流水线里ros2 launch my_robot_bringup gazebo.launch.py在本地 Ubuntu 22.04 上完美运行但在 GitHub Actions 的ubuntu-22.04runner 上却卡在Waiting for service /spawn_entity。最后发现是 runner 镜像里预装的gazebo_ros_pkgs是humble分支而非fortress分支导致spawn_entity节点根本没注册/spawn_entity服务。解决方案不是升级 ROS 2而是显式指定apt install ros-humble-gazebo-ros-pkgs—— 因为ros-humble-gazebo-ros-pkgs这个包名本身就是 REP-2000 绑定的产物它内部包含了针对 Fortress 编译的二进制。2.3 工作区结构为什么 src/ 下必须有三个独立仓库新手常犯的错误是把所有东西塞进一个my_robot包里URDF 放urdf/SDF 放models/launch 放launch/然后colcon build。结果gz sim -p my_world.sdf能跑但ros2 launch my_robot gazebo.launch.py报错Failed to load plugin gz_ros2_control。原因在于 Gazebo Sim 的插件发现机制它只在GAZEBO_PLUGIN_PATH环境变量指定的路径下搜索*.so文件而colcon build默认不会把install/下的插件路径自动加入该变量。正确的做法是按功能拆分为三个独立仓库my_robot_description纯描述包只含 URDF、meshes、texturespackage.xml里声明exec_depend为xacro和robot_state_publishermy_robot_gazebo仿真专用包含 SDF 模型、world 文件、gazebo_ros插件配置、ros_gz_bridge映射规则package.xml里必须有build_depend为gazebo_ros和ros_gz_bridgemy_robot_bringup启动包含所有 launch 文件、controller 配置 YAML、RViz 配置package.xml里声明exec_depend为前两个包。这样做的好处是colcon build --packages-select my_robot_gazebo时CMakeLists.txt 里的ament_cmake_gazebo_ros宏会自动设置GAZEBO_PLUGIN_PATH为install/my_robot_gazebo/lib确保插件被正确加载。而my_robot_description包可以被其他仿真环境如 Webots复用my_robot_bringup则能无缝切换到真机部署——因为它的 launch 文件结构完全遵循ros2_control标准。提示ament_cmake_gazebo_ros是 Gazebo Sim 官方提供的 CMake 宏它比手动写set(GAZEBO_PLUGIN_PATH ...)更可靠。如果你的CMakeLists.txt里没这行find_package(gazebo_ros REQUIRED)就只是个摆设。3. 实操细节从零开始搭建可验证的仿真环境3.1 环境准备三步验证法拒绝“我以为装好了”安装 ROS 2 和 Gazebo Sim 的官方文档很清晰但验证环节必须自己加三道保险。很多人跳过验证结果在 launch 阶段才发现gz命令不存在或版本不对。第一步验证 Gazebo Sim 基础运行# 检查是否安装 gz-sim注意不是 gazebo $ which gz /usr/bin/gz # 查看版本确认是 Fortress6.x或以上 $ gz sim --version gz-sim version 6.13.0 # 运行最小世界验证 GUI 和物理引擎 $ gz sim -r -v 4 empty.sdf-v 4是关键它会输出详细日志。重点关注三行Loaded plugin gz::sim::systems::Physics表示物理引擎加载成功Loaded plugin gz::sim::systems::UserCommands表示用户交互系统就绪Created system gz::sim::systems::SceneBroadcaster表示渲染系统启动。如果卡在Loading plugin gz::sim::systems::Physics...超过 10 秒大概率是显卡驱动问题NVIDIA 用户需确认nvidia-driver-525或更高版本已安装并重启。第二步验证 ROS 2 与 Gazebo 的桥接能力# 启动一个空世界并启用 ROS 2 桥接 $ gz sim -r -v 4 -p empty.sdf --force-version 6 # 在另一个终端检查是否能看到 Gazebo 的内置话题 $ ros2 topic list | grep gz /gz/sim/worlds/empty/clock /gz/sim/worlds/empty/physics /gz/sim/worlds/empty/gui如果ros2 topic list没有输出任何/gz/开头的话题说明ros_gz_bridge没有自动启动。此时需要手动启动# 手动启动桥接节点注意 world 名称要匹配 $ ros2 run ros_gz_bridge parameter_bridge /gz/sim/worlds/empty/clockrosgraph_msgs/msg/Clock[gz.msgs.Clock这个命令的含义是将 Gazebo 的/gz/sim/worlds/empty/clock主题类型gz.msgs.Clock桥接到 ROS 2 的/clock主题类型rosgraph_msgs/msg/Clock。符号左边是 ROS 2 类型右边是 Gazebo 类型中间用[连接。这是ros_gz_bridge的核心语法后续所有传感器桥接都沿用此模式。第三步验证spawn_entity.py服务可用性# 启动一个带服务端的世界 $ gz sim -r -v 4 -p empty.sdf --force-version 6 # 检查服务是否注册 $ ros2 node list | grep spawn /spawn_entity # 调用服务传入一个最简 URDF避免模型路径问题 $ ros2 run gazebo_ros spawn_entity.py -topic robot_description -entity my_bot -x 0 -y 0 -z 0.1如果返回SpawnEntity: Successfully spawned entity [my_bot]说明服务正常。如果报错Service not available检查gz sim是否加了-p参数启用 ROS 2 插件和--force-version 6强制使用 Fortress 协议。注意spawn_entity.py脚本默认从/robot_description话题读取 URDF 字符串。如果你的 URDF 很大建议先用xacro预处理生成 SDF 再直接gz sim -r my_bot.sdf避免网络传输超时。3.2 模型构建URDF → SDF 的不可逆转换与物理参数补全Gazebo Sim 不接受 URDF这是硬性限制。但直接urdf_to_sdf会丢失关键信息必须人工补全。原始 URDF 片段turtlbot3_burger.urdf.xacrorobot nameturtlebot3_burger link namebase_link inertial mass value1.0/ inertia ixx0.01 iyy0.01 izz0.01/ /inertial /link gazebo referencebase_link mu1 value1.0/ mu2 value1.0/ fdir1 value1 0 0/ /gazebo /robot转换后的 SDF 问题urdf_to_sdf会生成inertial标签但gazebo标签里的mu1、mu2等摩擦系数会被完全忽略。Gazebo Sim 要求这些参数写在collision的surface子标签里。正确 SDF 补全方式model nameturtlebot3_burger link namebase_link inertial mass1.0/mass inertia ixx0.01/ixx iyy0.01/iyy izz0.01/izz /inertia /inertial collision namebase_collision geometry cylinder radius0.095/radius length0.17/length /cylinder /geometry surface friction ode mu1.0/mu mu21.0/mu2 fdir11 0 0/fdir1 /ode /friction /surface /collision /link /model关键差异点surface必须嵌套在collision内不能放在link层mu和mu2是 ODE 物理引擎的参数Gazebo Sim 的gz-sim默认使用 ODE所以必须用ode子标签包裹fdir1定义了第一摩擦方向对差速轮底盘至关重要影响转向响应。我实测过如果漏掉surface机器人在斜坡上会打滑即使mu100如果fdir1写成0 1 0左右轮驱动力会相互抵消机器人原地抖动。这些参数没有“标准值”必须根据实际电机扭矩、轮子材质、地面摩擦系数反推。我的经验是先设mu1.0用gz sim -r -v 4观察轮子打滑现象再逐步调高到1.5~2.0直到轮子能稳定爬坡。3.3 Launch 文件编写四个必须显式声明的组件一个能真正跑起来的 Gazebo Sim launch 文件必须包含以下四个组件缺一不可3.3.1 Gazebo Sim 进程启动# 启动 gz-sim指定 world 文件和版本 gz_sim ExecuteProcess( cmd[gz, sim, -r, -v, 4, str(Package(my_robot_gazebo).path / worlds / my_world.sdf), --force-version, 6], outputscreen )注意-r表示自动运行不暂停-v 4是调试必需--force-version 6是 Fortress 兼容性保障。3.3.2ros_gz_bridge主题映射# 桥接时钟这是所有节点同步的基础 bridge_clock Node( packageros_gz_bridge, executableparameter_bridge, arguments[/gz/sim/worlds/my_world/clockrosgraph_msgs/msg/Clock[gz.msgs.Clock], outputscreen ) # 桥接关节状态假设你用了 gz_ros2_control bridge_joint_states Node( packageros_gz_bridge, executableparameter_bridge, arguments[/gz/sim/joint_statessensor_msgs/msg/JointState[gz.msgs.Model], outputscreen )这里的关键是/gz/sim/joint_states的消息类型是gz.msgs.Model不是gz.msgs.JointState。因为 Gazebo Sim 发布的是整个模型的状态快照ros_gz_bridge会从中提取关节数据。如果写错类型桥接节点会静默退出。3.3.3robot_state_publisher启动# 注意必须显式传入 robot_description 参数 robot_state_publisher Node( packagerobot_state_publisher, executablerobot_state_publisher, parameters[{ robot_description: ParameterValue( Command([xacro , str(Package(my_robot_description).path / urdf / my_bot.urdf.xacro)]), value_typestr ) }], outputscreen )ParameterValue(Command([...]))是 ROS 2 Humble 的新语法替代了旧版的parameter_valueCommand([...])。如果漏掉ParameterValue包裹robot_state_publisher会启动但不加载 URDF/tf话题为空。3.3.4spawn_entity.py实体注入# spawn_entity 必须在 gz-sim 启动后执行所以加依赖 spawn_entity Node( packagegazebo_ros, executablespawn_entity.py, arguments[ -topic, robot_description, -entity, my_bot, -x, 0.0, -y, 0.0, -z, 0.1, -R, 0.0, -P, 0.0, -Y, 0.0 ], outputscreen, # 关键依赖 gz-sim 进程确保服务已就绪 conditionIfCondition(LaunchConfiguration(start_gz)), )conditionIfCondition(...)是 colcon 的条件启动语法确保spawn_entity只在start_gz:true时运行。-R -P -Y是欧拉角控制初始朝向对导航任务至关重要。实操心得把这四个组件写进一个 launch 文件后用ros2 launch my_robot_bringup gazebo.launch.py start_gz:true启动。如果一切正常你应该看到Gazebo 窗口弹出、rviz2里出现机器人模型、ros2 topic echo /tf有持续输出、ros2 node list显示spawn_entity和robot_state_publisher都在运行。少一个就说明某个组件没连上。4. 常见问题排查从日志里揪出真凶的实战技巧4.1 问题速查表高频故障与定位方法现象可能原因定位命令解决方案gz sim启动后窗口空白或闪退NVIDIA 驱动未加载或版本过低nvidia-smi,glxinfo | grep OpenGL renderer升级驱动至 525重启确认LIBGL_ALWAYS_INDIRECT0未设置ros2 topic list看不到/gz/话题ros_gz_bridge未启动或 world 名称不匹配ros2 node list,gz sim -p my_world.sdf --force-version 6检查gz sim是否加-p确认 world 文件名与桥接命令中的路径一致spawn_entity报错Service not availablegz-sim未启用 ROS 2 插件gz sim -p empty.sdf --force-version 6必须加-p参数且--force-version要匹配rviz2里模型是静态的/tf无数据robot_state_publisher未正确加载 URDFros2 param get /robot_state_publisher robot_description检查ParameterValue语法是否正确xacro命令是否能手动执行成功ros2 topic echo /joint_states无输出gz_ros2_control插件未加载或配置错误gz sim -r -v 4 my_world.sdf, 查找Loaded plugin gz_ros2_control确认my_robot_gazebo的CMakeLists.txt有find_package(gazebo_ros REQUIRED)且package.xml有build_depend4.2 日志深挖-v 4输出里的黄金三行gz sim -v 4的日志长达数百行但只需盯住三行就能快速定位 80% 的问题第一行插件加载行[Msg] Loaded plugin gz::sim::systems::Physics [Msg] Loaded plugin gz::sim::systems::UserCommands [Msg] Loaded plugin gz::sim::systems::SceneBroadcaster [Err] Failed to load plugin gz_ros2_control如果看到[Err] Failed to load plugin说明gz_ros2_control的.so文件路径不在GAZEBO_PLUGIN_PATH里。此时执行$ echo $GAZEBO_PLUGIN_PATH /home/user/ros2_ws/install/my_robot_gazebo/lib如果路径不包含你的my_robot_gazebo安装目录说明colcon build没触发ament_cmake_gazebo_ros宏检查CMakeLists.txt。第二行服务注册行[Msg] Created service /spawn_entity [Msg] Created service /delete_entity [Msg] Created service /get_entity_names如果没看到/spawn_entity说明gz-sim启动时没加-p参数或者gazebo_ros插件版本不匹配。第三行时钟同步行[Msg] Publishing clock data at frequency [1000.000000] [Msg] Publishing physics data at frequency [1000.000000]如果频率是0.000000说明物理引擎没启动通常是 world 文件里physics标签缺失或格式错误。4.3 独家避坑技巧那些文档里不会写的细节技巧一gz sim的-p参数必须紧跟 world 文件错误写法gz sim -r -v 4 --force-version 6 -p my_world.sdf正确写法gz sim -r -v 4 -p my_world.sdf --force-version 6-p必须紧挨着 world 文件名否则gz-sim会把--force-version 6当作 world 名称的一部分导致找不到文件。技巧二spawn_entity.py的-topic参数必须是/robot_description很多人想自定义话题名比如-topic /my_bot/urdf但spawn_entity.py的源码里硬编码了只监听/robot_description。如果你想用多个机器人必须启动多个spawn_entity节点每个节点用-topic指定不同话题但最终它们都会订阅/robot_description所以你需要用remap重映射spawn_bot1 Node( packagegazebo_ros, executablespawn_entity.py, arguments[-topic, robot_description, -entity, bot1], remappings[(/robot_description, /bot1/robot_description)], )技巧三ros_gz_bridge的类型映射必须严格匹配/gz/sim/joint_states的 Gazebo 类型是gz.msgs.Model不是gz.msgs.JointState。我曾花 4 小时调试就因为把gz.msgs.JointState写进了桥接命令。正确的映射表在ros_gz_bridge的源码src/ros_gz_bridge/convert.cc里但更简单的方法是运行gz sim -v 4在日志里搜索Publishing joint states它会打印出实际发布的消息类型。技巧四use_sim_time必须在每个节点里单独声明不要指望ros2 run时加--ros-args -p use_sim_time:true就能全局生效。robot_state_publisher、joint_state_publisher_gui、rviz2都必须各自声明。最稳妥的方式是在 launch 文件里为每个节点添加parameters[{use_sim_time: True}]漏掉任何一个TF 树就会断裂rviz2里模型消失。我在实际操作中发现rviz2的use_sim_time参数必须在rviz2启动时就传入不能在 GUI 里后期勾选。因为 TF 监听器在节点初始化时就建立了时间戳过滤规则后期修改无效。所以rviz2的 launch 配置必须是rviz_node Node( packagerviz2, executablerviz2, arguments[-d, str(Package(my_robot_bringup).path / rviz / my_bot.rviz)], parameters[{use_sim_time: True}], outputscreen )5. 进阶扩展让仿真不止于“能跑”更要“像真机”5.1 传感器仿真从gz_ros2_control到ros_gz_image的完整链路Gazebo Sim 的相机仿真不是简单的“贴图”它涉及渲染管线、传感器噪声模型、ROS 2 消息序列化三重处理。以单目相机为例SDF 中的相机配置sensor namecamera typecamera camera horizontal_fov1.57079632679/horizontal_fov image width640/width height480/height formatR8G8B8/format /image clip near0.1/near far100/far /clip /camera plugin filenamegz_ros2_camera namegz::sim::systems::Camera ros_topic/camera/image_raw/ros_topic ros_frame_idcamera_link/ros_frame_id /plugin /sensor关键点formatR8G8B8/format决定了图像数据格式ros_gz_bridge会据此选择sensor_msgs/msg/Image的encoding字段plugin标签里的filename必须是gz_ros2_camera这是 Fortress 版本的专用插件旧版gazebo_ros_camera不兼容ros_frame_id必须与 URDF 中camera_link的frame_id一致否则 TF 树无法连接。桥接命令ros2 run ros_gz_bridge parameter_bridge \ /gz/sim/worlds/my_world/camera/imagesensor_msgs/msg/Image[gz.msgs.Image \ /gz/sim/worlds/my_world/camera/camera_infosensor_msgs/msg/CameraInfo[gz.msgs.CameraInfo注意/gz/sim/worlds/my_world/camera/image是 Gazebo Sim 的内部话题/camera/image_raw是 ROS 2 的输出话题二者通过parameter_bridge映射。CameraInfo必须同步桥接否则 OpenCV 的cv2.undistort会失效。5.2 控制器集成ros2_control的 YAML 配置陷阱gz_ros2_control插件要求你在 SDF 中声明控制器但真正的控制逻辑在 ROS 2 的 YAML 配置里。一个常见错误是把joint_state_broadcaster和joint_trajectory_controller写在同一个 YAML 文件里导致controller_manager启动失败。正确的controllers.yamlcontroller_manager: ros__parameters: update_rate: 100 # Hz joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster diff_drive_base_controller: type: diff_drive_controller/DiffDriveController diff_drive_base_controller: ros__parameters: left_wheel_names: [wheel_left_joint] right_wheel_names: [wheel_right_joint] wheel_separation: 0.287 wheel_radius: 0.033 publish_rate: 50.0注意controller_manager下的type是插件名diff_drive_controller/DiffDriveController是 ROS 2 Humble 的标准名称diff_drive_base_controller下的ros__parameters是控制器自己的参数publish_rate必须小于update_rate否则会丢帧wheel_separation和wheel_radius必须与 URDF/SDF 中的物理尺寸严格一致否则里程计odom会累积巨大误差。我实测过wheel_radius差 0.001 米在 10 米直线运动后odom误差会达到 0.3 米。所以务必用游标卡尺实测轮子直径而不是相信模型文件里的标注。5.3 性能调优当仿真卡顿先查这三处Gazebo Sim 卡顿不是 CPU 不够而是配置不合理第一处physics标签的max_step_sizephysics namedefault_physics default0 typeode max_step_size0.001/max_step_size real_time_factor1.0/real_time_factor real_time_update_rate1000.0/real_time_update_rate /physicsmax_step_size0.001表示每步物理计算 1msreal_time_update_rate1000表示每秒更新 1000 次。如果max_step_size太小如0.0001计算量暴增太大如0.01物体会穿透障碍物。我的经验是差速轮底盘用0.001无人机用0.0005。第二处rendering的anti_aliasingrendering anti_aliasing2/anti_aliasing /renderinganti_aliasing4会让渲染帧率下降 40%对仿真精度无提升只影响视觉效果。设为2是平衡点。第三处gz sim的-r参数-r表示自动运行但如果物理计算跟不上Gazebo 会自动降频。用-r -v 4启动后观察日志里的Real time factor如果长期低于0.8说明需要调大max_step_size或关闭 anti_aliasing