☰
gz-sim 自定义组件实战:用 C++ 为 Gazebo 仿真扩展你自己的 ECS Component
2026/10/13 13:59:31 网站建设 项目流程
  • 机器人

【免费下载链接】gz-sim

Open source robotics simulator. The latest version of Gazebo.

项目地址:https://gitcode.com/gh_mirrors/gz/gz-sim
点击查看免费下载

本文以 gz-sim 仓库自带的custom_component示例工程为主线,完整讲解如何在一个 System 插件中定义、注册并使用一个自定义 Component(组件):从components::Component模板别名与唯一 tag 类型入手,深入到GZ_SIM_REGISTER_COMPONENT宏背后的注册机制,再补齐 CMake 构建、GZ_SIM_SYSTEM_PLUGIN_PATH加载路径配置与世界文件(SDF)加载流程,使读者能够复制一套可独立编译运行的自定义组件插件工程。

1. 为什么需要自定义组件:gz-sim 的 ECS 架构

Gazebo(即当前仓库 gz-sim)采用实体-组件-系统(Entity Component System,ECS)软件架构:仿真世界由大量实体(Entity)组成,每个实体通过挂载一组组件(Component)来表达自身的属性,而系统插件(System plugin)通过EntityComponentManager(ECM)查询和修改这些组件来驱动仿真。这一术语体系的定义可参见 terminology 教程,组件的系统级用法可进一步参考 Using Components in a System Plugin 教程 和 Creating System Plugins 教程。

框架内置了Name、Pose、JointForceCmd等大量标准组件(见 components 头文件目录),但当你的插件需要携带私有状态——例如某个自定义控制器的内部计数、自定义标志位或用户数据——就需要定义自己的组件类型。examples/plugin/custom_component示例正是围绕这一目标展开的,工程包含以下 5 个文件:

文件作用
README.md构建与运行步骤说明
CustomComponentPlugin.hh组件类型定义、注册宏、System 插件类
CustomComponentPlugin.cc通过GZ_ADD_PLUGIN宏导出插件
CMakeLists.txt构建CustomComponent共享库
custom_component.sdf加载该插件的世界文件

2. 定义自定义组件:模板别名 + 唯一 tag

CustomComponentPlugin.hh 中最核心的两行代码如下:

namespace examples { // 最便捷的组件创建方式:为 components::Component 声明一个模板别名, // 第一个模板参数是被包装的数据类型(这里是 int), // 第二个参数是一个唯一的名称 tag。 using CustomComponent = gz::sim::components::Component<int, class CustomComponentTag>; // 使用注册宏注册组件,名称必须在整个仿真中保持唯一 GZ_SIM_REGISTER_COMPONENT("examples::CustomComponent", CustomComponent)

结合 Component 模板定义,可以拆解出三个模板参数的含义:

template <typename DataType, typename Identifier, typename Serializer = serializers::DefaultSerializer<DataType>> class Component : public BaseComponent
  • DataType:组件实际包装的数据类型。示例中为int,但可以是任意类型,也可以是一个std::shared_ptr<T>。
  • Identifier:唯一标识 tag 类型。注释和源码都强调——它必须是一个唯一类型,但并不要求这个类型在别处有定义,class CustomComponentTag就地声明即可。需要特别注意的是:如果两个人写出Component<bool, class StaticTag>与Component<bool, class StaticTag>这两个别名,由于数据类型和 tag 都相同,它们在 ECM 看来就是完全相同的类型,会被视为同一个组件。因此在命名 tag 时务必带上自己的项目/命名空间特征,避免撞车。
  • Serializer:可选,默认为DefaultSerializer<DataType>。

2.1 无数据组件(tag 组件)与序列化

Component还有一个针对无数据组件的偏特化(见 Component<NoData, ...> 特化):

using NoData = std::add_lvalue_reference<void>; using Joint = Component<NoData, class JointTag>;

这类组件不携带任何数据,其“存在”本身就足以对实体做出某种判断,同类型的两个实例恒等。

关于序列化:默认序列化器DefaultSerializer会利用 SFINAE 特性检查DataType是否提供了operator<</operator>>(见 traits::IsOutStreamable / IsInStreamable)。如果数据类型的流运算符可用,日志系统(<log>插件)就能把组件写入/读回;不可用时只打印一条警告,组件不会被序列化。示例中的int天然满足流运算符要求,因此该组件是“可序列化”的。若需要自定义序列化逻辑,可以传入第三个模板参数:

class MySerializer; // 别处定义 using CustomComponent = gz::sim::components::Component<int, class CustomComponentTag, MySerializer>;

2.2GZ_SIM_REGISTER_COMPONENT宏到底做了什么

Factory.hh 中的宏定义展开后包含三件事:

  1. 定义一个非成员函数gzSimFactoryComponentTypeId(_classname* ptr),其返回值是组件名字符串的 64 位哈希(gz::common::hash64(_compType))。Component模板通过 ADL(参数依赖查找)调用该函数得到静态成员typeId(见 Component 的 typeId 静态成员注释)。哈希值跨编译器、跨进程稳定,因此注册名"examples::CustomComponent"必须全局唯一——这就是宏注释中“在整个仿真中保持唯一”的原因。
  2. 生成一个静态初始化对象GzSimComponentsInitializer##_classname:其构造函数调用Factory::Instance()->Register<_classname>(...),把一个ComponentDescriptor(负责实际make_unique组件实例、并在 entt 注册表中开 storage)登记进全局 组件工厂 Factory。
  3. 其析构函数调用Factory::Unregister,在插件被卸载时移除描述符,避免其他插件引用已释放内存导致段错误(设计动机见 ComponentDescriptorQueue 注释)。

另外两个与排查相关的细节:

  • 如果不同插件用同一个注册名登记了不同类型,Factory::Register会向 stderr 打印警告Registered components of different types with same name: ...(源码);
  • 设置环境变量GZ_DEBUG_COMPONENT_FACTORY=true可以在静态初始化阶段打印每一条组件注册日志,便于确认插件库确实被加载、组件确实被注册(源码)。

3. 在 System 插件中创建与查询组件

定义组件之后,还要有一个 System 插件来使用它。示例让CustomComponentPlugin同时继承gz::sim::System和gz::sim::ISystemConfigure(见 CustomComponentPlugin.hh):

class CustomComponentPlugin : public gz::sim::System, public gz::sim::ISystemConfigure { public: CustomComponentPlugin() = default; public: void Configure(const gz::sim::Entity &_entity, const std::shared_ptr<const sdf::Element> &, gz::sim::EntityComponentManager &_ecm, gz::sim::EventManager &) override { // 创建自定义组件,与创建任何内置组件的方式完全相同 gzdbg << "Creating component" << std::endl; _ecm.CreateComponent(_entity, CustomComponent(123)); // 使用 ECM 的 Each API 查询该组件 _ecm.Each<CustomComponent>( & -> bool { gzdbg << "Entity: " << _entityEach << std::endl; gzdbg << "Component's data: " << _comp->Data() << std::endl; return true; }); } };

要点说明:

  • ISystemConfigure::Configure的调用时机:根据 System.hh 中 ISystemConfigure 的文档,Configure在系统实例化之后、仿真开始之前被调用,此时对应 SDF 父元素(本例中是<world>)下的所有实体和组件均已加载完成。因此在这里向“插件所挂载的实体”创建组件是安全的;若要访问父元素之外的实体,可能需要延迟到*Update()阶段。
  • _ecm.CreateComponent(_entity, CustomComponent(123)):ECM 的 CreateComponent 会拷贝传入的组件数据并挂到指定实体上,返回创建出来的组件指针(实体不存在时返回nullptr)。
  • _ecm.Each<CustomComponent>(lambda):对“拥有CustomComponent的每个实体”回调一次,lambda 返回true表示继续遍历。_comp->Data()即拿到被包装的int值。
  • ECM 还提供更细粒度的接口可用于生产代码:Component<Entity>按实体取组件、ComponentDefault(不存在则按默认值创建)、SetComponentData(不存在则创建、存在则更新并返回数据是否变化)、RemoveComponent 等,见 EntityComponentManager 头文件。

4. 导出插件:GZ_ADD_PLUGIN与 CMake 构建

4.1 插件导出

组件类型是在头文件中定义的,而“把 System 插件类注册进 gz-plugin 插件体系”则放在 CustomComponentPlugin.cc:

#include "CustomComponentPlugin.hh" #include <gz/plugin/Register.hh> GZ_ADD_PLUGIN(examples::CustomComponentPlugin, gz::sim::System, examples::CustomComponentPlugin::ISystemConfigure)

GZ_ADD_PLUGIN宏(来自gz-plugin库)声明该库导出的插件类及其实现的接口列表(System和ISystemConfigure)。加载侧的 SystemLoader 会检查插件是否实现了System接口,若不满足会打印包含“Resolved library path”和“Detected Plugins”的完整诊断信息。

4.2 CMake 工程

CMakeLists.txt 展示了一个最小但完整的 gz-sim 插件工程:

cmake_minimum_required(VERSION 3.22.1 FATAL_ERROR) find_package(gz-cmake REQUIRED) project(CustomComponentPlugin) find_package(gz-plugin REQUIRED COMPONENTS register) find_package(gz-sim REQUIRED) add_library(CustomComponentPlugin SHARED CustomComponentPlugin.cc ) set_property(TARGET CustomComponentPlugin PROPERTY CXX_STANDARD 17) target_link_libraries(CustomComponentPlugin PRIVATE gz-plugin::gz-plugin PRIVATE gz-sim::gz-sim)

参数说明:

  • find_package(gz-cmake REQUIRED):引入 Gazebo 家族的 CMake 工具链;
  • find_package(gz-plugin REQUIRED COMPONENTS register):register组件提供GZ_ADD_PLUGIN宏;
  • find_package(gz-sim REQUIRED):找到当前仓库所编译出的 gz-sim 开发库(头文件、config.hh、gz-sim::gz-sim导入目标);
  • add_library(... SHARED ...):插件必须是动态库才能被运行时加载;
  • CXX_STANDARD 17:示例明确要求 C++17。

5. 编译与运行

5.1 构建

从 gz-sim 仓库根目录执行(示例为独立 CMake 工程,与主工程分开构建):

cd examples/plugin/custom_component mkdir build cd build cmake .. make

构建完成后build目录下会生成CustomComponent库(示例 README 中的表述;实际库目标名为CustomComponentPlugin,以构建产物为准)。

5.2 配置插件搜索路径

把生成目录加入插件搜索路径:

cd examples/plugin/custom_component export GZ_SIM_SYSTEM_PLUGIN_PATH=`pwd`/build

GZ_SIM_SYSTEM_PLUGIN_PATH是 SystemLoader 默认读取的插件路径环境变量,也出现在 gz sim 命令的帮助文本 中(冒号分隔的多个路径)。该变量在 Migration.md 中记录的旧名是IGN_GAZEBO_SYSTEM_PLUGIN_PATH,升级旧脚本时要注意替换。

5.3 加载世界

准备一个引用该插件的世界文件 custom_component.sdf:

<?xml version="1.0" ?> <sdf version="1.6"> <world name="default"> <plugin filename="CustomComponentPlugin" name="examples::CustomComponentPlugin"> </plugin> </world> </sdf>

filename指定要加载的库名,name指定库内由GZ_ADD_PLUGIN导出的插件类名。然后仅以 server 模式启动并调高日志级别(-s只启动仿真服务端,-v 4输出 debug 级日志):

gz sim -s -v 4 custom_component.sdf

由于插件挂载在<world>上且实现了ISystemConfigure,启动时Configure会执行,在 debug 日志中可以看到示例打印的内容:Creating component、Entity: <world 实体 ID>以及Component's data: 123,证明组件已成功创建并被Each<CustomComponent>查询到。

6. 小结与扩展建议

本示例展示了 gz-sim 自定义组件的最小完整闭环:模板别名定义组件 →GZ_SIM_REGISTER_COMPONENT注册 →GZ_ADD_PLUGIN导出插件 →GZ_SIM_SYSTEM_PLUGIN_PATH加载 →Configure中CreateComponent/Each使用。在此基础上继续深入时,可以参考仓库内的既有实践:

  • 内置系统如何消费标准组件:src/systems 目录下的各插件实现;
  • 自定义组件的运行时行为验证:test/integration 中的集成测试模式;
  • 组件创建与仿真的配合:SdfEntityCreator(官方建议系统插件使用它创建具有正确父子关系的实体);
  • 组件工厂层面的单元测试:ComponentFactory_TEST.cc 与 Component_TEST.cc,可帮助理解注册、New()、反序列化等 API 的边界行为。

需要注意的是,GZ_SIM_REGISTER_COMPONENT的注册名必须全局唯一、tag 类型必须可区分;插件卸载时的自动反注册机制保证了多插件共存场景下的安全,这些机制的实现细节都可以从 Factory.hh 中直接查证。

  • 机器人

【免费下载链接】gz-sim

Open source robotics simulator. The latest version of Gazebo.

项目地址:https://gitcode.com/gh_mirrors/gz/gz-sim
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询