m06 · Transforms · reading · 10 min
The hierarchy — when entity data stops being independent
The property you are about to give up
Five modules have rested on one quiet assumption, and this lesson is where it breaks.
Module 1 said entities in a
A hierarchy destroys that property, deliberately and locally, and it is worth being precise about how.
Attach a turret to a tank. The turret’s LocalTransform now means relative to
the tank. To know where the turret is in the world you must first know where
the tank is in the world. The turret’s world position is no longer a function
of the turret’s data alone — it is a function of the turret’s data and its
parent’s result.
That is a dependency. And a set of dependencies over entities is a graph.
What the graph forces
Once you accept that framing, the entire transform system’s behaviour follows without needing to be told.
You cannot compute a child before its parent. The child’s world transform literally requires the parent’s world transform as input. So the work has an ordering constraint, and the constraint is the graph’s edges.
The ordering is by depth, not by chunk. All roots can be computed first (they depend on nothing). Then everything at depth 1, which now has its inputs. Then depth 2. This is a breadth-first sweep down the tree, and it’s forced — no clever chunk layout removes it, because the dependency is between entities, not between chunks.
Within a depth level, independence returns. Two entities at depth 1 with different parents don’t depend on each other at all. So each level parallelizes freely; it’s only between levels that you need a barrier. The shape is: pass over the roots, sync, pass over depth 1, sync, pass over depth 2.
That last point is the useful one, and notice it is the same shape as Module 4’s dependency chains: independent work parallelizes, dependent work serializes, and the cost of a dependency is a sync point. A transform hierarchy is a dependency chain expressed in data instead of in job handles. A tree of depth 3 costs you three sequential passes. A tree of depth 12 costs twelve, and each one is a barrier where cores idle waiting for the level above.
Depth is the cost. Not entity count — depth. Ten thousand entities parented flat to one root is three passes’ worth of structure and parallelizes beautifully. Two hundred entities in a chain twelve deep is twelve barriers to move two hundred things. This is the single most actionable fact in the module.
Why parent and child are both stored
The graph must be represented in components, and here the design does something that looks redundant until you price the alternative.
The child carries a Parent component holding its parent’s Entity. That’s
the edge, stored once, pointing up. It’s the minimal representation and it is
what you author.
But the transform system needs to walk down — roots first, then their
children. With only upward links, finding a parent’s children means scanning
every entity in the world and testing whether its Parent matches. Per parent.
That’s quadratic, per frame, and it’s unacceptable.
So the system also maintains a Child buffer on the parent: a dynamic buffer
of the entities beneath it. Now the downward walk is a traversal — start at a
root, read its children, recurse — proportional to the number of entities in
the tree rather than to the square of the world.
Two representations of one relationship, which means they can disagree, which means something must keep them in sync. That something is a system, and it is the reason parenting is not instantaneous.
The density cost nobody mentions
There is a second cost, and it’s the one Module 1 taught you to look for.
An entity with a parent carries: LocalTransform (32 bytes), LocalToWorld
(64 bytes, the composed matrix — next lesson), Parent (8 bytes), and if it
has children, a Child buffer. A root entity with no children carries
LocalTransform and LocalToWorld and nothing else.
More importantly, Parent is a component, so it changes the archetype.
Parented and unparented entities of otherwise identical composition live in
different chunks. Parenting an entity at runtime is a
So a hierarchy costs you three separate things, and they’re worth naming separately because they’re paid at different times: archetype fragmentation (paid in chunk density, permanently), structural change on reparent (paid per reparent), and a sync barrier per depth level (paid every frame, forever).
When to use one anyway
Given that bill, the discipline follows. A hierarchy is correct when the relationship is genuinely rigid and genuinely needed: a turret that must stay bolted to a tank, a hand attached to an arm, a UI element pinned to a panel. There, the alternative is worse — you’d be recomputing the relationship manually in a system, which is the same work without the engine’s parallelism.
It’s wrong when it’s used for organization. Grouping a thousand props under an empty “Props” root because it tidies the hierarchy adds a depth level and a parent component to a thousand entities in exchange for nothing — they don’t move with it, and there’s no spatial relationship to maintain. That’s the authoring model’s habit leaking into the runtime model, and Module 5 established that those models are optimized against each other.
What this buys you
You can now explain why a parent link converts independent entity data into a
dependency graph, why that forces a top-down depth-ordered sweep with a barrier
per level, why the relationship is stored twice and what that costs in
timeliness, and why depth rather than entity count is the number to watch. Next:
LocalToWorld — the composed result, why it’s stored rather than recomputed,
and who is allowed to write it.