Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
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 structureinterface SceneData {id: string; // Unique scene identifiername: string; // Display nameentities: SceneEntity[];}// Scene entity with metadatainterface SceneEntity {id: string; // UUIDeid?: number; // Runtime ECS entity IDname: string; // Display nametype: EntityType; // Entity categoryobject3D: THREE.Object3D;parentId?: string; // Parent UUID for hierarchycomponents: Map<string, unknown>;prefab?: PrefabInstanceBinding;hidden?: boolean; // Editor visibilitylocked?: boolean; // Editor lock state}
Entities organized in parent-child hierarchies with transform inheritance.
Save and load scenes with full component data, prefab instances, and asset references.
Load multiple scenes simultaneously for open-world or additive level streaming.
Scene format versioning with automatic migration for backwards compatibility.
Scenes organize entities in a hierarchical tree structure. Root entities have no parent, while child entities inherit transforms from their parents.
// Example scene hierarchyScene: "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.
Web Engine serializes scenes to JSON or binary format. The serialized format includes entity data, component values, hierarchies, and prefab instance bindings.
interface ECSSerializedScene {version: number; // Schema version for migrationmetadata: {name: string;createdAt: number;modifiedAt: number;};entities: ECSSerializedEntity[];}interface ECSSerializedEntity {uuid: string; // Persistent entity IDname: string;parentUuid?: string; // Parent relationshipsiblingIndex?: 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}}}
import { SceneSerializer, SerializationFormat } from '@web-engine/core';// Serialize scene to JSONconst jsonData = SceneSerializer.serialize(world, {format: SerializationFormat.JSON,prettyPrint: true,});// Save to fileawait 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 binaryawait 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.
Loading a scene creates entities in the ECS world and hydrates them with component data. Unloading removes all entities and cleans up resources.
import { SceneLoader } from '@web-engine/core';// Load scene from JSONconst sceneData = await fetch('/assets/scenes/level1.json').then(r => r.json());// Hydrate scene into worldconst entityMap = SceneLoader.hydrateScene(world, sceneData.entities);// entityMap: UUID -> entity ID mappingconsole.log(`Loaded ${entityMap.size} entities`);entityMap.forEach((eid, uuid) => {console.log(`Entity ${uuid} -> ECS ID ${eid}`);});
// Load with detailed hydration reportconst result = SceneLoader.hydrateSceneWithReport(world, sceneData.entities);console.log(`Successfully loaded: ${result.successCount} entities`);console.log(`Failed to load: ${result.errorCount} entities`);// Check for issuesif (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}`);});}
// Unload all entities from a scenefunction unloadScene(entityMap: Map<string, number>) {entityMap.forEach((eid, uuid) => {// Remove entity from worldremoveEntity(world, eid);// Clean up metadataclearEntityMetadata(eid);});console.log(`Unloaded ${entityMap.size} entities`);}// Or use SceneManagerSceneManager.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.
Web Engine supports loading multiple scenes simultaneously. This enables additive scene composition, level streaming, and collaborative editing workflows.
// Load main sceneconst mainScene = await loadScene('main-level');// Additively load lighting sceneconst lightingScene = await loadScene('lighting-setup', {additive: true, // Don't clear existing entities});// Additively load props sceneconst propsScene = await loadScene('level-props', {additive: true,});// All three scenes are now active in the same world// Total entities = main + lighting + props
// Level streaming systemclass 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 chunkconst 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 chunkconst entityMap = this.loadedChunks.get(chunkId)!;entityMap.forEach((eid) => {removeEntity(world, eid);});this.loadedChunks.delete(chunkId);console.log(`Unloaded chunk: ${chunkId}`);}}}// Usage in systemfunction LevelStreamingSystem(world: IWorld) {const streaming = new LevelStreamingSystem();const playerPos = getPlayerPosition(world);CHUNK_IDS.forEach((chunkId) => {streaming.streamChunk(chunkId, playerPos);});return world;}
Implement smooth transitions between scenes with loading screens, fades, and progressive loading.
// Scene transition managerclass SceneTransitionManager {async transition(fromScene: string, toScene: string) {// 1. Fade outawait this.fadeOut();// 2. Show loading screenthis.showLoadingScreen();// 3. Unload current sceneSceneManager.unloadScene(fromScene);// 4. Load new sceneconst sceneData = await fetch(`/scenes/${toScene}.json`).then(r => r.json());// 5. Progressive hydration with progress updatesawait this.loadSceneProgressively(sceneData, (progress) => {this.updateLoadingProgress(progress);});// 6. Hide loading screenthis.hideLoadingScreen();// 7. Fade inawait 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 batchSceneLoader.hydrateScene(world, batch);// Update progressconst progress = (i + batch.length) / entities.length;onProgress(progress);// Yield to prevent blockingawait new Promise(resolve => setTimeout(resolve, 0));}}}// Usageconst transitionManager = new SceneTransitionManager();await transitionManager.transition('main-menu', 'level-1');
Scenes reference external assets like models, textures, audio, and prefabs. The asset system tracks dependencies and ensures assets are loaded before use.
// Scene asset manifestinterface SceneAssetManifest {prefabs: string[]; // Prefab UUIDsmodels: string[]; // Model asset IDstextures: string[]; // Texture asset IDsmaterials: string[]; // Material asset IDsaudio: string[]; // Audio asset IDs}// Extract dependencies from scenefunction extractSceneAssets(sceneData: ECSSerializedScene): SceneAssetManifest {const manifest: SceneAssetManifest = {prefabs: [],models: [],textures: [],materials: [],audio: [],};sceneData.entities.forEach((entity) => {// Collect prefab referencesif (entity.prefab) {manifest.prefabs.push(entity.prefab.prefabId);}// Collect component asset referencesObject.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;}
// Preload all scene assets before hydrationasync function loadSceneWithAssets(sceneId: string) {// 1. Load scene JSONconst sceneData = await fetch(`/scenes/${sceneId}.json`).then(r => r.json());// 2. Extract asset dependenciesconst manifest = extractSceneAssets(sceneData);// 3. Preload all assets in parallelawait 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.
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 functionfunction migrateV2toV3(sceneData: ECSSerializedScene): ECSSerializedScene {if (sceneData.version !== 2) {throw new Error('Expected version 2');}// Transform entitiessceneData.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 formatentity.prefab = {prefabId: marker.prefabId,nodePath: marker.nodePath || '0',overrides: marker.overrides,};// Remove old componentdelete entity.components.PrefabMarker;}});// Update versionsceneData.version = 3;return sceneData;}// Register migrationMigrationRegistry.register(2, migrateV2toV3);// Migrations run automatically on loadconst sceneData = await loadScene('old-scene.json');// Automatically migrated from v2 -> v3
The SceneManager provides high-level APIs for scene lifecycle management.
import { SceneManager } from '@web-engine/core';// Create new empty sceneconst sceneId = SceneManager.createScene('New Level');// Set as active sceneSceneManager.setActiveScene(sceneId);// Get active sceneconst activeScene = SceneManager.getActiveScene();console.log(`Active scene: ${activeScene.name}`);// List all loaded scenesconst loadedScenes = SceneManager.getLoadedScenes();loadedScenes.forEach((scene) => {console.log(`- ${scene.name} (${scene.entities.length} entities)`);});// Save sceneawait SceneManager.saveScene(sceneId, '/scenes/my-level.json');// Load scene (replaces current)await SceneManager.loadScene('/scenes/main-menu.json');// Load scene additivelyawait SceneManager.loadSceneAdditive('/scenes/lighting.json');// Unload specific sceneSceneManager.unloadScene(sceneId);// Unload all scenesSceneManager.unloadAllScenes();
// Keep a persistent scene loaded across level transitionsasync function loadLevelWithPersistence(levelId: string) {// Unload current level (but not persistent scene)SceneManager.unloadScene(currentLevelId);// Load new level additivelyawait SceneManager.loadSceneAdditive(`/scenes/${levelId}.json`);// Persistent scene (UI, player, managers) stays loadedconsole.log('Persistent entities preserved');}// Initial setupawait SceneManager.loadScene('/scenes/persistent.json');await loadLevelWithPersistence('level-1');
// Save scene state as checkpointfunction saveCheckpoint() {const sceneData = SceneSerializer.serialize(world, {format: SerializationFormat.JSON,});// Store in local storage or save filelocalStorage.setItem('checkpoint', JSON.stringify(sceneData));console.log('Checkpoint saved');}// Restore from checkpointasync function loadCheckpoint() {const checkpointData = localStorage.getItem('checkpoint');if (checkpointData) {const sceneData = JSON.parse(checkpointData);// Clear current sceneSceneManager.unloadAllScenes();// Load checkpointSceneLoader.hydrateScene(world, sceneData.entities);console.log('Checkpoint restored');}}
// Validate scene before loadingfunction validateScene(sceneData: ECSSerializedScene): boolean {const errors: string[] = [];// Check versionif (!sceneData.version || sceneData.version > CURRENT_SCENE_VERSION) {errors.push('Invalid or unsupported scene version');}// Check entity countif (sceneData.entities.length > MAX_ENTITIES) {errors.push(`Too many entities: ${sceneData.entities.length}`);}// Check for circular hierarchiesconst visited = new Set<string>();sceneData.entities.forEach((entity) => {if (hasCircularHierarchy(entity, sceneData.entities, visited)) {errors.push(`Circular hierarchy detected: ${entity.uuid}`);}});// Check asset referencessceneData.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;}