Creating Custom Systems

Advanced

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.

System Basics#

A system is a function that processes entities matching a query. Systems run in phases and can declare dependencies on other systems.

BasicSystem.ts
typescript
import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";
import type { IWorld } from "bitecs";
// Define the system execute function
function mySystemExecute(world: IWorld, delta: number, time: number): IWorld {
// Process entities here
return world;
}
// Register the system
SystemRegistry.register({
name: "MySystem",
description: "Does something cool",
phase: SystemPhase.Logic,
priority: 0,
execute: mySystemExecute,
});

System Definition#

SystemDefinition.ts
typescript
interface SystemDefinition {
name: string; // Unique system identifier
description?: string; // Human-readable description
phase: SystemPhase; // Execution phase
priority?: number; // Priority within phase (default: 0)
execute: SystemExecute; // System logic function
enabled?: boolean; // Whether system is enabled (default: true)
requires?: string[]; // Hard dependencies (must be registered)
after?: string[]; // Systems that must run before this one
before?: string[]; // Systems that must run after this one
skipWhenEmpty?: boolean; // Skip execution if query is empty
query?: SystemQueryRef; // Primary entity query
}

Phase-Based Execution

Systems run in ordered phases each frame

Dependency Graph

Declare explicit ordering with after/before

Conditional Execution

Skip systems when no entities match

Priority Ordering

Fine-grained control within phases

System Phases#

SystemPhase.ts
typescript
export enum SystemPhase {
Input = 0, // Read input devices, update state
Network = 100, // Network synchronization
Logic = 200, // Game logic, AI, scripts
Physics = 300, // Physics simulation step
Animation = 400, // Skeletal animations, IK
Audio = 500, // Audio playback updates
Render = 600, // Three.js rendering
PostRender = 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.

Creating a System#

System with Entity Query#

GravitySystem.ts
typescript
import { defineQuery, IWorld } from "bitecs";
import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";
import { Transform, RigidBody, Velocity } from "./components";
// Define entity query
const gravityQuery = defineQuery([Transform, RigidBody, Velocity]);
// System execute function
function 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 bodies
if (RigidBody.type[eid] === 1) continue;
// Apply gravity
Velocity.y[eid] += gravity * delta;
}
return world;
}
// Register the system
SystemRegistry.register({
name: "GravitySystem",
description: "Applies gravity to dynamic rigid bodies",
phase: SystemPhase.Physics,
priority: -10, // Run before main physics step
execute: gravitySystemExecute,
skipWhenEmpty: true,
query: gravityQuery,
});

System with Dependencies#

CharacterControllerSystem.ts
typescript
SystemRegistry.register({
name: "CharacterControllerSystem",
description: "Player movement controller",
phase: SystemPhase.Logic,
priority: 10,
execute: characterControllerExecute,
// Declare dependencies
requires: ["InputSystem"], // Must be registered
after: ["InputSystem"], // Must run after InputSystem
before: ["AnimationSystem"], // Must run before AnimationSystem
skipWhenEmpty: true,
query: characterQuery,
});

System Priority#

Priority controls execution order within a phase. Lower values run first:

SystemPriority.ts
typescript
// Priority: -100 to 100 (recommended range)
SystemRegistry.register({
name: "EarlyInputSystem",
phase: SystemPhase.Input,
priority: -10, // Runs first
execute: earlyInputExecute,
});
SystemRegistry.register({
name: "InputSystem",
phase: SystemPhase.Input,
priority: 0, // Default priority
execute: inputExecute,
});
SystemRegistry.register({
name: "LateInputSystem",
phase: SystemPhase.Input,
priority: 10, // Runs last
execute: lateInputExecute,
});

Conditional Execution#

ConditionalSystem.ts
typescript
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 exist
skipWhenEmpty: true,
query: particleQuery,
});
// System will only run when particleQuery matches entities
function particleSystemExecute(world: IWorld, delta: number): IWorld {
const entities = particleQuery(world);
// This is only called when entities.length > 0
for (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.

Inter-System Communication#

Shared State via Components#

SharedState.ts
typescript
import { defineComponent, Types } from "bitecs";
// Define a singleton component for global state
export const GameState = defineComponent({
isPaused: Types.ui8,
timeScale: Types.f32,
score: Types.i32,
});
// System A writes to GameState
function systemA(world: IWorld) {
const state = 0; // Singleton entity
GameState.isPaused[state] = 1;
return world;
}
// System B reads from GameState
function systemB(world: IWorld) {
const state = 0;
if (GameState.isPaused[state]) {
// Game is paused, skip logic
return world;
}
// Process normal logic
return world;
}

Event Queues#

EventQueue.ts
typescript
import { EventPool } from "@web-engine-dev/core";
// Define event structure
interface 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 events
function 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 events
function healthSystem(world: IWorld): IWorld {
damageEvents.processEvents(event => {
applyDamage(event.entity, event.amount);
});
// Clear events for next frame
damageEvents.clear();
return world;
}

Performance Best Practices#

Avoid Allocations#

ZeroGCSystem.ts
typescript
import { Vector3Pool } from "@web-engine-dev/core";
// ❌ BAD - Allocates every frame
function 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 object
function 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;
}

Batch Operations#

BatchedSystem.ts
typescript
// ❌ BAD - Individual updates
function individualUpdates(world: IWorld): IWorld {
const entities = query(world);
for (const eid of entities) {
scene.add(getMesh(eid)); // ❌ Separate scene updates
}
return world;
}
// ✅ GOOD - Batched update
function 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 update
return world;
}

Debugging Systems#

SystemDebug.ts
typescript
import { frameProfiler } from "@web-engine-dev/core";
// Enable/disable systems at runtime
SystemRegistry.setEnabled("MySystem", false); // Disable
SystemRegistry.setEnabled("MySystem", true); // Enable
// Check if system is registered
if (SystemRegistry.has("MySystem")) {
console.log("MySystem is registered");
}
// Get system definition
const systemDef = SystemRegistry.get("MySystem");
console.log(systemDef?.phase, systemDef?.priority);
// Get execution order
const executionList = SystemRegistry.getExecutionList();
executionList.forEach((sys, index) => {
console.log(`${index}: ${sys.name} (phase: ${sys.phase})`);
});
// Profile system performance
const stats = frameProfiler.getStatistics();
stats.slowestSystems.forEach(sys => {
console.log(`${sys.name}: avg=${sys.avgTime.toFixed(2)}ms`);
});

Complete Example#

HealthRegenSystem.ts
typescript
import { defineQuery, defineComponent, Types, IWorld } from "bitecs";
import { SystemRegistry, SystemPhase } from "@web-engine-dev/core";
// Define components
export 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 query
const healthRegenQuery = defineQuery([Health, HealthRegen]);
// System logic
function 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 disabled
if (!HealthRegen.enabled[eid]) continue;
// Wait for regen delay after damage
const timeSinceDamage = time - Health.lastDamageTime[eid];
if (timeSinceDamage < Health.regenDelay[eid]) continue;
// Apply regeneration
const 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 system
SystemRegistry.register({
name: "HealthRegenSystem",
description: "Regenerates health over time",
phase: SystemPhase.Logic,
priority: 20,
execute: healthRegenExecute,
skipWhenEmpty: true,
query: healthRegenQuery,
});
Advanced | Web Engine Docs | Web Engine Docs