本节摘要:gz-sim 把"仿真服务器"与"图形客户端"拆成两个进程,服务器内部是一个 ECS 场景,物理、传感器、用户命令各自作为 System 在每次迭代中读写组件;进程内靠 ECS 直读,进程间靠 gz-transport 消息总线。这个模型解释了无头仿真、云端部署、GUI 掉帧不拖慢物理这三大工程红利。
打开两个终端。第一个跑:
# 只起服务器,不渲染画面,自动运行 gz sim -s -r empty.sdf
第二个跑:
# 只起图形客户端,连接已经在跑的服务器 gz sim -g
你会看到画面出现了,物理在跑。关掉 GUI,服务器照常步进;杀掉服务器,GUI 变成一帧死画面。这个实验揭示的角色分工:服务器是唯一的真值持有者,客户端只是观察窗口。
反过来看 Classic:GUI、物理、插件全在一个进程里,窗口最小化都可能影响物理,云端跑仿真还得配虚拟显示。gz-sim 的拆分不是炫技,是给部署形态松绑——CI 容器里 -s 就够,演示机上 -g 随开随关。
服务器的场景数据结构是 Entity-Component-System:

这个结构的工程含义有三条:
进程内是函数调用级的组件读写,进程间走 gz-transport(基于 ZeroMQ 的发布/订阅)。对使用者来说,最有用的几条总线通道:
| 话题方向 | 内容 | 典型订阅者 |
|---|---|---|
| 服务器 → 外 | /clock、关节状态、传感器数据 |
ros_gz_bridge、日志记录 |
| 外 → 服务器 | 速度指令、关节位置命令、spawn/delete 请求 | 你的控制器、测试脚本 |
| GUI ↔ 服务器 | 视角控制、播放暂停 | 图形客户端 |
# 不写一行代码,直接在总线上观察仿真 gz topic -l # 列出全部话题 gz topic -e -t /clock # 看时钟消息流 gz topic -t /model/vehicle/cmd_vel -m gz.msgs.Twist \ -p 'linear: {x: 0.5}' # 手动发一条速度指令,小车该动起来
gz topic -e 是排障第一工具:命令发了车不动,先看 /cmd_vel 话题上有没有消息——有消息车不动是插件/物理问题,没消息是通信或话题名问题。一句话把故障域切一半。
⚠️ 常见坑:话题名对不上。gz-sim 里传感器话题默认带模型与传感器名的层级前缀(如
/model/my_robot/link/link/sensor/lidar/scan),而教程里常简写。用gz topic -l核对实际名字,别抄例子。
理解 ECS 最快的办法是亲手当一个 System。假设需求是"每步仿真给所有动态模型记录质心高度,超阈值就打日志"。按 ECS 的分工,你要做的只是:声明读 Pose 组件与写自定义组件,在 Configure 里建订阅,在 PreUpdate/PostUpdate 钩子里搬运数据——不需要知道物理系统存在,也不需要碰渲染。
// gz-sim 系统插件骨架:只演示钩子与组件读写的分工 #include <gz/sim/System.hh> #include <gz/sim/components/Pose.hh> class AltitudeMonitor : public gz::sim::System, public gz::sim::ISystemConfigure, public gz::sim::ISystemPostUpdate { public: void Configure(const gz::sim::Entity &, const std::shared_ptr<const sdf::Element> &, gz::sim::EntityComponentManager &, gz::sim::EventManager &) override { gzmsg << "AltitudeMonitor configured\n"; } // 物理步进完成后被回调:此刻 Pose 已由 Physics 系统写好 void PostUpdate(const gz::sim::UpdateInfo &, const gz::sim::EntityComponentManager &ecm) override { ecm.Each<gz::sim::components::Pose, gz::sim::components::Name>( [&](const gz::sim::Entity &, const gz::sim::components::Pose *pose, const gz::sim::components::Name *name) -> bool { double z = pose->Data().Pos().Z(); if (z > warnZ_) gzmsg << name->Data() << " at " << z << " m\n"; return true; // 继续遍历 }); } private: double warnZ_{1.5}; };
骨架里有两个值得盯住的细节:一是 PostUpdate 被调用的时机恰在 Physics 系统写完 Pose 之后——系统间没有互相调用,靠的是调度顺序隐式接力;二是插件通过 EntityComponentManager 遍历组件而不是持有模型指针,模型增删时不会悬空。把这段骨架编成动态库、在 SDF 里用 <plugin filename> 挂到世界,它就与官方系统同权运行。这也是排障视角的收获:看到仿真行为异常时,你可以按"哪个 System 在这一步写了哪个组件"来列表排查,ECS 把"黑盒仿真器"变成了可逐行审计的流水线。
-s 无头、-g 只看,服务器是真值唯一持有者,云端部署因此不需要显示器。gz topic -l + gz topic -e,先确认消息有没有到总线上,再怀疑别的。齿轮分工清楚了,下一个问题是节拍:这些系统以什么顺序、按哪个时钟执行?2.2 讲整个架构里最容易出隐性 bug 的部分——时间。