Hierarchy & attachment
How parent-child entity relationships work, how TransformAttachment propagates transforms, and how EntityNode makes building scene-graph hierarchies ergonomic.
Meep implements parent-child entity relationships through two components and one system. The underlying data is kept in flat component arrays for fast traversal; the hierarchy is a separate layer on top.
The two components
ParentEntity records the parent’s entity ID on the child. When the parent is destroyed, ChildEntities (if present) tracks the reverse direction; the actual lifecycle management happens in the systems that consume these components.
TransformAttachment is what actually propagates transforms. It stores a local transform (position, rotation, scale relative to the parent) and the parent entity ID. When either the parent’s Transform or the attachment’s own local transform changes, TransformAttachmentSystem recomputes the child’s world-space Transform as parent ∘ local:
// world transform = parent_transform × attachment.transform
transform.multiplyTransforms(parent_transform, attachment.transform);
TransformAttachmentSystem must be registered for hierarchy to work:
import { TransformAttachmentSystem } from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js";
em.addSystem(new TransformAttachmentSystem());
The system’s dependencies are [TransformAttachment, Transform], so it links any entity that carries both. It subscribes to change signals on the parent’s Transform and on the local attachment.transform, so updates propagate reactively — there is no per-frame scan.
TransformAttachment directly
For simple cases you can create the components by hand:
import { TransformAttachment } from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachment.js";
import { ParentEntity } from "@woosh/meep-engine/src/engine/ecs/parent/ParentEntity.js";
const attachment = new TransformAttachment();
attachment.parent = parentEntityId;
attachment.transform.position.set(0, 1, 0); // 1 unit above the parent
new Entity()
.add(new Transform())
.add(attachment)
.add(ParentEntity.from(parentEntityId))
.build(ecd);
TransformAttachmentFlags.Immediate (the default) causes an initial update the first time the system links the entity, so the child’s world transform is correct from frame one.
EntityNode — the ergonomic wrapper
EntityNode is a scene-graph helper that manages TransformAttachment and ParentEntity for you. It wraps an Entity and exposes a transform property for the local pose. Building a tree of nodes with addChild / build mirrors how you would think about a scene graph:
import { EntityNode } from "@woosh/meep-engine/src/engine/ecs/parent/EntityNode.js";
import { TransformAttachment } from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachment.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
// Wrap the car's visual entity (already has a Transform)
const carNode = new EntityNode(carVisualEntity);
// Attach a headlight as a child node
const headlightNode = EntityNode.fromComponents(new Transform(), headlightComponent);
headlightNode.transform.position.set(0.6, 0.4, 1.5);
carNode.addChild(headlightNode);
// Build the whole tree into the dataset at once
carNode.build(ecd);
EntityNode.fromComponents(...components) is a factory that creates a node and adds the supplied components to its underlying entity.
EntityNode key members:
| Member | Description |
|---|---|
transform | Local Transform — changing it propagates to the world transform reactively |
entity | The underlying Entity builder |
parent | Parent EntityNode, or null for a root |
children | Read-only array of child EntityNode instances |
addChild(node) | Attaches node as a child; builds it immediately if the parent is already built |
removeChild(node) | Detaches a child |
traverse(visitor) | Depth-first visit of this node and all descendants |
root | Walks up to the root of the hierarchy |
build(ecd) | Builds this node and all children into the dataset |
destroy() | Destroys all children first, then this entity |
isBuilt | true after build() |
This pattern is used in the raycast-vehicle example to attach wheels and lights to the car chassis. The physics body is a separate entity; only the visual mesh rides the EntityNode tree.
Bone attachment sockets
For characters with skeletal animations, AttachmentSocket and BoneAttachmentSocket define named attachment points relative to a specific bone. BoneAttachmentSocket extends AttachmentSocket with a boneName string, and the AttachmentSockets component holds a collection of them on the character entity.
These are loaded from data (typically an asset file), and the AttachmentSocketsAssetLoader handles deserialisation. Equipment, held items, and effects are then placed by socket ID at runtime rather than hard-coded offsets.
Where to go next
- Scenes & references —
EntityReferencefor safe cross-entity handles. - Systems & scheduling — how
TransformAttachmentSystemintegrates into the system graph. - Raycast-vehicle example — a live demo using
EntityNodefor visual attachments.