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
Learn about the ECS world: creation, initialization, state management, reset, and lifecycle. Understand how Web Engine's WorldManager ensures safe, predictable world access.
The World is the central container for all ECS data. It holds all entities, components, and queries. Web Engine provides a robust WorldManager that handles initialization, state tracking, and safe concurrent access.
In bitECS, a world is a simple JavaScript object that stores all ECS state. It contains entity bitsets, component arrays, query caches, and metadata. Think of it as a database for your game state.
import { createWorld, addEntity, addComponent } from 'bitecs';import type { IWorld } from 'bitecs';// Create a new ECS worldconst world: IWorld = createWorld();// Add entities and componentsconst eid = addEntity(world);addComponent(world, Transform, eid);// World stores all ECS data internallyconsole.log(world); // Internal bitECS state
World is Opaque
You don't need to understand the world's internal structure. Treat it as an opaque handle that you pass to bitECS functions and systems.
Web Engine wraps the raw bitECS world in a WorldManager that provides:
Tracks world lifecycle: Uninitialized → Initializing → Ready → Disposed
Supports async hooks for loading assets, initializing physics, etc.
Prevents race conditions from concurrent initialization
Ensures only one world exists across the entire app
export enum WorldState {Uninitialized = 'uninitialized', // Not created yetInitializing = 'initializing', // Currently being set upReady = 'ready', // Fully initialized and usableError = 'error', // Initialization failedDisposed = 'disposed', // Destroyed and unusable}
State transitions follow a strict flow:
┌─────────────────┐│ Uninitialized │└────────┬────────┘│▼┌─────────────────┐ ┌─────────────────┐│ Initializing │──────▶│ Error │└────────┬────────┘ └────────┬────────┘│ │▼ │┌─────────────────┐ ││ Ready │ │└────────┬────────┘ ││ │└─────────┬───────────────┘▼┌─────────────────┐│ Disposed │ (terminal state)└─────────────────┘
Use the WorldManager singleton to create and initialize the world:
import { WorldManager } from '@web-engine/core';// Get the singleton managerconst manager = WorldManager.getInstance();// Initialize the world (safe to call multiple times)const result = await manager.ensureInitialized();if (result.success) {console.log('World ready!');console.log(`Initialization took ${result.totalTimeMs}ms`);console.log(`Phases: ${result.phases.length}`);} else {console.error('World initialization failed:', result.error);}
Register initialization hooks to set up systems, load assets, or configure the world during startup:
import { WorldManager } from '@web-engine/core';const manager = WorldManager.getInstance();// Register hook with priority (lower runs first)manager.registerInitHook('load-assets',async (world) => {console.log('Loading assets...');await AssetRegistry.loadDefaultAssets();},10 // Priority);manager.registerInitHook('init-physics',async (world) => {console.log('Initializing physics...');await PhysicsBridge.initialize();},20 // Runs after assets (higher priority));// Initialize world with all hooksawait manager.ensureInitialized();
Hook Priorities
Hooks run in priority order (lower first). Use this to ensure dependencies are initialized in the correct sequence. For example, load assets (priority 10) before initializing systems that depend on them (priority 20).
import { WorldManager } from '@web-engine/core';const manager = WorldManager.getInstance();// Safe access (throws if not ready)try {const world = manager.getWorld();// Use world...} catch (error) {console.error('World not ready:', error);}// Check state firstif (manager.isReady()) {const world = manager.getWorld();// Use world...}// Unsafe access (returns null if not ready)const world = manager.getWorldUnsafe();if (world) {// Use world...}
Listen to world state changes to react to initialization, errors, or disposal:
import { WorldManager, WorldState } from '@web-engine/core';const manager = WorldManager.getInstance();// Register state change listenerconst unsubscribe = manager.onStateChange((newState, prevState, world) => {console.log(`World state: ${prevState} → ${newState}`);if (newState === WorldState.Ready) {console.log('World is ready for use!');startGameLoop(world);}if (newState === WorldState.Error) {console.error('World initialization failed!');const error = manager.getLastError();console.error(error);}});// Later: unsubscribeunsubscribe();
Resetting the world clears all entities and components while keeping the world structure intact. This is useful for level transitions or restarting the game.
import { resetWorld, clearAllMetadata } from '@web-engine/core';// Reset the world (clears all entities and components)resetWorld(world);// Clear entity metadataclearAllMetadata();// World is now empty but still usableconst newEid = addEntity(world);
Reset vs Dispose
Reset clears the world but keeps it usable.Dispose destroys the world permanently. Use reset for level transitions, dispose for app shutdown.
When shutting down the application, dispose of the world to free resources:
import { WorldManager } from '@web-engine/core';const manager = WorldManager.getInstance();// Dispose the world (cannot be reused)manager.dispose();// State is now Disposedconsole.log(manager.getState()); // 'disposed'// Attempting to get world will throwtry {manager.getWorld();} catch (error) {console.error(error); // WorldStateError}
While Web Engine uses a singleton WorldManager by default, bitECS supports multiple independent worlds. This is useful for advanced scenarios like server-side multi-room games or parallel simulations.
import { createWorld, addEntity } from 'bitecs';// Create multiple independent worldsconst gameWorld = createWorld();const uiWorld = createWorld();const physicsPreviewWorld = createWorld();// Each world has its own entities and componentsconst player = addEntity(gameWorld);const button = addEntity(uiWorld);const preview = addEntity(physicsPreviewWorld);// Systems can target specific worldsfunction updateGameWorld(delta: number) {PhysicsSystem(gameWorld, delta, 0);RenderSystem(gameWorld, delta, 0);}function updateUIWorld(delta: number) {UIRenderSystem(uiWorld, delta, 0);}
Singleton vs Multi-World
Web Engine's default architecture assumes a single world. If you need multiple worlds, you'll need to bypass WorldManager and manage worlds manually. This is an advanced pattern — most games don't need it.
Web Engine stores world-level metadata outside the world object itself:
import {entityObjectMap, // Map<eid, THREE.Object3D>getMetadataObject, // Get entity metadataPhysicsRegistry, // Physics body registryAssetRegistry, // Asset loading and caching} from '@web-engine/core';// Access Three.js objects by entity IDconst threeObject = entityObjectMap.get(eid);// Access entity metadataconst metadata = getMetadataObject(eid);// Physics body handlesconst bodyHandle = PhysicsRegistry.getBodyHandle(eid);
WorldManager tracks initialization progress through phases:
import { WorldManager } from '@web-engine/core';const manager = WorldManager.getInstance();// After initialization, inspect phasesconst result = await manager.ensureInitialized();console.log('Initialization Phases:');for (const phase of result.phases) {const duration = phase.endTime! - phase.startTime;console.log(` ${phase.name}: ${duration.toFixed(2)}ms`);if (phase.error) {console.error(` Error: ${phase.error.message}`);}}// Example output:// create-world: 0.12ms// load-assets: 245.67ms// init-physics: 18.34ms// register-systems: 5.21ms
import { WorldManager } from '@web-engine/core';const manager = WorldManager.getInstance();// Initialization with error handlingtry {const result = await manager.ensureInitialized();if (!result.success) {console.error('Initialization failed');console.error('Failed phase:', result.phases.find(p => p.error)?.name);console.error('Error:', result.error);// Retry initialization (allowed after Error state)const retryResult = await manager.ensureInitialized();}} catch (error) {console.error('Unexpected error:', error);}// Get last errorconst lastError = manager.getLastError();if (lastError) {console.error('Last error:', lastError.message);}