Dependency Injection
EngineContext
Section titled “EngineContext”EngineContext is the typed dependency injection container at the heart of
YAGE. Plugins register services during install(), and the rest of the engine
resolves them by key.
Services are plugin-owned infrastructure: the renderer, the physics world, the input manager, the audio manager. Game state such as a score, lives or an inventory is not a service. It lives in a component on a host entity, as State Management shows.
import { EngineContext, ServiceKey } from "@yagejs/core";ServiceKey
Section titled “ServiceKey”Every service is identified by a ServiceKey<T>. The type parameter ensures
that resolve() returns the correct type without casts:
import { ServiceKey } from "@yagejs/core";
class AudioMixer { masterVolume = 1;}
// Define a key with a descriptive labelconst AudioMixerKey = new ServiceKey<AudioMixer>("AudioMixer");Never use plain strings for service resolution — always define a ServiceKey.
The id string is the key’s identity. Two ServiceKey objects with the same id
resolve the same service. An optional integration may repeat an id for the same
service contract when importing the owner’s key would add a runtime dependency.
Keep a comment beside the repeated declaration that names the package that owns
the key.
Registering and Resolving
Section titled “Registering and Resolving”import type { EngineContext } from "@yagejs/core";
// In a plugin's install():function install(context: EngineContext) { const mixer = new AudioMixer(); context.register(AudioMixerKey, mixer);}
// Resolve (throws if not registered)const mixer = context.resolve(AudioMixerKey);
// Try resolve (returns undefined if not registered)const maybeMixer = context.tryResolve(AudioMixerKey);
// Check existenceif (context.has(AudioMixerKey)) { // ...}context.resolve() throws a clear error listing the missing key name.
Prefer it when the service is required. Use context.tryResolve() for optional
services.
A key can add a hint to that error. Pass missingHint to say what provides the
service:
import { ServiceKey } from "@yagejs/core";
interface Haptics { pulse(seconds: number): void;}
const HapticsKey = new ServiceKey<Haptics>("haptics", { missingHint: "Install hapticsPlugin().",});
// Throws: Service "haptics" is not registered. Install hapticsPlugin().context.resolve(HapticsKey);InspectorKey uses it: resolving the Inspector when no plugin installed one
throws an error that names DebugPlugin and InspectorPlugin.
Well-Known Keys
Section titled “Well-Known Keys”YAGE’s core and official plugins register these services:
| Key | Type | Package |
|---|---|---|
EngineKey | Engine | @yagejs/core |
EventBusKey | EventBus | @yagejs/core |
SceneManagerKey | SceneManager | @yagejs/core |
LoggerKey | Logger | @yagejs/core |
QueryCacheKey | QueryCache | @yagejs/core |
ErrorBoundaryKey | ErrorBoundary | @yagejs/core |
GameLoopKey | GameLoop | @yagejs/core |
InspectorKey | Inspector | @yagejs/core |
SceneRandomSourceKey | SceneRandomSource | @yagejs/core |
SystemSchedulerKey | SystemScheduler | @yagejs/core |
ProcessSystemKey | ProcessSystem | @yagejs/core |
AssetManagerKey | AssetManager | @yagejs/core |
Accessing Services from Components
Section titled “Accessing Services from Components”Inside a component, use this.use(key). The result is cached after the first
call so repeated access is free:
import { Component } from "@yagejs/core";import { InputManagerKey } from "@yagejs/input";import { PhysicsWorldKey, RigidBodyComponent } from "@yagejs/physics";
class PlayerController extends Component { update(dt: number) { const input = this.use(InputManagerKey); const physics = this.use(PhysicsWorldKey);
if (input.isPressed("jump")) { const body = this.sibling(RigidBodyComponent); body.setVelocityY(-600); } }}Some keys like PhysicsWorldKey and SceneRenderTreeKey are per-scene —
each scene gets its own instance. this.use() resolves the correct one
automatically; you don’t need to do anything different.
Accessing Services from Systems
Section titled “Accessing Services from Systems”Systems receive the EngineContext in onRegister. Store references there or
call this.use(key) during update:
import { System, Phase, GameLoopKey, LoggerKey } from "@yagejs/core";import type { EngineContext, Logger } from "@yagejs/core";
class DebugOverlaySystem extends System { readonly phase = Phase.Render; readonly priority = 9999;
private logger!: Logger;
onRegister(context: EngineContext) { this.logger = context.resolve(LoggerKey); }
update(dt: number) { const frame = this.use(GameLoopKey).frameCount; this.logger.debug("overlay", `Frame ${frame}: ${Math.round(1 / dt)} FPS`); }}Custom Services
Section titled “Custom Services”A plugin of your own registers a service when it wraps infrastructure the engine does not provide, such as a browser API:
import { ServiceKey, type Plugin, type EngineContext } from "@yagejs/core";
/** Vibration on devices that support it. */export interface Haptics { pulse(seconds: number): void;}
export const HapticsKey = new ServiceKey<Haptics>("haptics");
export function hapticsPlugin(): Plugin { return { name: "haptics", version: "1.0.0", install(context: EngineContext) { context.register(HapticsKey, { pulse(seconds) { if ("vibrate" in navigator) navigator.vibrate(Math.round(seconds * 1000)); }, }); }, };}Any component can then resolve it with this.use(HapticsKey) or
this.service(HapticsKey):
import { Component, defineEvent } from "@yagejs/core";import { HapticsKey } from "./haptics.js";
const PlayerHit = defineEvent("player:hit");
class HitFeedback extends Component { private readonly haptics = this.service(HapticsKey);
onAdd(): void { this.listen(this.entity, PlayerHit, () => this.haptics.pulse(0.08)); }}Don’t register game state this way. A service is not an entity, so queries,
scene.findByKey and the Inspector don’t see it. An engine-level service also
keeps its state when a scene restarts.
Scene-Scoped Services
Section titled “Scene-Scoped Services”context.register services live for the engine’s lifetime. A plugin that
keeps one instance per scene, as the physics world does, declares its key with
scope: "scene". It registers each scene’s instance with
scene.registerScoped(key, value) from a beforeEnter scene hook:
import { SceneHookRegistryKey, ServiceKey, type Plugin, type EngineContext } from "@yagejs/core";
/** Walkable cells of one scene's level. */export class NavGrid {}
export const NavGridKey = new ServiceKey<NavGrid>("navGrid", { scope: "scene" });
export function navPlugin(): Plugin { return { name: "nav", version: "1.0.0", install(context: EngineContext) { context.resolve(SceneHookRegistryKey).register({ beforeEnter: (scene) => { scene.registerScoped(NavGridKey, new NavGrid()); }, }); }, };}Components resolve it the same way as any other service (this.use(NavGridKey)).
Every key registered this way is automatically unregistered when the scene
exits (after onExit and plugin afterExit hooks run), so the next scene
starts with a clean slate and there’s no cross-scene leakage.
Per-scene game state, such as a level clock, needs no scoped service. A component on an entity the scene spawns is destroyed with the scene in the same way.
Resolving scene-scoped services from anything holding the scene
Section titled “Resolving scene-scoped services from anything holding the scene”Scene.use(key) and Scene.service(key) are scope-aware: they check scene
scope first, then engine scope — exactly like Component.use. Plugin
beforeEnter hooks run before onEnter, so a scene-scoped value is
already registered by the time onEnter fires. That means the obvious call
works in the obvious place — e.g. attaching a scene-wide effect or mask at
setup:
import { Scene } from "@yagejs/core";import { SceneRenderTreeKey } from "@yagejs/renderer";import { crt } from "@yagejs/effects";
class LevelScene extends Scene { readonly name = "level";
override onEnter() { // Resolves this scene's render tree — registered by RendererPlugin's // beforeEnter hook, which ran before onEnter. this.use(SceneRenderTreeKey).fx.addEffect(crt({ lineContrast: 0.4 })); }}Both are public, so any holder of a scene reference resolves the same way —
an entity’s setup(), a helper function that takes a scene, an addon:
import { Entity, RandomKey, Transform, Vec2 } from "@yagejs/core";
class Grass extends Entity { setup({ position }: { position: Vec2 }) { // RandomKey is scene-scoped, so every scene gets its own seeded stream. const rng = this.scene.use(RandomKey); this.add(new Transform({ position, rotation: rng.range(-0.2, 0.2) })); }}You don’t need SceneRenderTreeProviderKey for this — that key is for
cross-scene tooling such as the inspector and debug overlay, which enumerate
every scene’s tree. Game code reaches its own scene’s tree with
this.use(SceneRenderTreeKey).
For scene-scoped keys, resolve them inside onEnter() (or later) via
use() rather than a field initializer with service() — the scoped value
is recreated each time the scene is entered, and a field-level proxy would
cache the first one.
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.