Prefabs, Bundles & Commands
Reference documentation for prefabs, bundles, commands, and entity mutation.
Prefabs
Section titled “Prefabs”Prefabs are immutable templates for spawning entities with predefined component data and optional child hierarchies.
Creating a prefab
Section titled “Creating a prefab”Use Define.prefab() to create a PrefabBuilder. Every builder method returns a new instance — builders are immutable.
import { Define } from '@shell/ecs';
const PlayerPrefab = Define.prefab() .with(Position, { x: 0, y: 0 }) .with(Velocity, { x: 0, y: 0 }) .with(Texture, { source: 'player.png' });- Components without explicit data use the component’s schema defaults.
Builder methods
Section titled “Builder methods”| Method | Returns | Description |
|---|---|---|
.with(Component, data?) | new prefab | Add a component with optional initial data. Partial data merges with defaults. |
.with(CompA, CompB, CompC) | new prefab | Add multiple components without data (schema defaults). |
.withComponents(otherPrefab) | new prefab | Merge root components from another prefab. Children are not included. |
.withChild(name, childPrefab) | new prefab | Add a named child slot. Replaces existing slot with the same name. |
.withChild(undefined, childPrefab) | new prefab | Add an anonymous child (parented but not in slot map). |
Overriding data at spawn
Section titled “Overriding data at spawn”.with() returns a new prefab — the original is unchanged:
// Override specific fields (others keep prefab defaults)const { root } = commands.spawn(PlayerPrefab.with(Position, { x: 100 }));Composing root components
Section titled “Composing root components”.withComponents() merges only root components from another prefab. Children are not included:
const EnemyPrefab = Define.prefab() .withComponents(PlayerPrefab) // merge root components only .with(Texture, { source: 'enemy.png' }) // override Texture .with(Health, { hp: 50 }); // add new component.with() does NOT accept a PrefabBuilder — use .withComponents() for prefab composition.
Multiple components without data
Section titled “Multiple components without data”const TagPrefab = Define.prefab().with(ComponentA, ComponentB, ComponentC);Children (hierarchical prefabs)
Section titled “Children (hierarchical prefabs)”Prefabs can declare named or anonymous child entities:
const SwordPrefab = Define.prefab() .with(Position, { x: 10 }) .with(Texture, { source: 'sword.png' });const ShieldPrefab = Define.prefab() .with(Position, { x: -10 }) .with(Texture, { source: 'shield.png' });
const ArmedPlayerPrefab = Define.prefab() .with(Position, { x: 0, y: 0 }) .withChild('sword', SwordPrefab) // named slot .withChild('shield', ShieldPrefab) // named slot .withChild(undefined, LabelPrefab); // anonymous — parented but no slotNamed slots bubble up through nested hierarchies. A grandchild slot 'gem' on a child 'sword' is accessible directly on the top-level SpawnedPrefab:
const Gem = Define.prefab().with(Texture, { source: 'gem.png' });const Sword = Define.prefab().with(Position).withChild('gem', Gem);const Player = Define.prefab().with(Position).withChild('sword', Sword);
const spawned = commands.spawn(Player);spawned.get('sword'); // Entityspawned.get('gem'); // Entity — bubbled up from grandchildKey behavior
Section titled “Key behavior”- Builders are immutable —
.with()and.withChild()always return a newPrefabBuilder. - Overrides are partial — only specified fields are replaced, others keep defaults.
.withChild('name', newPrefab)replaces an existing slot with the same name.- Slot names are flat — named slots from nested children bubble up to the top-level
SpawnedPrefab.
Bundles
Section titled “Bundles”Bundles are immutable reusable collections of component builders plus optional initial component data. They are root-component-only templates: no children, no slots.
Creating a bundle
Section titled “Creating a bundle”Use Define.bundle() to create a BundleBuilder. Like prefabs, every builder method returns a new instance.
import { Define } from '@shell/ecs';
const PhysicsBundle = Define.bundle().with(Position, { x: 0, y: 0 }).with(Velocity, { x: 0, y: 0 });
const RenderBundle = Define.bundle().with(Sprite).with(Texture);Composing bundles
Section titled “Composing bundles”Bundles can be nested inside other bundles using .with():
const ActorBundle = Define.bundle() .with(PhysicsBundle) .with(RenderBundle) .with(Health, { hp: 100 });Mixing builders without data
Section titled “Mixing builders without data”When every argument to .with() is a builder (bundle or component), no data object is needed:
const TagBundle = Define.bundle().with(ComponentA, ComponentB, ComponentC);Bundles inside prefabs
Section titled “Bundles inside prefabs”.with() on a prefab accepts a BundleBuilder. This is the preferred way to compose reusable component sets into prefabs:
const EnemyPrefab = Define.prefab() .with(ActorBundle) .with(Texture, { source: 'enemy.png' }) .with(Health, { hp: 50 });Important: .with() on a prefab does not accept another PrefabBuilder. Use .withComponents() for prefab-to-prefab root composition.
Adding bundles at runtime
Section titled “Adding bundles at runtime”Bundles can be added to existing entities via commands or direct entity methods:
// Via commandscommands.addComponent(entity, ActorBundle);
// Via direct entity methodentity.addComponent(ActorBundle);This applies all components (and nested bundles) from the bundle to the entity in a single operation.
Key behavior
Section titled “Key behavior”- Builders are immutable —
.with()always returns a newBundleBuilder. - Bundles are root-only — they cannot declare children or slots.
- Overrides are partial — only specified fields are replaced when using
.with(Component, data). - Components without explicit data use the component’s schema defaults.
Commands
Section titled “Commands”Commands execute entity operations. Available via the commands parameter in system update functions, or via the testWorld fixture.
commands.spawn() always returns a SpawnedPrefab, even for empty spawns. Use .root to get the root entity.
// Empty entityconst { root } = commands.spawn();
// From a prefabconst { root } = commands.spawn(PlayerPrefab);
// With explicit UID (for serialization/networking)const { root } = commands.spawn(PlayerPrefab, 'player-1');
// Hierarchical prefab — access named childrenconst spawned = commands.spawn(ArmedPlayerPrefab);spawned.root; // Entity — the playerspawned.get('sword'); // Entity — the sword child (type-safe)spawned.get('shield'); // Entity — the shield child (type-safe)spawned.getAll(); // Map<string, Entity> — all named slotsSpawnedPrefab interface
Section titled “SpawnedPrefab interface”interface SpawnedPrefab<TSlots extends string = never> { root: Entity; // always present get(slot: TSlots): Entity; // type-safe named child lookup getAll(): Map<string, Entity>; // all named slots}For childless prefabs, TSlots is never, making .get() unreachable at the type level.
Despawn
Section titled “Despawn”// By entity objectcommands.despawn(entity);
// By UID stringcommands.despawn('player-1');Add / remove components
Section titled “Add / remove components”commands.addComponent(entity, Position, { x: 10, y: 20 });commands.addComponent(entity, ActorBundle); // add all components from a bundlecommands.removeComponent(entity, Position);Commands that receive an exact Entity object route to that entity’s owning registry anywhere in
the same root/scene world family. This allows a scene system to mutate root or sibling entities
returned by shared queries. spawn() and UID-only operations remain local to the calling world’s
registry, and entities from an independent World are rejected. Every entity in one
updateComponents() call must share the same registry.
Lifecycle event commands, hierarchy links, and stored entity references remain registry-local so scene teardown cannot leak into another owner.
Direct entity methods
Section titled “Direct entity methods”Entities also expose component operations directly. These delegate to commands under the hood.
Adding components
Section titled “Adding components”entity.addComponent(Position, { x: 10, y: 20 });entity.addComponent(ActorBundle); // add all components from a bundleentity.addComponents(Position, Velocity, Sprite);Removing components
Section titled “Removing components”entity.removeComponent(Position);Checking existence
Section titled “Checking existence”entity.hasComponent(Position); // booleanGetting a component proxy
Section titled “Getting a component proxy”const position = entity.getComponent(Position);position?.x; // numberposition?.y; // numberHierarchy operations
Section titled “Hierarchy operations”entity.setParent(parentEntity);parentEntity.addChild(childEntity);entity.getChildren(); // Entity[]entity.getParent(); // Entity | undefinedentity.getChildCount(); // numberEntity properties
Section titled “Entity properties”| Property | Type | Description |
|---|---|---|
entity.id | EntityId | Monotonic runtime ID within the world family |
entity.index | number | Compact registry index, reusable after despawn |
entity.generation | number | Lifecycle generation for the reusable index |
entity.row | number | Row in the current archetype’s storage |
entity.uid | string | Lazy persistent identifier for external formats |
entity.archetype | Archetype | Current archetype |
entity.markedForRemoval | boolean | Pending despawn |
entity.destroyed | boolean | Already removed |
entity.children | Entity[] | Child entities (hierarchy) |
An Entity object represents exactly one lifecycle. Its ID, UID (once materialized), registry ID,
index, and generation never change, including after despawn. Reading entity.uid for the first time
generates and registers it; entities.getByUid() can discover only explicitly supplied or already
materialized UIDs. A first UID read after destruction remains stable but is not registered.
IDs are unique and never reused within a root world and its descendant scenes. Independent root worlds may reuse the same numeric IDs, so use UIDs for serialization, protocols, and networking. A later entity can reuse the numeric index with a higher generation, but it is a different JavaScript object. Structural mutations through the old object and writes through one of its retained component proxies throw instead of targeting the replacement.
Important timing note
Section titled “Important timing note”In tests, entity spawns and component changes are committed when the scheduler ticks. Always call scheduler.tick() after mutations to see their effect in queries.
const spawned = commands.spawn(PlayerPrefab);scheduler.tick(); // commit spawn
expect(spawned.root.hasComponent(Position)).toBe(true);Summary
Section titled “Summary”- Prefabs are immutable templates created with
Define.prefab(). - Bundles are immutable root-only component collections created with
Define.bundle(). .with()and.withChild()return new builders; the original is unchanged..withComponents()merges root components only; children are not copied.- Named child slots bubble up through nested hierarchies and are type-safe.
commands.spawn()always returns aSpawnedPrefabwith.rootand.get().- Entity methods (
addComponent,removeComponent,hasComponent,getComponent) delegate to commands. - Call
scheduler.tick()in tests after spawning to commit changes.