ECS

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.

Parent entity[package]Child entity[package]ParentEntityTransformAttachmentTransform (child)Transform (parent)TransformAttachmentSystem

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:

MemberDescription
transformLocal Transform — changing it propagates to the world transform reactively
entityThe underlying Entity builder
parentParent EntityNode, or null for a root
childrenRead-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
rootWalks 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
isBuilttrue 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