World Management

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.

What is a World?#

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 world
const world: IWorld = createWorld();
// Add entities and components
const eid = addEntity(world);
addComponent(world, Transform, eid);
// World stores all ECS data internally
console.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.

WorldManager: Safe Lifecycle Management#

Web Engine wraps the raw bitECS world in a WorldManager that provides:

State Machine

Tracks world lifecycle: Uninitialized → Initializing → Ready → Disposed

Async Initialization

Supports async hooks for loading assets, initializing physics, etc.

Mutex Protection

Prevents race conditions from concurrent initialization

Singleton Pattern

Ensures only one world exists across the entire app

World Lifecycle States#

packages/core/src/engine/ecs/WorldManager.ts
typescript
export enum WorldState {
Uninitialized = 'uninitialized', // Not created yet
Initializing = 'initializing', // Currently being set up
Ready = 'ready', // Fully initialized and usable
Error = 'error', // Initialization failed
Disposed = 'disposed', // Destroyed and unusable
}

State transitions follow a strict flow:

┌─────────────────┐
│ Uninitialized │
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ Initializing │──────▶│ Error │
└────────┬────────┘ └────────┬────────┘
│ │
▼ │
┌─────────────────┐ │
│ Ready │ │
└────────┬────────┘ │
│ │
└─────────┬───────────────┘
┌─────────────────┐
│ Disposed │ (terminal state)
└─────────────────┘

World Creation#

Use the WorldManager singleton to create and initialize the world:

import { WorldManager } from '@web-engine/core';
// Get the singleton manager
const 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);
}

Initialization Hooks#

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 hooks
await 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).

Accessing the World#

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 first
if (manager.isReady()) {
const world = manager.getWorld();
// Use world...
}
// Unsafe access (returns null if not ready)
const world = manager.getWorldUnsafe();
if (world) {
// Use world...
}

State Change Listeners#

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 listener
const 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: unsubscribe
unsubscribe();

World Reset#

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 metadata
clearAllMetadata();
// World is now empty but still usable
const 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.

World Disposal#

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 Disposed
console.log(manager.getState()); // 'disposed'
// Attempting to get world will throw
try {
manager.getWorld();
} catch (error) {
console.error(error); // WorldStateError
}

Multiple Worlds (Advanced)#

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 worlds
const gameWorld = createWorld();
const uiWorld = createWorld();
const physicsPreviewWorld = createWorld();
// Each world has its own entities and components
const player = addEntity(gameWorld);
const button = addEntity(uiWorld);
const preview = addEntity(physicsPreviewWorld);
// Systems can target specific worlds
function 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.

World-Level Metadata#

Web Engine stores world-level metadata outside the world object itself:

import {
entityObjectMap, // Map<eid, THREE.Object3D>
getMetadataObject, // Get entity metadata
PhysicsRegistry, // Physics body registry
AssetRegistry, // Asset loading and caching
} from '@web-engine/core';
// Access Three.js objects by entity ID
const threeObject = entityObjectMap.get(eid);
// Access entity metadata
const metadata = getMetadataObject(eid);
// Physics body handles
const bodyHandle = PhysicsRegistry.getBodyHandle(eid);

Initialization Phases#

WorldManager tracks initialization progress through phases:

import { WorldManager } from '@web-engine/core';
const manager = WorldManager.getInstance();
// After initialization, inspect phases
const 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

Error Handling#

import { WorldManager } from '@web-engine/core';
const manager = WorldManager.getInstance();
// Initialization with error handling
try {
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 error
const lastError = manager.getLastError();
if (lastError) {
console.error('Last error:', lastError.message);
}

Best Practices#

  • Use WorldManager — Don't create worlds manually unless you need multiple worlds
  • Initialize once — Call ensureInitialized() at app startup, not in loops
  • Check state before access — Use isReady() or try/catch when getting the world
  • Register hooks early — Register all init hooks before calling ensureInitialized()
  • Use priorities — Order hooks with priorities to ensure correct initialization sequence
  • Listen to state changes — React to world state transitions for better UX
  • Reset, don't recreate — Use resetWorld() for level transitions, not dispose/create
  • Dispose on shutdown — Always dispose the world when the app closes
World Management | Web Engine Docs | Web Engine Docs