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.

packages/core/src/engine/ecs/systems/MovementSystem.ts
typescript
import type { IWorld } from 'bitecs';
import { defineQuery } from 'bitecs';
import { Transform, Velocity } from '../components';
// Define a query for entities with Transform and Velocity
const movingQuery = defineQuery([Transform, Velocity]);
// Define the system function
export 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.

packages/core/src/registries/SystemRegistry.ts
typescript
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.

packages/core/src/engine/ecs/systems/SystemManifest.ts
typescript
import { SystemRegistry, SystemPhase } from '@web-engine/core';
import { MovementSystem } from './MovementSystem';
// Register the system
SystemRegistry.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 system
  • description — Human-readable description
  • phase — Execution phase (Input, Logic, Physics, etc.)
  • priority — Order within phase (lower runs first, default: 0)
  • execute — The system function
  • enabled — Whether the system is active (default: true)
  • requires — Hard dependencies (systems that must be registered)
  • after — Systems that must run before this one
  • before — Systems that must run after this one
  • skipWhenEmpty — Skip execution if query returns 0 entities
  • query — 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:

packages/core/src/engine/ecs/systems/HealthRegenSystem.ts
typescript
import type { IWorld } from 'bitecs';
import { defineQuery } from 'bitecs';
import { Health, Transform } from '../components';
// Define component if not exists
export 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 HealthRegen
const 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;
}
Register the custom system
typescript
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 last

Runtime Control#

Systems can be enabled or disabled at runtime without re-registration:

import { SystemRegistry } from '@web-engine/core';
// Disable a system
SystemRegistry.setEnabled('PhysicsDebugSystem', false);
// Enable a system
SystemRegistry.setEnabled('PhysicsDebugSystem', true);
// Check if system is enabled
const enabled = SystemRegistry.isEnabled('PhysicsDebugSystem');
// Get system definition
const 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
Systems | Web Engine Docs | Web Engine Docs