Skip to content

Engine & Plugins

The Engine is the top-level object that owns the game loop, scene stack, and service container. Pass an options object to configure it:

import { Engine } from "@yagejs/core";
const engine = new Engine({
debug: true,
fixedTimestep: 1 / 60, // ~0.0167 s (default)
maxFixedStepsPerFrame: 5, // spiral-of-death protection
});
OptionDefaultPurpose
debugfalsePublish window.__yage__ for the browser console and test drivers. Its inspector comes from DebugPlugin or InspectorPlugin — see The Inspector
fixedTimestep1 / 60Seconds per fixed-update tick
maxFixedStepsPerFrame5Cap on fixed-update iterations per frame

Errors thrown inside code the engine calls on your behalf are reported and rethrown — see Errors.

The three-step lifecycle is intentionally simple:

import { RendererPlugin } from "@yagejs/renderer";
import { PhysicsPlugin } from "@yagejs/physics";
import { InputPlugin } from "@yagejs/input";
// 1. Register plugins
engine.use(new RendererPlugin({ width: 800, height: 600, container: document.getElementById("game")! }));
engine.use(new PhysicsPlugin());
engine.use(new InputPlugin());
// 2. Start the loop
await engine.start();
// 3. Tear down when done
engine.destroy();

engine.use() accepts plugins in any order — the engine resolves the correct initialization order automatically. engine.start() is async because some plugins need to load external resources (e.g. the Rapier WASM binary).

destroy() is permanent. Afterwards start() and use() throw, and further destroy() calls do nothing, so a host that tears down defensively (hot reload, a component unmounting) can call it without tracking whether it already did. Create a new Engine when you need to run again: plugins release resources they cannot rebuild, so the same instance cannot be restarted.

A start() that rejects is terminal in the same way. Plugins installed before the failure already hold their services, and the container refuses to register those twice, so a retry would fail on the duplicate rather than on the original problem. Call destroy() to release what did install, then build a new engine.

Calling destroy() while start() is still awaiting a plugin cancels the rest of startup — the loop never starts and no further plugin hook runs.

Restarting the game is a different operation, and the engine stays up for it. Reset the scene stack instead:

import { Scene } from "@yagejs/core";
class GameScene extends Scene {
readonly name = "game";
}
class MenuScene extends Scene {
readonly name = "menu";
}
// Restart the level
await engine.scenes.replace(new GameScene());
// Or drop the whole stack and start from the menu
await engine.scenes.popAll();
await engine.scenes.push(new MenuScene());

Scene changes requested after destroy() are ignored. In development builds the scene manager logs a warning naming the call it dropped.

A plugin is a plain object that implements the Plugin interface:

import {
Plugin,
EngineContext,
Phase,
ServiceKey,
System,
SystemScheduler,
} from "@yagejs/core";
class MyService {
spritesDrawn = 0;
}
const MyServiceKey = new ServiceKey<MyService>("my-plugin:service");
class MySpriteSystem extends System {
readonly phase = Phase.Render;
update(): void {
// draw the plugin's sprites and count them on MyService
}
}
const myPlugin: Plugin = {
name: "my-plugin",
version: "1.0.0",
dependencies: ["renderer"], // other plugin names this depends on
install(context: EngineContext) {
// Register services into the DI container
context.register(MyServiceKey, new MyService());
},
registerSystems(scheduler: SystemScheduler) {
// Add systems to the game loop
scheduler.add(new MySpriteSystem());
},
onStart() {
// Called after the loop has started and all plugins are wired
},
onDestroy() {
// Cleanup: dispose GPU resources, close connections, etc.
},
};

Every field except name and version is optional. A minimal plugin is { name: "hello", version: "1.0.0" }.

The scheduler also reports where the current call is executing, for plugin code that can be reached from more than one phase:

import { SystemSchedulerKey } from "@yagejs/core";
const scheduler = context.resolve(SystemSchedulerKey);
scheduler.currentPhase; // the Phase running right now, or null outside any phase
scheduler.fixedStepIndex; // monotonic count of fixed steps started — a frame can
// run several fixed steps, or none

@yagejs/input reads these to scope its edge queries to the caller’s frame or fixed step; any plugin whose behavior depends on its execution context can branch on them the same way.

When engine.start() is called the engine processes plugins in this sequence:

  1. install(context) — register services and configuration into the DI container. Runs for every plugin before any systems are added.
  2. registerSystems(scheduler) — add systems to the scheduler. All services from step 1 are available.
  3. System wiring — every scheduler.add() keeps its phase sorted by priority, at startup and at runtime.
  4. Loop starts — frames begin to run. RendererPlugin connects the loop to the PixiJS application’s ticker, so with a renderer the ticker drives each frame. Without a renderer, the loop requests its own frames with requestAnimationFrame in a browser.
  5. onStart() — called once the first frame is queued. Safe to spawn entities, push scenes, and interact with the full engine.
  6. Game runs — the loop calls systems each frame.
  7. onDestroy() — called by engine.destroy() for every plugin whose install() was called, in reverse install order.

Built-in plugins accept configuration via their constructor:

import { RendererPlugin } from "@yagejs/renderer";
import { PhysicsPlugin } from "@yagejs/physics";
engine.use(
new RendererPlugin({
width: 1280,
height: 720,
backgroundColor: 0x1a1a2e,
container: document.getElementById("game")!,
}),
);
engine.use(
new PhysicsPlugin({
gravity: { x: 0, y: 980 },
}),
);

Plugins declare dependencies by name. The engine performs a topological sort before running the lifecycle so that a plugin’s dependencies are always installed first:

import type { Plugin } from "@yagejs/core";
const uiPlugin: Plugin = {
name: "ui",
version: "1.0.0",
dependencies: ["renderer", "input"],
// ...
};

If a dependency is missing the engine throws at startup with a clear message listing the unresolved names. Circular dependencies are also detected and reported.

Install order is not registration order. Plugins that declare no dependencies install first, in the order they were registered. A plugin with dependencies installs after all of them, even when you registered it first. So DebugPlugin, which depends on the renderer, installs after InputPlugin and PhysicsPlugin wherever you register it.

A plugin that uses a service another plugin provides, without depending on it, resolves that service in onStart rather than install: by then every plugin has installed. RendererPlugin publishes its render facet to the Inspector this way, since the Inspector usually comes from DebugPlugin.

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.