- 机器人
【免费下载链接】gz-sim
Open source robotics simulator. The latest version of Gazebo.
本文以 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 BaseComponentDataType:组件实际包装的数据类型。示例中为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 中的宏定义展开后包含三件事:
- 定义一个非成员函数
gzSimFactoryComponentTypeId(_classname* ptr),其返回值是组件名字符串的 64 位哈希(gz::common::hash64(_compType))。Component模板通过 ADL(参数依赖查找)调用该函数得到静态成员typeId(见 Component 的 typeId 静态成员注释)。哈希值跨编译器、跨进程稳定,因此注册名"examples::CustomComponent"必须全局唯一——这就是宏注释中“在整个仿真中保持唯一”的原因。 - 生成一个静态初始化对象
GzSimComponentsInitializer##_classname:其构造函数调用Factory::Instance()->Register<_classname>(...),把一个ComponentDescriptor(负责实际make_unique组件实例、并在 entt 注册表中开 storage)登记进全局 组件工厂 Factory。 - 其析构函数调用
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`/buildGZ_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.
相关推荐
vue-class-component 自定义装饰器实战:用 createDecorator 扩展你的 Vue 类组件
vue class component 自定义装饰器实战:用 createDecorator 扩展你的 Vue 类组件 自定义装饰器是 vue class co
前端Alamofire自定义扩展:创建自己的网络组件
Alamofire自定义扩展:创建自己的网络组件 痛点:为什么需要自定义网络组件? 在iOS/macOS开发中,网络请求是每个应用的核心功能。Alamofire
网络通信后端ToolJet 自定义组件(Custom Component)实战:用 React 代码扩展应用能力
ToolJet 自定义组件(Custom Component)实战:用 React 代码扩展应用能力 导读 当 ToolJet 内置组件无法满足特定业务需求时,
低代码后端前端AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考