gz-ecs-overview
Concise reference for the gz-sim Entity-Component-System architecture — how Entities, Components, Systems, the ECM, the Server, and SimulationRunner fit together. Trigger when the user asks how gz-sim is organized, where to add code, or how the simulation loop runs.
git clone --depth 1 https://github.com/harunkurtdev/ros2-claude-code-template /tmp/gz-ecs-overview && cp -r /tmp/gz-ecs-overview/.claude/skills/gz-ecs-overview ~/.claude/skills/gz-ecs-overviewSKILL.md
# gz-sim ECS architecture in 5 minutes
```
┌──────────────────────────────────────────┐
│ Server │
│ (one process, owns N runners) │
└──────┬───────────────────────────────────┘
│
┌──────────▼──────────────────────────┐
│ SimulationRunner │
│ - drives the iteration loop │
│ - owns the EventManager │
│ - owns the EntityComponentManager │
│ - owns the System list │
└──────────┬──────────────────────────┘
│ every step
┌───────────────────┼───────────────────────────────────────┐
▼ ▼ ▼ ▼
Configure* PreUpdate* Update* PostUpdate*
(once on load) (mutates ECM) (mutates ECM) (read-only)
└── Reset hooks on /reset
```
## The core types
| Type | Header | Role |
|------|--------|------|
| `Entity` | `Entity.hh` | Just a `uint64_t` ID. Means nothing on its own. |
| `components::Component<T, id>` | `components/Component.hh` | Strongly-typed data attached to an Entity. |
| `EntityComponentManager` | `EntityComponentManager.hh` | The world's database. Add/remove/query components. |
| `EventManager` | `EventManager.hh` | Pub/sub for cross-system events. |
| `System` (`ISystemConfigure`, `ISystemPreUpdate`, `ISystemUpdate`, `ISystemPostUpdate`, `ISystemReset`) | `System.hh` | Behaviour. Plugins implement one or more of these. |
| `SimulationRunner` | (internal) | Owns and drives the above. |
| `Server` | `Server.hh` | Public façade — `Run()`, `SetUpdatePeriod()`, etc. |
## Adding behaviour: pick a system phase
* **`Configure()`** — once on plugin load. Read SDF, cache handles, register
topics. Do *not* mutate the world here unless you're spawning fixtures.
* **`PreUpdate()`** — every iteration, *before* physics. Apply commands
(forces, joint targets), spawn entities.
* **`Update()`** — every iteration, alongside physics. Most user code does
*not* belong here.
* **`PostUpdate()`** — every iteration, *after* physics. **Read-only ECM**
access — emit telemetry, publish state, write logs. The ECM is `const`
here for a reason.
* **`Reset()`** — when the world resets (e.g. via `/world/<name>/control`).
Restore any internal state cached in the system.
## The "Cmd / state / Reset" component triplet
A common pattern in gz-sim:
* `<X>` — current value (e.g. `JointPosition`).
* `<X>Cmd` — request from a user system, consumed and cleared in
`PreUpdate` by the physics-coupled system.
* `<X>Reset` — value to apply at the next world reset.
Look at `components/JointPositionReset.hh` / `JointVelocityCmd.hh` for the
canonical examples.
## Querying the ECM efficiently
Prefer `Each<>` views — they're cached and re-used across iterations:
```cpp
_ecm.Each<components::Joint, components::JointVelocity>(
[&](const Entity &_ent,
const components::Joint *,
const components::JointVelocity *_vel) -> bool
{
// do work with *_vel
return true; // keep iterating
});
```
`EachNew`, `EachRemoved`, `EachChanged` exist for change-detection passes.
## Where things live
* **Component definitions** — `include/gz/sim/components/<Name>.hh`
(header-only). Register serialization in `src/ComponentFactory.cc` when
you want the component to survive log replay / save.
* **System plugins** — `src/systems/<snake_case>/<CamelCase>.{hh,cc}` plus
an entry in `src/systems/CMakeLists.txt`. Use the `new-system` skill to
scaffold.
* **High-level wrappers** — `Model`, `Link`, `Joint`, `Light`, `Actor`,
`Sensor` (`include/gz/sim/`). These wrap an `Entity` plus ECM accessors
and are usually how user code touches a model.
## Threading model
* `PreUpdate` / `Update` / `PostUpdate` of *different* systems run
sequentially inside one runner iteration — not in parallel.
* `Server::Run(true, n)` blocks; `Run(false, n)` runs the loop on a worker
thread. Multi-runner setups exist (different worlds), each on its own
thread.
* Don't add long-running work in `Update()`. Spawn a thread in `Configure`,
communicate via lock-free queues, drain in `PreUpdate`.
## Read the source, not just this
If you only have time for two files, read:
1. `include/gz/sim/EntityComponentManager.hh` — every comment matters.
2. `include/gz/sim/System.hh` — the interfaces are tiny and tell you
exactly what each phase guarantees.Use proactively before opening a PR that adds or changes BehaviorTree.CPP nodes or BehaviorTree.ROS2 wrappers (RosActionNode/RosServiceNode/RosTopicPub/SubNode, TreeExecutionServer). Reviews a diff against BT.CPP v4 conventions — node base-class choice, non-blocking ticks, ports/blackboard typing, factory/plugin registration, XML v4, and the ROS 2 wrapper contract. Returns a punch list with file:line anchors, not a rewrite.
Use when a design decision touches Clean Architecture boundaries in a ROS 2 project — which layer a new behaviour belongs to, whether a port belongs in domain or application, whether a new node should be lifecycle-managed, whether to compose nodes or split packages. Returns an architectural recommendation with trade-offs, not implementation.
Use when a design decision touches the gz-sim ECS — where new state should live, which system phase should write it, how to avoid coupling, whether to add a component vs. a member variable, whether a new system should be split or merged with an existing one. Returns an architectural recommendation with trade-offs, not implementation.
Use proactively before opening any gz-sim PR. Reviews a diff against the project's C++17 style, ECS conventions, plugin registration patterns, CMake structure, test placement, Migration.md / Changelog.md expectations, and pre-commit configuration. Returns a punch list, not a rewrite.
Use proactively before opening a PR that adds or changes a ros2_control controller, broadcaster, or hardware component (incl. URDF <ros2_control> bringup). Reviews a diff against ros2_controllers / ros2_control_demos conventions — controller & hardware lifecycle, command/state interface configuration, real-time safety of update()/read()/write(), generate_parameter_library usage, pluginlib registration, chainable-controller correctness, URDF wiring, and tests. Returns a punch list with file:line anchors, not a rewrite.
Use proactively before opening any ROS 2 / Nav 2 PR. Reviews a diff against this template's Clean Architecture, ROS 2 communication, lifecycle, testing, and Nav 2 plugin conventions. Returns a punch list with file:line anchors, not a rewrite.
Use proactively before opening a PR that touches a VDA 5050 connector / fleet bridge. Reviews a diff against VDA 5050 v3.0.0 protocol compliance (topics, QoS, header rules, base/horizon, action state machine, schema validation) and the template's Clean Architecture for the MQTT↔Nav 2 bridge. Returns a punch list with file:line anchors, not a rewrite.
Build the colcon workspace (optionally a single package) and report the outcome.