Engine & Plugins
Creating an Engine
Section titled “Creating an Engine”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});| Option | Default | Purpose |
|---|---|---|
debug | false | Enable verbose logging and dev tools |
fixedTimestep | 1 / 60 | Seconds per fixed-update tick |
maxFixedStepsPerFrame | 5 | Cap on fixed-update iterations per frame |
Errors thrown inside code the engine calls on your behalf are reported and rethrown — see Errors.
Engine Lifecycle
Section titled “Engine Lifecycle”The three-step lifecycle is intentionally simple:
// 1. Register pluginsengine.use(new RendererPlugin({ width: 800, height: 600, container: document.getElementById("game")! }));engine.use(new PhysicsPlugin());engine.use(new InputPlugin());
// 2. Start the loopawait engine.start();
// 3. Tear down when doneengine.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).
An engine runs once
Section titled “An engine runs once”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 levelawait engine.scenes.replace(new GameScene());
// Or drop the whole stack and start from the menuawait 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.
The Plugin Interface
Section titled “The Plugin Interface”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 phasescheduler.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.
Plugin Lifecycle Order
Section titled “Plugin Lifecycle Order”When engine.start() is called the engine processes plugins in this sequence:
install(context)— register services and configuration into the DI container. Runs for every plugin before any systems are added.registerSystems(scheduler)— add systems to the scheduler. All services from step 1 are available.- System wiring — the scheduler sorts systems by phase and priority.
- Loop starts —
requestAnimationFramebegins ticking. onStart()— called once the first frame is queued. Safe to spawn entities, push scenes, and interact with the full engine.- Game runs — the loop calls systems each frame.
onDestroy()— called byengine.destroy()in reverse plugin order.
Plugin Configuration
Section titled “Plugin Configuration”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 }, }),);Dependency Resolution
Section titled “Dependency Resolution”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.