Systems
Systems vs Components
Section titled “Systems vs Components”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.
Defining a System
Section titled “Defining a System”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(); }}System Lifecycle
Section titled “System Lifecycle”| Hook | When |
|---|---|
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. |
Phases
Section titled “Phases”Every system declares a phase that determines when it runs in the frame:
import { Phase } from "@yagejs/core";| Phase | Order | Typical use |
|---|---|---|
Phase.EarlyUpdate | 1 | Input polling, network message processing |
Phase.FixedUpdate | 2 | Physics stepping, deterministic gameplay |
Phase.Update | 3 | Physics interpolation, general game logic, component updates |
Phase.LateUpdate | 4 | UI layout, constraint solving |
Phase.Render | 5 | Display sync, sprite rendering |
Phase.EndOfFrame | 6 | Entity cleanup, deferred teardown (onDestroy, detach from scene) |
Priority
Section titled “Priority”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.
QueryCache
Section titled “QueryCache”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; } }}Working with Query Results
Section titled “Working with Query Results”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 entitiesfor (const entity of result) { // ...}
// Get the countconsole.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.