本节摘要:插件是 gz-sim 架构预留的正式扩展点——编译为动态库、在 SDF 里声明加载、在迭代流水线里获得回调。按挂载位置分世界插件、模型插件、传感器插件与系统插件四类。本节给出选型对照表、一个可编译的最小插件全文,以及插件加载失败的排障路径。
想象一个没有任何插件的 Gazebo:物理会算,画面会显示,但机器人是道具——你发 /cmd_vel 没人理(差速驱动插件没挂)、电池永远是满的、传感器数据发了没人订阅转发。Gazebo 的默认交付形态是一个骨架,行为几乎全靠插件填充。 官方常用插件就是一批"标准行为件":
| 插件 | 挂载点 | 干什么 |
|---|---|---|
| DiffDrive | 模型 | 订阅速度指令,驱动两轮差速运动学 |
| JointStatePublisher | 模型 | 发布关节状态到总线 |
| JointController / JointPositionController | 模型 | 关节位置/速度命令 |
| IMU / Contact 传感器系统 | 世界/传感器 | 产出 IMU 与接触消息 |
| ros_gz_bridge 相关 | 桥接 | gz 消息与 ROS 消息互译 |
一个典型的移动机器人 SDF 尾部,往往就是一串插件声明:
<plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive"> <left_joint>left_wheel_joint</left_joint> <right_joint>right_wheel_joint</right_joint> <wheel_separation>0.44</wheel_separation> <wheel_radius>0.12</wheel_radius> <odom_publish_frequency>50</odom_publish_frequency> <topic>/model/robot/cmd_vel</topic> </plugin>
注意这行 SDF 的含义:模型的行为在场景描述里声明,而不是编译进仿真器。同一台机器人,挂 DiffDrive 就是差速车,换成 AckermannSteering 就是阿克曼车——行为件可插拔,正是第 1 章"消息与插件"承诺的落地。
按"你想影响什么"选挂载点:
需求很典型:世界启动后,每 100 步打一行日志,顺便在迭代回调里读一次某个模型的位姿。世界插件的骨架:
#include <gz/sim/World.hh> #include <gz/sim/System.hh> #include <gz/common/Console.hh> #include <gz/plugin/Register.hh> class StepLogger : public gz::sim::System, public gz::sim::ISystemConfigure, public gz::sim::ISystemPostUpdate { public: void Configure(const gz::sim::Entity & /*entity*/, const std::shared_ptr<const sdf::Element> & /*sdf*/, gz::sim::EntityComponentManager & /*ecm*/, gz::sim::EventManager & /*events*/) override { gzmsg << "StepLogger configured." << std::endl; } // PostUpdate: 物理已写完新状态,适合只读观察 void PostUpdate(const gz::sim::UpdateInfo &info, const gz::sim::EntityComponentManager &ecm) override { if (info.iteration % 100 != 0) return; gzmsg << "sim time: " << std::fixed << std::chrono::duration<double>(info.simTime).count() << " s" << std::endl; // 此处可用 ecm 按名字查询模型实体,读取其 Pose 组件(只读观察) } }; GZ_ADD_PLUGIN(StepLogger, gz::sim::System, gz::sim::ISystemConfigure, gz::sim::ISystemPostUpdate)
三个回调时机的差别,决定你的代码放哪:
| 回调 | 时机 | 适合做 |
|---|---|---|
| PreUpdate | 迭代开始、物理求解前 | 施加外力、改写命令 |
| Update | 系统中段 | 少用,除非有明确理由 |
| PostUpdate | 物理写完新状态后 | 只读观测、日志、统计 |
⚠️ 常见坑:在 PostUpdate 里改状态组件,和下一迭代 PreUpdate 的写入打架,出现"速度忽大忽小"的鬼现象。规则很简单——写在 Pre、读在 Post。
加载失败的排查顺序:GZ_SIM_SYSTEM_PLUGIN_PATH 是否包含你的动态库目录;SDF 里 filename 是动态库文件名(gz-sim 新版写法如上,Classic 时代是 libxxx.so);name 是否与 GZ_ADD_PLUGIN 注册的类名一致。服务器日志里搜 "Failed to load plugin" 能拿到第一现场。
💡 关键直觉:插件本质是"你向迭代流水线租了一个回调名额"。写插件前先问:这事用现有插件 + 消息能不能拼出来?官方插件覆盖的行为,组合它们的成本远低于自己写 C++。
第 2 章完成了架构层的拆解。第 3 章进入流水线上最重的系统——物理:惯性、接触、关节这些不可回避的硬账,如何记、如何核对。