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
Understand ECS systems: the logic layer that processes entities with specific components. Learn about system phases, registration, dependencies, and creating custom systems.
Systems are functions that operate on entities matching specific component sets. They contain all game logic — physics, rendering, AI, animation, and more. Systems run every frame in a well-defined order determined by their phase and priority.
A system is a pure function that takes the ECS world, delta time, and elapsed time as input, and returns the world. Systems query for entities with specific components, then iterate over them to update state.
import type { IWorld } from 'bitecs';import { defineQuery } from 'bitecs';import { Transform, Velocity } from '../components';// Define a query for entities with Transform and Velocityconst movingQuery = defineQuery([Transform, Velocity]);// Define the system functionexport function MovementSystem(world: IWorld, delta: number, time: number): IWorld {// Get all entities matching the queryconst entities = movingQuery(world);// Iterate and updatefor (let i = 0; i < entities.length; i++) {const eid = entities[i];// Apply velocity to positionTransform.position.x[eid] += Velocity.linear.x[eid] * delta;Transform.position.y[eid] += Velocity.linear.y[eid] * delta;Transform.position.z[eid] += Velocity.linear.z[eid] * delta;}return world;}
System Signature
All systems must have the signature: (world: IWorld, delta: number, time: number) => IWorld. This allows the SystemRegistry to execute them in a uniform way.
Web Engine organizes systems into execution phases. Each phase runs in order every frame, ensuring predictable behavior and proper data flow.
export enum SystemPhase {Input = 0, // Read input devices, update input stateNetwork = 100, // Process network packets, entity replicationLogic = 200, // Game logic, AI, scripts, behaviorsPhysics = 300, // Physics simulation, collision detectionAnimation = 400, // Skeletal animation, blend treesAudio = 500, // Spatial audio, music, sound effectsRender = 600, // Rendering, culling, material sortingPostRender = 700 // Debug overlays, profiling, cleanup}
InputSystem polls keyboard, mouse, gamepad, and touch devices. Writes to Input components.
NetworkSystem processes packets from server. GameStateSyncSystem synchronizes entity state. ChatSystem buffers messages.
BehaviorSystem, ScriptSystem, AIStateMachineSystem, PlayerControllerSystem, CharacterControllerSystem, InteractionSystem, and more run here.
PhysicsSystem steps Rapier simulation. ColliderDataStagingSystem prepares mesh colliders. JointSystem maintains constraints. TriggerSystem processes trigger volumes.
PlayerAnimationSystem maps locomotion state to blend parameters. AnimationSystem evaluates animation graphs. AnimationEventSystem dispatches events.
AudioSettingsSystem configures global mixer. AudioSystem plays spatial and UI audio. FootstepSystem triggers surface-aware footsteps. VoiceSystem handles VOIP.
FrustumCullingSystem culls off-screen objects. MaterialSortSystem optimizes draw calls. RenderSystem syncs ECS to Three.js. InstancedRenderSystem batches draws. ParticleSystem, TerrainSystem, WaterSystem, and more.
NavMeshDebugSystem, PhysicsDebugSystem, ProfilerSystem, and EntityCleanupSystem run last.
Phase Order Matters
The phase order ensures proper data flow. For example, Input must run before Logic so that game code can read input state. Physics must run before Render so that rendering sees the latest physics results.
Systems are registered with the SystemRegistry, which handles execution order, dependency validation, and runtime enable/disable.
import { SystemRegistry, SystemPhase } from '@web-engine-dev/core';import { MovementSystem } from './MovementSystem';// Register the systemSystemRegistry.register({name: 'MovementSystem',description: 'Applies velocity to transform positions.',phase: SystemPhase.Logic,priority: 10, // Lower runs first within phaseexecute: MovementSystem,enabled: true,});
name — Unique identifier for the systemdescription — Human-readable descriptionphase — Execution phase (Input, Logic, Physics, etc.)priority — Order within phase (lower runs first, default: 0)execute — The system functionenabled — Whether the system is active (default: true)requires — Hard dependencies (systems that must be registered)after — Systems that must run before this onebefore — Systems that must run after this oneskipWhenEmpty — Skip execution if query returns 0 entitiesquery — Primary query for skipWhenEmpty optimizationSystems can declare dependencies to enforce execution order and validate that required systems are registered.
SystemRegistry.register({name: 'CharacterControllerSystem',phase: SystemPhase.Logic,priority: 5,// Require PlayerControllerSystem to be registeredrequires: ['PlayerControllerSystem'],// Run after PlayerControllerSystem within the Logic phaseafter: ['PlayerControllerSystem'],execute: CharacterControllerSystem,});
The SystemRegistry validates dependencies at registration time and when building the execution schedule:
Let's create a complete custom system from scratch:
import type { IWorld } from 'bitecs';import { defineQuery } from 'bitecs';import { Health, Transform } from '../components';// Define component if not existsexport const HealthRegen = defineComponent({rate: Types.f32, // HP per seconddelay: Types.f32, // Delay after damage before regen startstimeSinceDamage: Types.f32,});// Query for entities with both Health and HealthRegenconst healthRegenQuery = defineQuery([Health, HealthRegen]);export function HealthRegenSystem(world: IWorld,delta: number,time: number): IWorld {const entities = healthRegenQuery(world);for (let i = 0; i < entities.length; i++) {const eid = entities[i];// Increment time since last damageHealthRegen.timeSinceDamage[eid] += delta;// Only regen if delay has passedif (HealthRegen.timeSinceDamage[eid] >= HealthRegen.delay[eid]) {const current = Health.current[eid];const max = Health.max[eid];const rate = HealthRegen.rate[eid];// Regenerate health, clamped to maxHealth.current[eid] = Math.min(current + rate * delta, max);}}return world;}
import { SystemRegistry, SystemPhase } from '@web-engine-dev/core';import { HealthRegenSystem } from './HealthRegenSystem';SystemRegistry.register({name: 'HealthRegenSystem',description: 'Regenerates health over time after delay.',phase: SystemPhase.Logic,priority: 50,execute: HealthRegenSystem,enabled: true,});
For systems that operate on rare components, you can skip execution entirely when no entities match the query using the skipWhenEmpty optimization.
import { defineQuery } from 'bitecs';import { Vehicle, Transform } from '../components';const vehicleQuery = defineQuery([Vehicle, Transform]);SystemRegistry.register({name: 'VehicleSystem',phase: SystemPhase.Logic,priority: 7,execute: VehicleSystem,// Skip this system if no vehicles existskipWhenEmpty: true,query: vehicleQuery,});
Performance Benefit
skipWhenEmpty prevents wasted CPU cycles on systems that have no work to do. This is especially useful for conditional features like vehicles, weather, particles, and debug overlays. The query check is O(1) as bitECS caches results.
Systems execute in a well-defined order every frame:
Frame N:┌─ INPUT Phase (priority: -∞ to +∞)│ └─ InputSystem (priority: 0)│├─ NETWORK Phase│ ├─ NetworkSystem (priority: 0)│ ├─ GameStateSyncSystem (priority: 420, after: NetworkSystem)│ └─ ChatSystem (priority: 430, after: NetworkSystem)│├─ LOGIC Phase│ ├─ BehaviorSystem (priority: -20)│ ├─ ScriptSystem (priority: -5)│ ├─ PlayerControllerSystem (priority: 4)│ ├─ CharacterControllerSystem (priority: 5, after: PlayerControllerSystem)│ └─ ... more logic systems ...│├─ PHYSICS Phase│ ├─ PhysicsLODSystem (priority: -1)│ ├─ ColliderDataStagingSystem (priority: 0, after: PhysicsLODSystem)│ ├─ PhysicsSystem (after: ColliderDataStagingSystem)│ ├─ JointSystem (priority: 5, after: PhysicsSystem)│ └─ TriggerSystem (priority: 10, after: PhysicsSystem, JointSystem)│├─ ANIMATION Phase│ ├─ PlayerAnimationSystem (priority: -5)│ ├─ AnimationSystem (priority: 0, after: PlayerAnimationSystem)│ └─ AnimationEventSystem (priority: 10, after: AnimationSystem)│├─ AUDIO Phase│ ├─ AudioSettingsSystem (priority: -20)│ ├─ AudioSystem (priority: 0)│ ├─ FootstepSystem (priority: 10, after: AudioSystem)│ └─ VoiceSystem (priority: 20, after: AudioSystem)│├─ RENDER Phase│ ├─ BoundingBoxSystem (priority: -200)│ ├─ FrustumCullingSystem (priority: -2, after: CameraSystem)│ ├─ MaterialSortSystem (priority: -1, after: FrustumCullingSystem)│ ├─ RenderSystem (priority: 0, after: MaterialSortSystem)│ ├─ ... terrain, water, particles, etc. ...│ └─ RenderStatsSystem (priority: 100, after: RenderSystem)│└─ POST_RENDER Phase├─ NavMeshDebugSystem (if enabled)├─ PhysicsDebugSystem (if enabled)├─ ProfilerSystem (priority: 90)└─ EntityCleanupSystem (priority: 999) ← Always runs last
Systems can be enabled or disabled at runtime without re-registration:
import { SystemRegistry } from '@web-engine-dev/core';// Disable a systemSystemRegistry.setEnabled('PhysicsDebugSystem', false);// Enable a systemSystemRegistry.setEnabled('PhysicsDebugSystem', true);// Check if system is enabledconst enabled = SystemRegistry.isEnabled('PhysicsDebugSystem');// Get system definitionconst def = SystemRegistry.get('PhysicsDebugSystem');console.log(def?.description);