2.1 进程模型与组件通信


2.1 进程模型与组件通信

本节摘要: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 随开随关。

服务器内部:ECS 三件套

服务器的场景数据结构是 Entity-Component-System:

  • 实体(Entity):一个编号,代表世界里的"某个东西"——一个模型、一个链接、一盏灯,甚至世界本身。
  • 组件(Component):贴在实体上的数据片段。一个方块实体上贴着 Name、Pose、LinearVelocity、Collision 等组件;组件是数据,不含逻辑。
  • 系统(System):每步迭代被调度的逻辑,按需读写组件。Physics 系统读 Pose 与外力、写出新 Pose;Sensor 系统读 Pose 与传感器配置,产出数据消息。

02-02-fig01

这个结构的工程含义有三条:

  • 数据与逻辑分离,系统可以按需加载(不用传感器就不加载渲染,这就是无头模式省资源的原因);
  • 系统间通过组件隐式协作,Physics 写的 Pose 自动成为 Sensors 的输入,无需手写胶水;
  • 进程外只认消息,你的控制器、可视化、日志记录全部平权地挂在总线上。

进程间通信:gz-transport 一览

进程内是函数调用级的组件读写,进程间走 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 核对实际名字,别抄例子。

把自己写进服务器:一个最小 System 插件的骨架

理解 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 把"黑盒仿真器"变成了可逐行审计的流水线。

本节要点回顾

  • 服务器/GUI 分进程-s 无头、-g 只看,服务器是真值唯一持有者,云端部署因此不需要显示器。
  • ECS 三件套:实体是编号、组件是数据、系统是逻辑;数据与逻辑分离带来按需加载与自由组合。
  • 进程内走组件、进程间走消息:你的节点从不在服务器进程内,全部通过 gz-transport 平权交互。
  • 排障第一刀gz topic -l + gz topic -e,先确认消息有没有到总线上,再怀疑别的。

齿轮分工清楚了,下一个问题是节拍:这些系统以什么顺序、按哪个时钟执行?2.2 讲整个架构里最容易出隐性 bug 的部分——时间。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U