Architecture

Advanced

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.

Core Philosophy#

  • Data-Oriented Design — Data lives in flat TypedArrays. Logic operates on data in batches.
  • Registry-Driven — No hardcoded lists. All types are registered dynamically.
  • Zero-GC Hot Path — No allocations in the game loop. Pre-allocate everything.
  • Monorepo Structure — Clear separation between packages (core, editor, player).
  • Production-First — No placeholders. Every feature is production-ready.

Registry Architecture#

Registries are the central source of truth for metadata:

ComponentRegistry

Defines all ECS components with their schemas and editor metadata.

SystemRegistry

Registers all systems with their phase, dependencies, and enable conditions.

AssetLoaderRegistry

Maps file extensions to asset loader functions.

ComponentRegistry.ts
typescript
// Register a custom component
ComponentRegistry.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 registry
const schema = ComponentRegistry.get("MyComponent");
const allComponents = ComponentRegistry.getAll();

System Phases#

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:

SystemPhase.ts
typescript
export enum SystemPhase {
Input = 0, // Read input devices
Network = 100, // Network sync
Logic = 200, // Game logic, AI, scripts
Physics = 300, // Physics simulation
Animation = 400, // Skeletal animations
Audio = 500, // Audio updates
Render = 600, // Three.js rendering
PostRender = 700 // Post-processing, cleanup
}

Systems are sorted first by phase, then by priority within that phase. Lower priority values run first:

SystemRegistration.ts
typescript
SystemRegistry.register({
name: "InputSystem",
phase: SystemPhase.Input,
priority: 0, // Runs first in Input phase
execute: (world, delta, time) => { /* ... */ }
});
SystemRegistry.register({
name: "ScriptSystem",
phase: SystemPhase.Logic,
priority: 10, // Runs after priority 0 systems
after: ["InputSystem"], // Explicit ordering dependency
execute: (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.

Data Flow#

Scene Loading#

scene.json → SceneLoader → Create Entities → Add Components → Run onStart

Game Loop#

Input → Physics → Scripts → Animation → Render

Editor → Engine#

UI Event → Command → ECS Mutation → Sync to Three.js → Render

Module Structure#

Web Engine is organized as a monorepo with clear separation between packages:

@web-engine-dev/core

ECS runtime, systems, components, registries, asset loading, and physics integration.

@web-engine-dev/config

Branding configuration, storage keys, and white-label settings.

@web-engine-dev/logging

Performance monitoring, frame profiling, and logging utilities.

@web-engine-dev/build-config

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

Dependency Graph#

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)

Zero-GC Principles#

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.ts
typescript
// ❌ BAD - Allocates every frame
function update(entities) {
for (const eid of entities) {
const pos = new THREE.Vector3(); // ❌ new
pos.set(Transform.x[eid], ...);
const offset = { x: 1, y: 0, z: 0 }; // ❌ object literal
}
}
Good.ts
typescript
// ✅ GOOD - Zero allocations
const _tempVec3 = new THREE.Vector3(); // Pre-allocated
const _offset = { x: 0, y: 0, z: 0 }; // Reused
function 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;
}
}

Extending the Engine#

Add custom components and systems without modifying core code:

MyGamePlugin.ts
typescript
import { ComponentRegistry, SystemRegistry } from "@web-engine-dev/core";
// Register custom component
ComponentRegistry.register("Inventory", {
name: "Inventory",
description: "Player inventory",
props: {
capacity: { type: "int", default: 20 },
gold: { type: "int", default: 0 },
},
});
// Register custom system
SystemRegistry.register("InventorySystem", {
phase: "GAMEPLAY",
query: ["Inventory"],
update: (world, entities) => {
for (const eid of entities) {
// Process inventory logic
}
},
});
Architecture | Web Engine Docs