Skip to content

Scenes & Prefabs

Scenes are snapshots of a collection of entities and their components, essentially a saved world state. Prefabs are reusable entity templates you can instantiate multiple times with different property overrides. Together they form the foundation of level design in Web Engine Dev.

Scenes

Loading a Scene

The @web-engine-dev/scene package provides standalone functions (no class needed):

typescript
import { loadScene, saveScene, unloadScene, getLoadedScenes } from '@web-engine-dev/scene';
import { readJsonFile, atomicWriteJson } from '@web-engine-dev/scene';

// Read scene JSON from disk (browser: fetch; Node/editor: readJsonFile)
const sceneData = await readJsonFile('levels/level-01.scene');

// Load into the world (additive by default - does not clear existing entities)
loadScene(world, sceneData, {
  sceneId: 'level-01',
  path: 'levels/level-01.scene',
  setActive: true, // mark this scene as the active scene
});

// Save the current world state to a scene file (editor tool)
const snapshot = saveScene(world, { sceneId: 'level-01', name: 'Level 01' });
await atomicWriteJson('levels/level-01.scene', snapshot);

// Unload a specific scene
unloadScene(world, 'level-01');

// Unload all scenes
unloadScene(world);

Scene Files

Scene files are JSON by default. They describe the entity list and component data:

json
{
  "version": 1,
  "entities": [
    {
      "id": "player-spawn",
      "components": {
        "Position": { "x": 64, "y": 464 },
        "Player": {},
        "Health": { "current": 100, "max": 100 }
      }
    },
    {
      "id": "platform-1",
      "components": {
        "Position": { "x": 400, "y": 500 },
        "Platform": {},
        "StaticBody": {},
        "Collider": { "shape": "box", "width": 128, "height": 24 }
      }
    }
  ]
}

Scene Transitions

The scene package provides pure load/unload primitives; transition effects (fades, wipes) are implemented at the application layer using your UI/tween system:

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

async function transitionToScene(world: World, nextPath: string): Promise<void> {
  // 1. Persist cross-scene data before unloading
  const playerData = capturePlayerState(world);

  // 2. Unload the current scene
  const activeId = getActiveSceneId(world);
  if (activeId) unloadScene(world, activeId);

  // 3. Load the next scene
  const sceneData = await readJsonFile(nextPath);
  loadScene(world, sceneData, { sceneId: nextPath, setActive: true });

  // 4. Restore cross-scene state
  restorePlayerState(world, playerData);
}

Scene Events

typescript
import { OnSceneLoaded, OnSceneUnloaded } from '@web-engine-dev/scene';

// Listen for scene lifecycle events
function SceneSetupSystem(world: World): void {
  for (const { sceneId, name } of world.eventReader(OnSceneLoaded).read()) {
    console.log('Scene loaded:', name);
    // Initialize game state for this scene
    initializeEnemySpawners(world);
    showLevelTitle(name);
  }
}

Prefabs

Prefabs are entity templates you define once and spawn many times. They support property overrides at spawn time.

Defining a Prefab

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

// Use the builder pattern: .withComponent().withTags().build()
export const gruntPrefab = definePrefab('Grunt')
  .withName('Grunt Enemy')
  .withTags('enemy', 'ground')
  .withComponent(Position, { x: 0, y: 0, z: 0 })
  .withComponent(Velocity, { x: 0, y: 0, z: 0 })
  .withComponent(Health, { current: 50, max: 50 })
  .withComponent(Enemy, {})
  .withComponent(EnemyData, { type: 'grunt', damage: 10, speed: 80 })
  .withComponent(Sprite, { texture: 'grunt.png', width: 32, height: 32 })
  .withComponent(Collider, { shape: 'box', width: 28, height: 30 })
  .build();

// Variants override specific components from a base prefab
const eliteVariant: PrefabVariant = {
  id: createPrefabId('EliteGrunt'),
  name: 'Elite Grunt',
  basePrefabId: gruntPrefab.id,
  componentOverrides: [
    { typeId: Health, overrides: { current: 150, max: 150 } },
    { typeId: EnemyData, overrides: { type: 'elite-grunt', damage: 25 } },
    { typeId: Sprite, overrides: { texture: 'grunt-elite.png' } },
  ],
};

// Register prefabs
const registry = new PrefabRegistry();
registry.register(gruntPrefab);
registry.registerVariant(eliteVariant);

Spawning Prefabs

Use PrefabInstantiator with ECS adapters to spawn prefab entities:

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

const instantiator = new PrefabInstantiator({
  registry,
  entityFactory: { create: () => world.spawn() },
  componentFactory: { insert: (entity, typeId, data) => world.insert(entity, typeId, data) },
});

// Spawn from the registry
const grunt = instantiator.instantiate(gruntPrefab.id, [
  { typeId: Position, overrides: { x: 400, y: 300 } }, // runtime override
]);

// Spawn a variant
const elite = instantiator.instantiate(eliteVariant.id, [
  { typeId: Position, overrides: { x: 600, y: 300 } },
]);

Nested Prefabs (Hierarchy)

Prefabs can include children, forming hierarchical prefabs:

typescript
export const TankPrefab = definePrefab('Tank', {
  components: [
    [Position, { x: 0, y: 0, z: 0 }],
    [Velocity, { x: 0, y: 0, z: 0 }],
    [Health, { current: 500, max: 500 }],
    [TankBody, {}],
    [Sprite, { texture: 'tank-body.png', width: 64, height: 48 }],
    [Collider, { shape: 'box', width: 60, height: 44 }],
  ],
  children: [
    {
      name: 'Turret',
      components: [
        [Position, { x: 0, y: -16, z: 0 }], // offset from parent
        [TankTurret, { rotationSpeed: 90 }],
        [Sprite, { texture: 'tank-turret.png', width: 32, height: 32 }],
        [Cannon, { damage: 100, cooldown: 2 }],
      ],
    },
  ],
});

Level Design Workflow

A typical level design workflow using scenes and prefabs:

1. Design your level in the editor
   - Place tiles/terrain
   - Drag prefabs from the Asset Browser
   - Set spawn points, triggers, paths

2. Save as a .scene file
   - Editor serializes all entities and components

3. Load in-game
   - `loadScene()` deserializes entities
   - Systems initialize (physics, lighting, AI paths)

4. Play & iterate
   - Hot reload: save in editor → scene reloads without restarting

Dynamic Level Loading

For large games, load level chunks on demand:

typescript
import { StreamingManager } from '@web-engine-dev/streaming';

const streaming = world.getResource(StreamingManager);

// Define streaming cells (chunks of the world)
streaming.defineGrid({
  cellSize: 512, // world units per cell
  viewDistance: 2, // load cells within 2 cells of the camera
});

// Streaming manager automatically loads/unloads cells as
// the camera moves: no manual management needed

Saving & Loading Player Progress

Persist entity state between sessions using the Save system:

typescript
import { SaveManager } from '@web-engine-dev/save';

const save = world.getResource(SaveManager);

// Save current game state
await save.save('slot-1', world);

// Load from a save slot (handles migration if schema changed)
const loaded = await save.load('slot-1');
if (loaded) {
  await scenes.load(loaded.currentScene, world);
  restorePlayerState(world, loaded.playerState);
}

See Save & Load for the full persistence guide.

Next Steps

Proprietary software. All rights reserved.