Systems
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.
What is a System?#
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 query const entities = movingQuery(world); // Iterate and update for (let i = 0; i < entities.length; i++) { const eid = entities[i]; // Apply velocity to position Transform.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.
System Phases#
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 state Network = 100, // Process network packets, entity replication Logic = 200, // Game logic, AI, scripts, behaviors Physics = 300, // Physics simulation, collision detection Animation = 400, // Skeletal animation, blend trees Audio = 500, // Spatial audio, music, sound effects Render = 600, // Rendering, culling, material sorting PostRender = 700 // Debug overlays, profiling, cleanup}Phase Breakdown#
INPUT (0)
InputSystem polls keyboard, mouse, gamepad, and touch devices. Writes to Input components.
NETWORK (100)
NetworkSystem processes packets from server. GameStateSyncSystem synchronizes entity state. ChatSystem buffers messages.
LOGIC (200)
BehaviorSystem, ScriptSystem, AIStateMachineSystem, PlayerControllerSystem, CharacterControllerSystem, InteractionSystem, and more run here.
PHYSICS (300)
PhysicsSystem steps Rapier simulation. ColliderDataStagingSystem prepares mesh colliders. JointSystem maintains constraints. TriggerSystem processes trigger volumes.
ANIMATION (400)
PlayerAnimationSystem maps locomotion state to blend parameters. AnimationSystem evaluates animation graphs. AnimationEventSystem dispatches events.
AUDIO (500)
AudioSettingsSystem configures global mixer. AudioSystem plays spatial and UI audio. FootstepSystem triggers surface-aware footsteps. VoiceSystem handles VOIP.
RENDER (600)
FrustumCullingSystem culls off-screen objects. MaterialSortSystem optimizes draw calls. RenderSystem syncs ECS to Three.js. InstancedRenderSystem batches draws. ParticleSystem, TerrainSystem, WaterSystem, and more.
POST_RENDER (700)
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.
System Registration#
Systems are registered with the SystemRegistry, which handles execution order, dependency validation, and runtime enable/disable.
import { SystemRegistry, SystemPhase } from '@web-engine/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 phase execute: MovementSystem, enabled: true,});System Definition Properties#
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 optimization
System Dependencies#
Systems 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 registered requires: ['PlayerControllerSystem'], // Run after PlayerControllerSystem within the Logic phase after: ['PlayerControllerSystem'], execute: CharacterControllerSystem,});Dependency Validation#
The SystemRegistry validates dependencies at registration time and when building the execution schedule:
- Missing dependencies — Throws error if required systems aren't registered
- Circular dependencies — Detects and prevents circular after/before chains
- Phase violations — Ensures after/before targets are in compatible phases
- Priority conflicts — Warns when explicit ordering contradicts priorities
Creating Custom Systems#
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 second delay: Types.f32, // Delay after damage before regen starts timeSinceDamage: 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 damage HealthRegen.timeSinceDamage[eid] += delta; // Only regen if delay has passed if (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 max Health.current[eid] = Math.min(current + rate * delta, max); } } return world;}import { SystemRegistry, SystemPhase } from '@web-engine/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,});Query Optimization: skipWhenEmpty#
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 exist skipWhenEmpty: 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.
System Execution Order#
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 lastRuntime Control#
Systems can be enabled or disabled at runtime without re-registration:
import { SystemRegistry } from '@web-engine/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);Best Practices#
- Keep systems focused — One system, one responsibility
- Define queries once — Reuse queries, don't create them in loops
- Use skipWhenEmpty — Skip systems with no matching entities
- Minimize dependencies — Only use after/before when truly needed
- Profile your systems — Use ProfilerSystem to find bottlenecks
- Avoid allocations — Reuse objects, use TypedArrays directly
- Document phase choice — Explain why a system is in a specific phase
- Test dependency order — Ensure systems run in the correct order