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.