Skip to content

Systems

In YAGE the split is clear:

  • Components hold state and game logic — player controllers, health bars, enemy AI. Most gameplay code lives here.
  • Systems are for engine-level cross-cutting concerns — physics stepping, rendering, audio mixing. They are typically authored by plugin developers, not game developers.

The built-in ComponentUpdateSystem bridges the two: it iterates every active component each frame and calls update(dt) / fixedUpdate(dt). Within an entity, components run in ascending updatePriority (add order by default) — see Update Order.

Extend the System base class:

import { System, Phase, EngineContext, ServiceKey } from "@yagejs/core";
class ParticleManager {
step(dt: number) {
// advance every live particle by dt
}
dispose() {
// release the particle pool
}
}
const ParticleManagerKey = new ServiceKey<ParticleManager>("my-particles");
class ParticleSystem extends System {
readonly phase = Phase.Update;
readonly priority = 500;
private particles!: ParticleManager;
onRegister(context: EngineContext) {
this.particles = context.resolve(ParticleManagerKey);
}
update(dt: number) {
this.particles.step(dt);
}
onUnregister() {
this.particles.dispose();
}
}
HookWhen
onRegister(context)Called once when the system is added to the scheduler. Use it to resolve services.
update(dt)Called every frame during the system’s assigned phase.
onUnregister()Called when the system is removed or the engine is destroyed.

Every system declares a phase that determines when it runs in the frame:

import { Phase } from "@yagejs/core";
PhaseOrderTypical use
Phase.EarlyUpdate1Input polling, network message processing
Phase.FixedUpdate2Physics stepping, deterministic gameplay
Phase.Update3Physics interpolation, general game logic, component updates
Phase.LateUpdate4UI layout, constraint solving
Phase.Render5Display sync, sprite rendering
Phase.EndOfFrame6Entity cleanup, deferred teardown (onDestroy, detach from scene)

Within a phase, systems are sorted by priority — lower numbers run first:

import { System, Phase } from "@yagejs/core";
class WindSystem extends System {
readonly phase = Phase.FixedUpdate;
// after PhysicsSystem (0), before ComponentFixedUpdateSystem (1000)
readonly priority = 100;
update(dt: number) {
// apply wind to the physics bodies
}
}

The built-in component passes use priority 1000, so any system with a lower number in the same phase runs before component update() / fixedUpdate() calls. The full order of the shipped systems, and the priority bands to place a new one in, are in What Runs When.

Systems that need to iterate entities matching a specific component signature use the QueryCache:

import {
System,
Phase,
EngineContext,
QueryCacheKey,
QueryResult,
Transform,
} from "@yagejs/core";
import { SpriteComponent } from "@yagejs/renderer";
class SpriteRenderSystem extends System {
readonly phase = Phase.Render;
readonly priority = 0;
private query!: QueryResult;
onRegister(context: EngineContext) {
const cache = context.resolve(QueryCacheKey);
this.query = cache.register([Transform, SpriteComponent]);
}
update(dt: number) {
for (const entity of this.query) {
const transform = entity.get(Transform);
const sprite = entity.get(SpriteComponent);
// sync sprite position to transform
sprite.sprite.x = transform.position.x;
sprite.sprite.y = transform.position.y;
}
}
}

A filter class matches the class itself and any subclass of it, so register([Transform, VisualComponent]) finds entities carrying a SpriteComponent, a TextComponent, or your own subclass. A result holds entities, not components — when one entity may carry several matching components, read them with entity.getAll(VisualComponent).

A QueryResult is a live result set — it updates automatically as entities gain or lose components:

import { QueryCacheKey, Transform } from "@yagejs/core";
const result = context.resolve(QueryCacheKey).register([Transform]);
// Iterate all matching entities
for (const entity of result) {
// ...
}
// Get the count
console.log(result.size);
// Get the first match (or undefined)
const first = result.first;
// Snapshot to an array (creates a copy)
const all = result.toArray();

Queries are shared: if two systems register the same component set, they receive the same QueryResult instance. This keeps memory usage low and updates O(1).

Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/core-concepts.md.