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):
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:
{
"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:
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
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
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:
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:
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 restartingDynamic Level Loading
For large games, load level chunks on demand:
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 neededSaving & Loading Player Progress
Persist entity state between sessions using the Save system:
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
- Camera Systems, follow cameras and camera transitions for your scenes
- 2D Development, tilemaps and tile-based level design
- Save & Load, persisting player progress