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
Deep dive into Web Engine's internal architecture. Understand the registry-first design, system phases, and data flow.
Web Engine follows a Registry-First architecture where all components, systems, and asset types are registered at runtime. This enables extensibility, runtime modification, and a clean separation between the engine core and game-specific logic.
Registries are the central source of truth for metadata:
Defines all ECS components with their schemas and editor metadata.
Registers all systems with their phase, dependencies, and enable conditions.
Maps file extensions to asset loader functions.
// Register a custom componentComponentRegistry.register("MyComponent", {name: "MyComponent",description: "Does something cool",category: "gameplay",props: {speed: { type: "float", default: 5.0, min: 0, max: 100 },target: { type: "entity", default: null },enabled: { type: "boolean", default: true },},});// Later: query the registryconst schema = ComponentRegistry.get("MyComponent");const allComponents = ComponentRegistry.getAll();
Systems run in ordered phases each frame. Each phase has a numeric value, and systems within a phase can specify a priority for fine-grained ordering:
export enum SystemPhase {Input = 0, // Read input devicesNetwork = 100, // Network syncLogic = 200, // Game logic, AI, scriptsPhysics = 300, // Physics simulationAnimation = 400, // Skeletal animationsAudio = 500, // Audio updatesRender = 600, // Three.js renderingPostRender = 700 // Post-processing, cleanup}
Systems are sorted first by phase, then by priority within that phase. Lower priority values run first:
SystemRegistry.register({name: "InputSystem",phase: SystemPhase.Input,priority: 0, // Runs first in Input phaseexecute: (world, delta, time) => { /* ... */ }});SystemRegistry.register({name: "ScriptSystem",phase: SystemPhase.Logic,priority: 10, // Runs after priority 0 systemsafter: ["InputSystem"], // Explicit ordering dependencyexecute: (world, delta, time) => { /* ... */ }});
System Dependencies
Use after and before to declare explicit ordering dependencies. The registry validates the dependency graph at initialization and throws an error if dependencies are circular or missing.
scene.json → SceneLoader → Create Entities → Add Components → Run onStart
Input → Physics → Scripts → Animation → Render
UI Event → Command → ECS Mutation → Sync to Three.js → Render
Web Engine is organized as a monorepo with clear separation between packages:
ECS runtime, systems, components, registries, asset loading, and physics integration.
Branding configuration, storage keys, and white-label settings.
Performance monitoring, frame profiling, and logging utilities.
Shared Vite/Rollup chunking strategy for consistent builds.
web-engine/├── apps/│ ├── studio/ # Visual editor (Next.js)│ │ ├── src/│ │ │ ├── app/ # Next.js pages│ │ │ ├── components/ # React UI components│ │ │ ├── managers/ # Editor-specific managers│ │ │ ├── services/ # Context menus, diagnostics│ │ │ └── store/ # Zustand state management│ │ └── ...│ ││ ├── player/ # Runtime player (Vite)│ │ └── src/│ ││ ├── server/ # Multiplayer server (Colyseus)│ │ └── src/│ ││ └── docs/ # Documentation site (Next.js)│ └── src/│├── packages/│ ├── core/ # Engine runtime│ │ ├── src/│ │ │ ├── engine/ # Core engine systems│ │ │ │ ├── ecs/ # bitECS integration│ │ │ │ ├── physics/ # Rapier3D integration│ │ │ │ ├── assets/ # Asset loading pipeline│ │ │ │ ├── perf/ # Performance profiling│ │ │ │ └── utils/ # Object pools, math│ │ │ └── registries/ # Component/System registries│ │ └── ...│ ││ ├── config/ # Branding configuration│ ├── logging/ # Performance monitoring│ └── build-config/ # Build configuration│└── turbo.json # Turborepo build orchestration
Packages follow a strict dependency hierarchy to avoid circular dependencies:
@web-engine-dev/config (0 dependencies)↑@web-engine-dev/logging (depends on config)↑@web-engine-dev/core (depends on logging, config)↑apps/studio, apps/player, apps/server (depend on core)
The game loop must never allocate memory. Here's how we achieve this:
Critical Rule
In src/engine (the "hot path"): NEVERuse new inside update loops. No temporary objects, arrays, or closures. Always use pre-allocated buffers.
// ❌ BAD - Allocates every framefunction update(entities) {for (const eid of entities) {const pos = new THREE.Vector3(); // ❌ newpos.set(Transform.x[eid], ...);const offset = { x: 1, y: 0, z: 0 }; // ❌ object literal}}
// ✅ GOOD - Zero allocationsconst _tempVec3 = new THREE.Vector3(); // Pre-allocatedconst _offset = { x: 0, y: 0, z: 0 }; // Reusedfunction update(entities) {for (let i = 0; i < entities.length; i++) {const eid = entities[i];_tempVec3.set(Transform.x[eid], Transform.y[eid], Transform.z[eid]);_offset.x = 1;_offset.y = 0;_offset.z = 0;}}
Add custom components and systems without modifying core code:
import { ComponentRegistry, SystemRegistry } from "@web-engine-dev/core";// Register custom componentComponentRegistry.register("Inventory", {name: "Inventory",description: "Player inventory",props: {capacity: { type: "int", default: 20 },gold: { type: "int", default: 0 },},});// Register custom systemSystemRegistry.register("InventorySystem", {phase: "GAMEPLAY",query: ["Inventory"],update: (world, entities) => {for (const eid of entities) {// Process inventory logic}},});