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 = 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);
physics.applyImpulse(body, { x: 0, y: -600 });
}
}
}

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 = 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.