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
debugfalseEnable verbose logging and dev tools
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:

// 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:

// 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, SystemScheduler } from "@yagejs/core";
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 is optional. A minimal plugin can be just { name: "hello" }.

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

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 — the scheduler sorts systems by phase and priority.
  4. Loop startsrequestAnimationFrame begins ticking.
  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() in reverse plugin 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:

const uiPlugin: Plugin = {
name: "ui",
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.