Joël van der Werf
All posts

Where runtime state lives: four rules for components in an ECS

Settings, per-entity state, library-owned state and derived data each have one home in Hydro. Deciding that once removed a whole class of bugs.

Hydro uses EnTT, and its components are plain data: public fields with defaults, a few pure helpers, no virtual functions and no update logic. Systems are functions that run over a view of components.

That's the easy part. The hard question came with the particle system: where does runtime state go? A sprite animator's current frame, a Box2D body, a particle buffer on the GPU. Put it in the wrong place and the editor saves it into scene files, Play mode starts from stale values, or two places disagree about the truth.

So I wrote down one rule set and applied it everywhere.

The rules

  1. Saved settings live in the component.
  2. Small runtime state can live in the component too, if it is fixed-size, per entity and needs nothing outside the component. It is grouped in one State member, never saved, reset when the game starts, and written only by its system. Example: the sprite animator's current frame and timer.
  3. Everything else lives in a per-scene object. Variable-size data, data owned by a library or the GPU, or data that must outlive the entity goes into a ...Scene object owned by the scene: PhysicsScene for Box2D bodies, PhysicsScene3D for Jolt bodies and characters. Then all of that component's runtime state goes there, so there is one source of truth.
  4. Scripts and editor tools go through a facade (Physics2D, Physics3D, CharacterBody, SpriteAnimator), never through runtime fields.

Derived data is its own thing

Later, the scene hierarchy and runtime UI brought a fifth kind of data: values computed from authored components, like an entity's world transform. It isn't runtime state and it isn't a setting.

It gets its own component, added and removed together with its source through EnTT's construct and destroy signals: WorldTransformComponent next to TransformComponent, UIResolvedRectComponent next to UIRectComponent. The pass that computes them only writes values and never changes the registry's structure, which keeps iteration safe. Derived components are never saved, copied on Play or shown in the inspector.

Why it was worth writing down

  • Play mode just works. The play copy of a scene copies components; rule-2 state is reset on start anyway, and rule-3 state is rebuilt by its scene object. No "reset" code scattered around.
  • Scene files stay clean. Only settings are saved, so diffs of scene files mean something.
  • New systems need no debate. Particles, physics and UI each slotted into the rules without a design meeting with myself.
  • It is checkable. Agents and the project validator can tell a setting from state because the shape of the code says which is which.

The general lesson I take back to Unity work as well: decide where state lives once, name the categories, and make the code's shape show which category a field is in.

Older: 56 issues: what a full engine review foundNewer: Pixel-art sprites in a 3D world: upright cards, cut-outs and depth

Building a game, an engine, or the tools around them?

I'm based in Tokyo and open to conversations about Unity, C++ and engine roles, C# and .NET software roles, and teams that want to put AI agents to work on real codebases.