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 how to create and register custom ECS systems with proper phases, dependencies, and optimization patterns.
Systems are the logic layer in Web Engine's ECS architecture. This guide covers creating custom systems, registering them with the SystemRegistry, managing dependencies, and optimizing for performance.
A system is a function that processes entities matching a query. Systems run in phases and can declare dependencies on other systems.
import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";import type { IWorld } from "bitecs";// Define the system execute functionfunction mySystemExecute(world: IWorld, delta: number, time: number): IWorld {// Process entities herereturn world;}// Register the systemSystemRegistry.register({name: "MySystem",description: "Does something cool",phase: SystemPhase.Logic,priority: 0,execute: mySystemExecute,});
interface SystemDefinition {name: string; // Unique system identifierdescription?: string; // Human-readable descriptionphase: SystemPhase; // Execution phasepriority?: number; // Priority within phase (default: 0)execute: SystemExecute; // System logic functionenabled?: boolean; // Whether system is enabled (default: true)requires?: string[]; // Hard dependencies (must be registered)after?: string[]; // Systems that must run before this onebefore?: string[]; // Systems that must run after this oneskipWhenEmpty?: boolean; // Skip execution if query is emptyquery?: SystemQueryRef; // Primary entity query}
Systems run in ordered phases each frame
Declare explicit ordering with after/before
Skip systems when no entities match
Fine-grained control within phases
export enum SystemPhase {Input = 0, // Read input devices, update stateNetwork = 100, // Network synchronizationLogic = 200, // Game logic, AI, scriptsPhysics = 300, // Physics simulation stepAnimation = 400, // Skeletal animations, IKAudio = 500, // Audio playback updatesRender = 600, // Three.js renderingPostRender = 700 // Post-processing, cleanup}
Phase Ordering
Systems are executed in phase order. Within a phase, systems are sorted by priority (lower runs first), then by dependency graph.
import { defineQuery, IWorld } from "bitecs";import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";import { Transform, RigidBody, Velocity } from "./components";// Define entity queryconst gravityQuery = defineQuery([Transform, RigidBody, Velocity]);// System execute functionfunction gravitySystemExecute(world: IWorld, delta: number, time: number): IWorld {const entities = gravityQuery(world);const gravity = -9.81;for (let i = 0; i < entities.length; i++) {const eid = entities[i];// Skip static bodiesif (RigidBody.type[eid] === 1) continue;// Apply gravityVelocity.y[eid] += gravity * delta;}return world;}// Register the systemSystemRegistry.register({name: "GravitySystem",description: "Applies gravity to dynamic rigid bodies",phase: SystemPhase.Physics,priority: -10, // Run before main physics stepexecute: gravitySystemExecute,skipWhenEmpty: true,query: gravityQuery,});
SystemRegistry.register({name: "CharacterControllerSystem",description: "Player movement controller",phase: SystemPhase.Logic,priority: 10,execute: characterControllerExecute,// Declare dependenciesrequires: ["InputSystem"], // Must be registeredafter: ["InputSystem"], // Must run after InputSystembefore: ["AnimationSystem"], // Must run before AnimationSystemskipWhenEmpty: true,query: characterQuery,});
Priority controls execution order within a phase. Lower values run first:
// Priority: -100 to 100 (recommended range)SystemRegistry.register({name: "EarlyInputSystem",phase: SystemPhase.Input,priority: -10, // Runs firstexecute: earlyInputExecute,});SystemRegistry.register({name: "InputSystem",phase: SystemPhase.Input,priority: 0, // Default priorityexecute: inputExecute,});SystemRegistry.register({name: "LateInputSystem",phase: SystemPhase.Input,priority: 10, // Runs lastexecute: lateInputExecute,});
import { defineQuery } from "bitecs";import { Particle, Transform } from "./components";const particleQuery = defineQuery([Particle, Transform]);SystemRegistry.register({name: "ParticleSystem",phase: SystemPhase.Logic,execute: particleSystemExecute,// Skip execution when no particles existskipWhenEmpty: true,query: particleQuery,});// System will only run when particleQuery matches entitiesfunction particleSystemExecute(world: IWorld, delta: number): IWorld {const entities = particleQuery(world);// This is only called when entities.length > 0for (let i = 0; i < entities.length; i++) {// Process particles}return world;}
Performance Optimization
Use skipWhenEmpty: true for systems that process specific entities. This saves CPU cycles when no matching entities exist.
import { defineComponent, Types } from "bitecs";// Define a singleton component for global stateexport const GameState = defineComponent({isPaused: Types.ui8,timeScale: Types.f32,score: Types.i32,});// System A writes to GameStatefunction systemA(world: IWorld) {const state = 0; // Singleton entityGameState.isPaused[state] = 1;return world;}// System B reads from GameStatefunction systemB(world: IWorld) {const state = 0;if (GameState.isPaused[state]) {// Game is paused, skip logicreturn world;}// Process normal logicreturn world;}
import { EventPool } from "@web-engine-dev/core";// Define event structureinterface DamageEvent {entity: number;amount: number;source: number;}// Create event pool (zero-GC)const damageEvents = new EventPool<DamageEvent>(() => ({ entity: 0, amount: 0, source: 0 }),256 // Max events per frame);// System A: Emit damage eventsfunction combatSystem(world: IWorld): IWorld {// ...const event = damageEvents.acquireSlot();if (event) {event.entity = targetEid;event.amount = 25;event.source = attackerEid;}return world;}// System B: Process damage eventsfunction healthSystem(world: IWorld): IWorld {damageEvents.processEvents(event => {applyDamage(event.entity, event.amount);});// Clear events for next framedamageEvents.clear();return world;}
import { Vector3Pool } from "@web-engine-dev/core";// ❌ BAD - Allocates every framefunction badSystem(world: IWorld): IWorld {const entities = query(world);for (const eid of entities) {const direction = new THREE.Vector3(1, 0, 0); // ❌// Use direction...}return world;}// ✅ GOOD - Reuses pooled objectfunction goodSystem(world: IWorld): IWorld {const entities = query(world);for (let i = 0; i < entities.length; i++) {const eid = entities[i];const direction = Vector3Pool.acquire();direction.set(1, 0, 0);// Use direction...Vector3Pool.release(direction);}return world;}
// ❌ BAD - Individual updatesfunction individualUpdates(world: IWorld): IWorld {const entities = query(world);for (const eid of entities) {scene.add(getMesh(eid)); // ❌ Separate scene updates}return world;}// ✅ GOOD - Batched updatefunction batchedUpdates(world: IWorld): IWorld {const entities = query(world);const group = new THREE.Group();for (let i = 0; i < entities.length; i++) {group.add(getMesh(entities[i]));}scene.add(group); // Single scene updatereturn world;}
import { frameProfiler } from "@web-engine-dev/core";// Enable/disable systems at runtimeSystemRegistry.setEnabled("MySystem", false); // DisableSystemRegistry.setEnabled("MySystem", true); // Enable// Check if system is registeredif (SystemRegistry.has("MySystem")) {console.log("MySystem is registered");}// Get system definitionconst systemDef = SystemRegistry.get("MySystem");console.log(systemDef?.phase, systemDef?.priority);// Get execution orderconst executionList = SystemRegistry.getExecutionList();executionList.forEach((sys, index) => {console.log(`${index}: ${sys.name} (phase: ${sys.phase})`);});// Profile system performanceconst stats = frameProfiler.getStatistics();stats.slowestSystems.forEach(sys => {console.log(`${sys.name}: avg=${sys.avgTime.toFixed(2)}ms`);});
import { defineQuery, defineComponent, Types, IWorld } from "bitecs";import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";// Define componentsexport const Health = defineComponent({current: Types.f32,max: Types.f32,regenRate: Types.f32,regenDelay: Types.f32,lastDamageTime: Types.f32,});export const HealthRegen = defineComponent({enabled: Types.ui8,});// Create queryconst healthRegenQuery = defineQuery([Health, HealthRegen]);// System logicfunction healthRegenExecute(world: IWorld, delta: number, time: number): IWorld {const entities = healthRegenQuery(world);for (let i = 0; i < entities.length; i++) {const eid = entities[i];// Skip if regen is disabledif (!HealthRegen.enabled[eid]) continue;// Wait for regen delay after damageconst timeSinceDamage = time - Health.lastDamageTime[eid];if (timeSinceDamage < Health.regenDelay[eid]) continue;// Apply regenerationconst current = Health.current[eid];const max = Health.max[eid];const regenRate = Health.regenRate[eid];if (current < max) {Health.current[eid] = Math.min(max, current + regenRate * delta);}}return world;}// Register systemSystemRegistry.register({name: "HealthRegenSystem",description: "Regenerates health over time",phase: SystemPhase.Logic,priority: 20,execute: healthRegenExecute,skipWhenEmpty: true,query: healthRegenQuery,});