Scenes

Master scenes: collections of entities with hierarchies, serialization, and asset references. Learn scene loading, multi-scene editing, transitions, and management.

Scenes are collections of entities that represent a level, area, or state in your game. They contain entity hierarchies, component data, prefab instances, and asset references. Scenes can be saved, loaded, and composed together for multi-scene workflows.

What is a Scene?#

A scene is the fundamental organizational unit in Web Engine. It defines the entities, their components, hierarchies, and relationships that make up a playable environment or menu.

// Runtime scene structure
interface SceneData {
id: string; // Unique scene identifier
name: string; // Display name
entities: SceneEntity[];
}
// Scene entity with metadata
interface SceneEntity {
id: string; // UUID
eid?: number; // Runtime ECS entity ID
name: string; // Display name
type: EntityType; // Entity category
object3D: THREE.Object3D;
parentId?: string; // Parent UUID for hierarchy
components: Map<string, unknown>;
prefab?: PrefabInstanceBinding;
hidden?: boolean; // Editor visibility
locked?: boolean; // Editor lock state
}

Hierarchical

Entities organized in parent-child hierarchies with transform inheritance.

Serializable

Save and load scenes with full component data, prefab instances, and asset references.

Composable

Load multiple scenes simultaneously for open-world or additive level streaming.

Versionable

Scene format versioning with automatic migration for backwards compatibility.

Scene Structure#

Scenes organize entities in a hierarchical tree structure. Root entities have no parent, while child entities inherit transforms from their parents.

// Example scene hierarchy
Scene: "Main Level"
├── Environment (root)
│ ├── Terrain
│ ├── Sky
│ └── Lighting
│ ├── Sun (DirectionalLight)
│ └── Ambient
├── Player (root)
│ ├── Camera
│ ├── Weapon
│ └── PlayerModel
│ ├── Head
│ ├── Body
│ └── Legs
└── Props (root)
├── Crate_001 (prefab instance)
├── Crate_002 (prefab instance)
└── Barrel_001 (prefab instance)
// Root entities: Environment, Player, Props
// Child entities: Everything else

This hierarchy enables logical grouping, transform inheritance, and efficient culling/batch processing.

Scene Serialization#

Web Engine serializes scenes to JSON or binary format. The serialized format includes entity data, component values, hierarchies, and prefab instance bindings.

Serialized Scene Format#

Serialized scene structure
typescript
interface ECSSerializedScene {
version: number; // Schema version for migration
metadata: {
name: string;
createdAt: number;
modifiedAt: number;
};
entities: ECSSerializedEntity[];
}
interface ECSSerializedEntity {
uuid: string; // Persistent entity ID
name: string;
parentUuid?: string; // Parent relationship
siblingIndex?: number;
components: Record<string, SerializedComponentData>;
prefab?: PrefabInstanceBinding;
}
// Example serialized entity
{
"uuid": "entity-123",
"name": "Player",
"components": {
"Transform": {
"position": [0, 1, 0],
"rotation": [0, 0, 0],
"scale": [1, 1, 1]
},
"RigidBody": {
"mass": 70.0,
"friction": 0.5,
"type": 0
},
"Health": {
"current": 100,
"max": 100
}
}
}

Saving Scenes#

import { SceneSerializer, SerializationFormat } from '@web-engine/core';
// Serialize scene to JSON
const jsonData = SceneSerializer.serialize(world, {
format: SerializationFormat.JSON,
prettyPrint: true,
});
// Save to file
await fs.writeFile('scene.json', JSON.stringify(jsonData, null, 2));
// Serialize to binary (more compact)
const binaryData = SceneSerializer.serialize(world, {
format: SerializationFormat.BINARY,
compression: true,
});
// Save binary
await fs.writeFile('scene.bin', binaryData);

Prefab Efficiency

Entities that are prefab instances only serialize their overrides, not full component data. This dramatically reduces scene file size for scenes with many prefab instances.

Scene Loading and Unloading#

Loading a scene creates entities in the ECS world and hydrates them with component data. Unloading removes all entities and cleans up resources.

Loading Scenes#

import { SceneLoader } from '@web-engine/core';
// Load scene from JSON
const sceneData = await fetch('/assets/scenes/level1.json').then(r => r.json());
// Hydrate scene into world
const entityMap = SceneLoader.hydrateScene(world, sceneData.entities);
// entityMap: UUID -> entity ID mapping
console.log(`Loaded ${entityMap.size} entities`);
entityMap.forEach((eid, uuid) => {
console.log(`Entity ${uuid} -> ECS ID ${eid}`);
});

Loading with Error Reporting#

// Load with detailed hydration report
const result = SceneLoader.hydrateSceneWithReport(world, sceneData.entities);
console.log(`Successfully loaded: ${result.successCount} entities`);
console.log(`Failed to load: ${result.errorCount} entities`);
// Check for issues
if (result.errors.length > 0) {
console.error('Hydration errors:');
result.errors.forEach((error) => {
console.error(` ${error.entityUuid}: ${error.message}`);
});
}
// Check warnings (non-fatal)
if (result.warnings.length > 0) {
console.warn('Hydration warnings:');
result.warnings.forEach((warning) => {
console.warn(` ${warning.entityUuid}: ${warning.message}`);
});
}
// Unresolved references (e.g., missing prefabs)
if (result.unresolvedReferences.length > 0) {
console.warn('Unresolved asset references:');
result.unresolvedReferences.forEach((ref) => {
console.warn(` Entity ${ref.entityUuid} -> Asset ${ref.assetId}`);
});
}

Unloading Scenes#

// Unload all entities from a scene
function unloadScene(entityMap: Map<string, number>) {
entityMap.forEach((eid, uuid) => {
// Remove entity from world
removeEntity(world, eid);
// Clean up metadata
clearEntityMetadata(eid);
});
console.log(`Unloaded ${entityMap.size} entities`);
}
// Or use SceneManager
SceneManager.unloadScene('level1-id');

Resource Cleanup

Always clean up scene resources (textures, geometries, audio) when unloading. Use AssetRegistry.releaseAsset() to decrement reference counts and free unused assets.

Multi-Scene Editing#

Web Engine supports loading multiple scenes simultaneously. This enables additive scene composition, level streaming, and collaborative editing workflows.

Additive Scene Loading#

// Load main scene
const mainScene = await loadScene('main-level');
// Additively load lighting scene
const lightingScene = await loadScene('lighting-setup', {
additive: true, // Don't clear existing entities
});
// Additively load props scene
const propsScene = await loadScene('level-props', {
additive: true,
});
// All three scenes are now active in the same world
// Total entities = main + lighting + props

Level Streaming#

// Level streaming system
class LevelStreamingSystem {
private loadedChunks = new Map<string, Map<string, number>>();
async streamChunk(chunkId: string, playerPosition: vec3) {
const distance = calculateDistance(playerPosition, chunkId);
if (distance < LOAD_DISTANCE && !this.loadedChunks.has(chunkId)) {
// Load chunk
const chunkData = await fetch(`/chunks/${chunkId}.json`).then(r => r.json());
const entityMap = SceneLoader.hydrateScene(world, chunkData.entities);
this.loadedChunks.set(chunkId, entityMap);
console.log(`Loaded chunk: ${chunkId}`);
}
else if (distance > UNLOAD_DISTANCE && this.loadedChunks.has(chunkId)) {
// Unload chunk
const entityMap = this.loadedChunks.get(chunkId)!;
entityMap.forEach((eid) => {
removeEntity(world, eid);
});
this.loadedChunks.delete(chunkId);
console.log(`Unloaded chunk: ${chunkId}`);
}
}
}
// Usage in system
function LevelStreamingSystem(world: IWorld) {
const streaming = new LevelStreamingSystem();
const playerPos = getPlayerPosition(world);
CHUNK_IDS.forEach((chunkId) => {
streaming.streamChunk(chunkId, playerPos);
});
return world;
}

Scene Transitions#

Implement smooth transitions between scenes with loading screens, fades, and progressive loading.

// Scene transition manager
class SceneTransitionManager {
async transition(fromScene: string, toScene: string) {
// 1. Fade out
await this.fadeOut();
// 2. Show loading screen
this.showLoadingScreen();
// 3. Unload current scene
SceneManager.unloadScene(fromScene);
// 4. Load new scene
const sceneData = await fetch(`/scenes/${toScene}.json`).then(r => r.json());
// 5. Progressive hydration with progress updates
await this.loadSceneProgressively(sceneData, (progress) => {
this.updateLoadingProgress(progress);
});
// 6. Hide loading screen
this.hideLoadingScreen();
// 7. Fade in
await this.fadeIn();
}
async loadSceneProgressively(
sceneData: any,
onProgress: (progress: number) => void
) {
const entities = sceneData.entities;
const batchSize = 50;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
// Load batch
SceneLoader.hydrateScene(world, batch);
// Update progress
const progress = (i + batch.length) / entities.length;
onProgress(progress);
// Yield to prevent blocking
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
// Usage
const transitionManager = new SceneTransitionManager();
await transitionManager.transition('main-menu', 'level-1');

Scene Asset References#

Scenes reference external assets like models, textures, audio, and prefabs. The asset system tracks dependencies and ensures assets are loaded before use.

Asset Dependencies#

// Scene asset manifest
interface SceneAssetManifest {
prefabs: string[]; // Prefab UUIDs
models: string[]; // Model asset IDs
textures: string[]; // Texture asset IDs
materials: string[]; // Material asset IDs
audio: string[]; // Audio asset IDs
}
// Extract dependencies from scene
function extractSceneAssets(sceneData: ECSSerializedScene): SceneAssetManifest {
const manifest: SceneAssetManifest = {
prefabs: [],
models: [],
textures: [],
materials: [],
audio: [],
};
sceneData.entities.forEach((entity) => {
// Collect prefab references
if (entity.prefab) {
manifest.prefabs.push(entity.prefab.prefabId);
}
// Collect component asset references
Object.values(entity.components).forEach((componentData: any) => {
if (componentData.modelAssetId) {
manifest.models.push(componentData.modelAssetId);
}
if (componentData.textureAssetId) {
manifest.textures.push(componentData.textureAssetId);
}
// ... etc
});
});
return manifest;
}

Preloading Assets#

// Preload all scene assets before hydration
async function loadSceneWithAssets(sceneId: string) {
// 1. Load scene JSON
const sceneData = await fetch(`/scenes/${sceneId}.json`).then(r => r.json());
// 2. Extract asset dependencies
const manifest = extractSceneAssets(sceneData);
// 3. Preload all assets in parallel
await Promise.all([
...manifest.prefabs.map(id => AssetRegistry.loadPrefab(id)),
...manifest.models.map(id => AssetRegistry.loadModel(id)),
...manifest.textures.map(id => AssetRegistry.loadTexture(id)),
...manifest.materials.map(id => AssetRegistry.loadMaterial(id)),
...manifest.audio.map(id => AssetRegistry.loadAudio(id)),
]);
// 4. Hydrate scene (all assets are now loaded)
const entityMap = SceneLoader.hydrateScene(world, sceneData.entities);
return entityMap;
}

Asset Reference Counting

The AssetRegistry uses reference counting. When a scene is loaded, asset references increment. When unloaded, they decrement. Assets with zero references are automatically released from memory.

Scene Versioning and Migration#

Web Engine supports scene format versioning. When the engine schema changes, migration functions automatically upgrade old scenes to the current format.

// Scene version history
// v0: Initial format
// v1: Added quaternion to Transform
// v2: Split Mesh into Geometry + Material components
// v3: Added prefab instance bindings
// Migration function
function migrateV2toV3(sceneData: ECSSerializedScene): ECSSerializedScene {
if (sceneData.version !== 2) {
throw new Error('Expected version 2');
}
// Transform entities
sceneData.entities.forEach((entity) => {
// If entity has a prefab marker (old format)
if (entity.components.PrefabMarker) {
const marker = entity.components.PrefabMarker as any;
// Convert to new prefab binding format
entity.prefab = {
prefabId: marker.prefabId,
nodePath: marker.nodePath || '0',
overrides: marker.overrides,
};
// Remove old component
delete entity.components.PrefabMarker;
}
});
// Update version
sceneData.version = 3;
return sceneData;
}
// Register migration
MigrationRegistry.register(2, migrateV2toV3);
// Migrations run automatically on load
const sceneData = await loadScene('old-scene.json');
// Automatically migrated from v2 -> v3

Scene Management API#

The SceneManager provides high-level APIs for scene lifecycle management.

import { SceneManager } from '@web-engine/core';
// Create new empty scene
const sceneId = SceneManager.createScene('New Level');
// Set as active scene
SceneManager.setActiveScene(sceneId);
// Get active scene
const activeScene = SceneManager.getActiveScene();
console.log(`Active scene: ${activeScene.name}`);
// List all loaded scenes
const loadedScenes = SceneManager.getLoadedScenes();
loadedScenes.forEach((scene) => {
console.log(`- ${scene.name} (${scene.entities.length} entities)`);
});
// Save scene
await SceneManager.saveScene(sceneId, '/scenes/my-level.json');
// Load scene (replaces current)
await SceneManager.loadScene('/scenes/main-menu.json');
// Load scene additively
await SceneManager.loadSceneAdditive('/scenes/lighting.json');
// Unload specific scene
SceneManager.unloadScene(sceneId);
// Unload all scenes
SceneManager.unloadAllScenes();

Best Practices#

  • Organize by purpose — Separate lighting, props, and gameplay into different scenes for modularity
  • Use prefabs extensively — Leverage prefabs to reduce scene file size and improve consistency
  • Preload assets — Load all scene assets before hydration to avoid runtime loading hitches
  • Version scenes — Always include version numbers and register migration functions
  • Test migrations — Validate that old scenes load correctly after schema changes
  • Limit entity count — Keep scenes under 10,000 entities for optimal editor and runtime performance
  • Stream large worlds — Use additive scene loading and chunk streaming for open-world games
  • Clean up on unload — Always release assets and clear metadata when unloading scenes

Common Patterns#

Persistent Scene Pattern#

// Keep a persistent scene loaded across level transitions
async function loadLevelWithPersistence(levelId: string) {
// Unload current level (but not persistent scene)
SceneManager.unloadScene(currentLevelId);
// Load new level additively
await SceneManager.loadSceneAdditive(`/scenes/${levelId}.json`);
// Persistent scene (UI, player, managers) stays loaded
console.log('Persistent entities preserved');
}
// Initial setup
await SceneManager.loadScene('/scenes/persistent.json');
await loadLevelWithPersistence('level-1');

Scene Checkpoints#

// Save scene state as checkpoint
function saveCheckpoint() {
const sceneData = SceneSerializer.serialize(world, {
format: SerializationFormat.JSON,
});
// Store in local storage or save file
localStorage.setItem('checkpoint', JSON.stringify(sceneData));
console.log('Checkpoint saved');
}
// Restore from checkpoint
async function loadCheckpoint() {
const checkpointData = localStorage.getItem('checkpoint');
if (checkpointData) {
const sceneData = JSON.parse(checkpointData);
// Clear current scene
SceneManager.unloadAllScenes();
// Load checkpoint
SceneLoader.hydrateScene(world, sceneData.entities);
console.log('Checkpoint restored');
}
}

Scene Validation#

// Validate scene before loading
function validateScene(sceneData: ECSSerializedScene): boolean {
const errors: string[] = [];
// Check version
if (!sceneData.version || sceneData.version > CURRENT_SCENE_VERSION) {
errors.push('Invalid or unsupported scene version');
}
// Check entity count
if (sceneData.entities.length > MAX_ENTITIES) {
errors.push(`Too many entities: ${sceneData.entities.length}`);
}
// Check for circular hierarchies
const visited = new Set<string>();
sceneData.entities.forEach((entity) => {
if (hasCircularHierarchy(entity, sceneData.entities, visited)) {
errors.push(`Circular hierarchy detected: ${entity.uuid}`);
}
});
// Check asset references
sceneData.entities.forEach((entity) => {
if (entity.prefab && !AssetRegistry.hasPrefab(entity.prefab.prefabId)) {
errors.push(`Missing prefab: ${entity.prefab.prefabId}`);
}
});
if (errors.length > 0) {
console.error('Scene validation failed:');
errors.forEach(err => console.error(` - ${err}`));
return false;
}
return true;
}
Scenes | Web Engine Docs