Skip to content

Scenes

The scene system (@web-engine-dev/scene) provides a function-based API for saving and loading ECS world state to and from JSON. Paired with the prefab system (@web-engine-dev/prefab), it enables reusable entity templates, hierarchical scenes, and VCS-friendly serialization.

Scene Data Format

Scenes use a single, canonical JSON format designed for version control diffing:

json
{
  "version": 1,
  "name": "Main Level",
  "entities": [
    {
      "persistentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "Player",
      "components": {
        "transform": { "px": 0, "py": 1, "pz": 0, "rx": 0, "ry": 0, "rz": 0, "rw": 1, "sx": 1, "sy": 1, "sz": 1 },
        "Health": { "current": 100, "max": 100 }
      }
    },
    {
      "persistentId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Sword",
      "parent": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "components": {
        "transform": { "px": 0.5, "py": 0, "pz": 0, "rx": 0, "ry": 0, "rz": 0, "rw": 1, "sx": 1, "sy": 1, "sz": 1 }
      }
    }
  ],
  "editorMetadata": {
    "editorCamera": { "position": [0, 5, 10] },
    "selectedEntities": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
  }
}

Key design decisions:

  • Persistent UUIDs -- Every entity has a persistentId that survives save/load cycles and enables VCS-friendly diffing
  • Flat entity list with parent references -- Children reference parents by persistentId, avoiding deeply nested JSON
  • Component data by type name -- Keyed by the name used in defineComponent(), enabling automatic mapping through the component registry
  • Editor metadata -- Optional section stripped in production builds

Loading Scenes

Load a scene JSON into an ECS world:

typescript
import { loadScene } from '@web-engine-dev/scene';
import { createWorld } from '@web-engine-dev/ecs';

const world = createWorld();
const sceneData = JSON.parse(sceneJson);

const result = loadScene(world, sceneData);
console.log(`Loaded ${result.entityCount} entities`);

// Access the entity map (persistentId -> Entity)
const playerEntity = result.entityMap.get('a1b2c3d4-e5f6-7890-abcd-ef1234567890');

The loader:

  1. Validates the format version
  2. Checks for duplicate persistent IDs and circular parent references
  3. Spawns entities and assigns persistent IDs
  4. Sets up parent-child hierarchy via world.setParent()
  5. Maps component data through the ComponentRegistry for automatic deserialization
  6. Emits an OnSceneLoaded event

Component Registry

The scene system uses a global ComponentRegistry to map serialized component names to ECS component types. Register your components before loading:

typescript
import { ComponentRegistry } from '@web-engine-dev/scene';

ComponentRegistry.global.register('Health', Health, {
  // Optional: custom mapper for complex deserialization
  mapper: (entity, world, data) => {
    world.insert(entity, Health, data as { current: number; max: number });
  },
  // Optional: custom serializer for saving
  serializer: (entity, world) => {
    const h = world.get(entity, Health);
    return h ? { current: h.current, max: h.max } : undefined;
  },
});

If no custom mapper is provided, loadScene uses direct insertion: world.insert(entity, componentType, data).

Saving Scenes

Save the current world state back to the scene format:

typescript
import { saveScene } from '@web-engine-dev/scene';

const sceneData = saveScene(world, {
  name: 'Main Level',
  editorMetadata: {
    editorCamera: { position: [0, 5, 10] },
  },
});

// Serialize to JSON
const json = JSON.stringify(sceneData, null, 2);

The saver:

  1. Queries all entities with the SceneEntity tag
  2. Skips entities with the Volatile tag (runtime-only entities that should not be saved)
  3. Assigns persistent IDs to any entity that doesn't have one
  4. Serializes in hierarchy-traversal order (parents before children) to preserve sibling order
  5. Iterates all registered components on each entity and serializes them
  6. Detects circular references in the hierarchy and throws

Serialization Formats

typescript
import { serializeScene } from '@web-engine-dev/scene';

// Development: pretty-printed, preserves editor metadata
const devJson = serializeScene(sceneData, { format: 'development' });

// Production: compact, strips editor metadata
const prodJson = serializeScene(sceneData, { format: 'production' });

Filtering Entities

Save a subset of entities:

typescript
const sceneData = saveScene(world, {
  name: 'Player Only',
  filter: (entity) => world.has(entity, IsPlayer),
});

Unloading Scenes

Remove all scene entities from the world:

typescript
import { unloadScene } from '@web-engine-dev/scene';

unloadScene(world);
// All entities with the SceneEntity tag are despawned
// Persistent ID index and entity names are cleared
// OnSceneUnloaded event is emitted

Entity Identity

Every scene entity has a persistent UUID that survives save/load cycles. Use these for stable references:

typescript
import {
  assignPersistentId,
  getPersistentId,
  findEntityByPersistentId,
  getEntityName,
  setEntityName,
} from '@web-engine-dev/scene';

// Assign an ID to a new entity
const id = assignPersistentId(world, entity);

// Look up an entity by its persistent ID
const entity = findEntityByPersistentId(world, 'a1b2c3d4-...');

// Entity display names (editor-only, stored in SceneState resource)
setEntityName(world, entity, 'Main Camera');
const name = getEntityName(world, entity); // 'Main Camera'

Scene State Resource

All scene-level state is consolidated in a single ECS resource:

typescript
import { SceneState, getSceneState } from '@web-engine-dev/scene';

const state = world.getResource(SceneState);
// state.activeScene -- { name, path?, dirty }
// state.persistentIds -- bidirectional entity <-> UUID index
// state.entityNames -- display names map
// state.loadedScenes -- multi-scene tracking
// state.activeSceneId -- currently active scene ID

Multi-Scene Workflows

Load multiple scenes additively into the same world:

typescript
import {
  loadSceneAdditive,
  unloadSceneById,
  saveSceneById,
  setActiveSceneId,
  getLoadedScenes,
} from '@web-engine-dev/scene';

// Load scenes additively with unique IDs
loadSceneAdditive(world, levelData, {
  sceneId: 'level-1',
  path: '/scenes/level-1.json',
});

loadSceneAdditive(world, uiData, {
  sceneId: 'ui-overlay',
  path: '/scenes/ui.json',
});

// Set which scene receives new entities
setActiveSceneId(world, 'level-1');

// Save only one scene
const levelSave = saveSceneById(world, 'level-1');

// Unload just one scene (preserves others)
unloadSceneById(world, 'ui-overlay');

// List loaded scenes
const scenes = getLoadedScenes(world);

Entities in multi-scene workflows receive a SceneMembership component that tracks which scene they belong to.

Scene Tags and Components

The scene package provides several built-in ECS components:

ComponentTypeDescription
SceneEntityTagMarks an entity as belonging to a loaded scene
PersistentIdData (u32 high/low)Hash of the persistent UUID
ActiveTagEntity is enabled (default for all loaded entities)
VolatileTagEntity should not be saved (runtime-only)
SceneMembershipData (u32 high/low)Tracks which scene an entity belongs to (multi-scene)

Prefab System

The prefab system (@web-engine-dev/prefab) provides reusable entity templates.

Defining Prefabs

typescript
import { definePrefab, createComponentTypeId } from '@web-engine-dev/prefab';

const Position = createComponentTypeId('Position');
const Health = createComponentTypeId('Health');

const playerPrefab = definePrefab('Player')
  .withName('Player Entity')
  .withDescription('The main player character')
  .withTags('character', 'controllable')
  .withComponent(Position, { x: 0, y: 0, z: 0 })
  .withComponent(Health, { current: 100, max: 100 })
  .build();

Prefab Variants

Variants inherit from a base prefab and modify specific properties:

typescript
const variant = {
  id: createPrefabId('DamagedPlayer'),
  name: 'Damaged Player',
  basePrefabId: playerPrefab.id,
  componentOverrides: [
    { typeId: Health, overrides: { current: 25 } },
  ],
  addedComponents: [
    { typeId: StatusEffects, values: { bleeding: true } },
  ],
  removedComponents: [],
};

When a variant is resolved, the system:

  1. Recursively resolves the base prefab
  2. Removes components listed in removedComponents
  3. Applies componentOverrides (merges values)
  4. Adds addedComponents

Hierarchical Prefabs

Prefabs can contain child prefabs for complex entity hierarchies:

typescript
const vehiclePrefab = definePrefab('Vehicle')
  .withComponent(Position, { x: 0, y: 0, z: 0 })
  .withChild('driver_seat', seatPrefab.id, {
    localTransform: { position: { x: -0.5, y: 0, z: 0 } },
    overrides: [{ typeId: SeatType, overrides: { isDriver: true } }],
  })
  .withChild('passenger_seat', seatPrefab.id, {
    localTransform: { position: { x: 0.5, y: 0, z: 0 } },
  })
  .build();

Instantiation

Create entities from prefabs at runtime through the PrefabInstantiator:

typescript
import { PrefabInstantiator, PrefabRegistry } from '@web-engine-dev/prefab';

const registry = new PrefabRegistry();
registry.register(playerPrefab);

const instantiator = new PrefabInstantiator({
  registry,
  entityFactory: {
    create: () => world.spawn(),
    destroy: (entity) => world.despawn(entity),
  },
  componentFactory: {
    addComponent: (entity, typeId, values) => {
      world.insert(entity, typeId, values);
    },
    setParent: (entity, parent) => {
      world.setParent(entity, parent);
    },
  },
});

// Instantiate with optional runtime overrides
const result = instantiator.instantiate(playerPrefab.id, {
  overrides: [
    { typeId: Health, overrides: { current: 50 } },
  ],
});

Override Tracking

The PrefabOverrideTracker records which properties on prefab instances have been modified from their template values. These overrides are persisted in scene data:

typescript
import {
  PrefabOverrideTracker,
  PrefabOverrideTrackerResource,
} from '@web-engine-dev/prefab';

const tracker = new PrefabOverrideTracker();
world.insertResource(PrefabOverrideTrackerResource, tracker);

// Overrides are automatically serialized into EntityData.prefabOverrides
// when saving scenes

ECS Integration

The prefab package provides ECS components and resources:

  • PrefabInstanceComponent -- Links an entity to its source prefab
  • PrefabChildComponent -- Identifies child entities within a prefab hierarchy
  • PrefabEntity -- Tag marking prefab-spawned entities
  • PrefabRegistryResource -- ECS resource for the prefab registry
  • PrefabInstantiatorResource -- ECS resource for the instantiator

Events: OnPrefabInstantiated, OnPrefabDestroyed, OnPrefabPooled, OnPrefabReactivated

Next Steps

  • ECS -- Understand the component system scenes serialize
  • Rendering -- Learn about Transform3D and rendering components
  • Assets -- Load 3D models to populate scenes

Proprietary software. All rights reserved.