Skip to content

Dependency Injection

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.

import { EngineContext, ServiceKey } from "@yagejs/core";

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";
// Define a key with a descriptive label
const AudioMixerKey = new ServiceKey<AudioMixer>("AudioMixer");

Never use plain strings for service resolution — always define a ServiceKey.

// 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 mixer = context.tryResolve(AudioMixerKey);
// Check existence
if (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.

YAGE’s core and official plugins register these services:

KeyTypePackage
EngineKeyEngine@yagejs/core
EventBusKeyEventBus@yagejs/core
SceneManagerKeySceneManager@yagejs/core
LoggerKeyLogger@yagejs/core
QueryCacheKeyQueryCache@yagejs/core
ErrorBoundaryKeyErrorBoundary@yagejs/core
GameLoopKeyGameLoop@yagejs/core
InspectorKeyInspector@yagejs/core
SystemSchedulerKeySystemScheduler@yagejs/core
ProcessSystemKeyProcessSystem@yagejs/core
AssetManagerKeyAssetManager@yagejs/core

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 } 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(RigidBody);
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.

Systems receive the EngineContext in onRegister. Store references there or call this.use(key) during update:

import { System, Phase, EngineContext } 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 stats = this.use(GameLoopKey).stats;
this.logger.debug(`FPS: ${stats.fps}`);
}
}

Register your own services for shared game state:

import { ServiceKey, Plugin, EngineContext } from "@yagejs/core";
interface Inventory {
items: string[];
add(item: string): void;
has(item: string): boolean;
}
export const InventoryKey = new ServiceKey<Inventory>("Inventory");
export function inventoryPlugin(): Plugin {
return {
name: "inventory",
install(context: EngineContext) {
context.register(InventoryKey, {
items: [],
add(item) { this.items.push(item); },
has(item) { return this.items.includes(item); },
});
},
};
}

Any component or system can then this.use(InventoryKey) to access the shared inventory.

context.register services live for the engine’s lifetime. For state that belongs to a single scene, call scene.registerScoped(key, value) — plugins typically do this from their beforeEnter hook, but game code can use it directly to attach scene-local services:

import { Scene, ServiceKey } from "@yagejs/core";
const LevelClockKey = new ServiceKey<{ elapsed: number }>("LevelClock");
class LevelScene extends Scene {
override onEnter() {
this.registerScoped(LevelClockKey, { elapsed: 0 });
}
}

Components resolve it the same way as any other service (this.use(LevelClockKey)). 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.