Debug Tools
The @yagejs/debug package provides a toggleable debug overlay with physics
shape visualization, HUD text, performance stats, and a contributor system
for custom debug views.
DebugPlugin Setup
Section titled “DebugPlugin Setup”import { DebugPlugin } from "@yagejs/debug";
engine.use(new DebugPlugin({ startEnabled: true, // show debug overlay on launch toggleKey: "Backquote", // key to toggle (default: backtick `)}));Press the toggle key (` by default) to show/hide the debug overlay at
runtime.
DebugPlugin Config
Section titled “DebugPlugin Config”| Property | Type | Default | Description |
|---|---|---|---|
startEnabled | boolean | false | Show overlay on launch |
toggleKey | string | "Backquote" | Key code to toggle overlay |
manualClock | boolean | false | Enable frame-step mode |
stepKey | string | "Period" | Key to advance one frame (manual clock) |
maxGraphics | number | 256 | Graphics object pool size |
maxHudLines | number | 32 | Max HUD text lines |
flags | Record<string, boolean> | — | Initial flag overrides |
Built-In Debug Views
Section titled “Built-In Debug Views”When the debug overlay is active, the plugin automatically shows:
- Physics colliders — colored outlines around all collider shapes:
- Green: dynamic bodies
- Gray: static bodies
- Blue: kinematic bodies
- Yellow: sensor colliders
- FPS counter — frames per second in the HUD
- Entity count — total entities in the current scene
- System timing — per-system execution time breakdown
Custom Debug Contributors
Section titled “Custom Debug Contributors”Implement DebugContributor to add your own debug visualizations:
import type { DebugContributor, WorldDebugApi, HudDebugApi } from "@yagejs/debug/api";
class WallDebugContributor implements DebugContributor { readonly name = "walls"; readonly flags = ["show-walls"] as const;
private readonly shapes: Array<{ x: number; y: number; w: number; h: number }>;
constructor(shapes: Array<{ x: number; y: number; w: number; h: number }>) { this.shapes = shapes; }
drawWorld(api: WorldDebugApi): void { if (!api.isFlagEnabled("show-walls")) return;
for (const s of this.shapes) { const g = api.acquireGraphics(); if (!g) break; g.rect(s.x, s.y, s.w, s.h) .stroke({ color: 0xff0000, width: 2 / api.cameraZoom }); } }
drawHud(api: HudDebugApi): void { api.addLine(`Walls: ${this.shapes.length}`); }}Register it in your scene:
import { DebugRegistryKey } from "@yagejs/debug/api";
onEnter(): void { const registry = this.service(DebugRegistryKey); registry.register(new WallDebugContributor(this.wallShapes));}WorldDebugApi
Section titled “WorldDebugApi”| Method | Description |
|---|---|
acquireGraphics() | Get a pooled Graphics object (returns undefined if pool exhausted) |
cameraZoom | Current camera zoom level (scale line widths by 1/cameraZoom) |
isFlagEnabled(flag) | Check if a debug flag is active |
HudDebugApi
Section titled “HudDebugApi”| Method | Description |
|---|---|
addLine(text) | Add a line to the HUD text overlay |
isFlagEnabled(flag) | Check if a debug flag is active |
screenWidth / screenHeight | Screen dimensions for custom positioning |
Debug Flags
Section titled “Debug Flags”Contributors can declare flags for toggling specific views:
class MyContributor implements DebugContributor { readonly name = "my-debug"; readonly flags = ["show-paths", "show-hitboxes"] as const;
drawWorld(api: WorldDebugApi): void { if (api.isFlagEnabled("show-paths")) { // draw paths... } if (api.isFlagEnabled("show-hitboxes")) { // draw hitboxes... } }}Set flags programmatically:
registry.setFlag("my-debug", "show-paths", true);StatsStore
Section titled “StatsStore”StatsStore provides rolling-window statistics for performance monitoring:
import { StatsStore } from "@yagejs/debug";
const stats = new StatsStore();
// Push samples each framestats.push("updateTime", performance.now() - start);
// Read statsstats.average("updateTime"); // rolling averagestats.latest("updateTime"); // most recent sampleFrame-Step Mode
Section titled “Frame-Step Mode”Enable manualClock in the config to pause the game loop and step one frame
at a time using the step key. This is useful for debugging physics, animations,
or frame-specific logic.
new DebugPlugin({ manualClock: true, stepKey: "Period" })Logging
Section titled “Logging”The Logger class is a category-tagged, ring-buffered logger built into the
engine. Unlike the debug overlays above, it lives in @yagejs/core, not
@yagejs/debug — you don’t need the DebugPlugin installed to use it. Every
Engine has a Logger attached at construction time, and the game loop
auto-updates the logger’s frame counter so every log entry records the frame
it was emitted on.
Access it as engine.logger, or through DI via LoggerKey:
import { Logger, LogLevel, LoggerKey } from "@yagejs/core";
// Direct access on the engineengine.logger.info("physics", "Shape spawned", { x: 100, y: 200 });
// From inside a System or Componentclass SpawnSystem extends System { private logger!: Logger;
init(context: EngineContext) { this.logger = context.resolve(LoggerKey); }
update(dt: number) { if (wave.complete) { this.logger.warn("gameplay", "Wave ended with no kills"); } }}Log Methods
Section titled “Log Methods”All four methods take the same shape: (category, message, data?). The
category is a free-form string — use whatever taxonomy suits your game
("physics", "ai", "input", "gameplay"). data is an arbitrary object
that gets attached to the entry for later inspection.
| Method | Level | Typical use |
|---|---|---|
logger.debug(cat, msg, data?) | Debug | Chatter that only matters when actively diagnosing |
logger.info(cat, msg, data?) | Info | Normal lifecycle events (scene change, entity spawn) |
logger.warn(cat, msg, data?) | Warn | Recoverable problems (missing texture, late frame) |
logger.error(cat, msg, data?) | Error | Broken invariants — something a human should look at |
LoggerConfig
Section titled “LoggerConfig”Pass a logger option when constructing the engine to configure level,
category filter, ring buffer size, or a custom output sink:
import { Engine, LogLevel } from "@yagejs/core";
const engine = new Engine({ logger: { level: LogLevel.Info, // drop anything below Info categories: ["physics", "ai"], // only accept these categories (empty = all) bufferSize: 1000, // keep the last 1000 entries output: (entry) => { // optional: override console output console.log(`[${entry.category}] ${entry.message}`, entry.data); }, },});LogLevel is an enum: Debug (0), Info (1), Warn (2), Error (3),
None (4). Anything below the configured level is silently dropped at the
log site — entries never enter the ring buffer, so there’s no cost to leaving
debug calls in shipping code with level: LogLevel.Info.
Inspecting the Ring Buffer
Section titled “Inspecting the Ring Buffer”The ring buffer is what makes the logger useful beyond console.log. Each
entry is a LogEntry carrying level, category, message, optional
data, a timestamp, and the frame number the game loop was on when the
entry was emitted. You can pull recent entries at any time:
// Grab the last N entries (default: everything in the buffer)const recent: LogEntry[] = engine.logger.getRecent(20);
// Or pre-formatted as a string, handy for crash dumpsconst dump: string = engine.logger.formatRecentLogs(50);console.log(dump);A common pattern is to catch an error, dump the recent logs, and ship the result to a crash reporter:
window.addEventListener("error", (ev) => { reportCrash({ error: ev.error, recentLogs: engine.logger.formatRecentLogs(100), });});Because every entry carries a frame number, you can correlate log output
with specific frames seen in the debug overlay or inspector — very useful for
reproducing frame-specific bugs.
Custom Output Sinks
Section titled “Custom Output Sinks”The output option replaces the default console.* handler. Use it to ship
logs somewhere else — an in-game debug panel, a remote telemetry service, or
a file (in Electron/Tauri builds):
const engine = new Engine({ logger: { output: (entry) => { // Forward warnings and errors to a remote logging backend if (entry.level >= LogLevel.Warn) { telemetry.send("game-log", { level: LogLevel[entry.level], category: entry.category, message: entry.message, data: entry.data, frame: entry.frame, timestamp: entry.timestamp, }); }
// Also mirror to the console in dev if (import.meta.env.DEV) { console.log(`[${entry.category}]`, entry.message, entry.data); } }, },});Custom sinks don’t replace the ring buffer — entries are still stored
in-memory and accessible via getRecent() regardless of what the sink does.