Debug Console
DebugConsole installs a namespace on window, sage by default, and exposes values, dynamic getters, and callable commands onto it. registerBuiltins wires a GameEngine's commands onto it at boot. Open the browser's developer console, type sage.list(), and the running simulation's entities print as a table.
Configuration
The console is enabled by default and configured through SageOptions.debug:
| Value | Effect |
|---|---|
| omitted | Enabled as window.sage |
{ namespace: 'myGame' } | Enabled as window.myGame |
false | Disabled. engine.debug.expose becomes a no-op. |
const engine = await createGameEngine(canvas, entityClasses, { debug: { namespace: 'myGame' } });Installing over an occupied window key throws, naming the key and pointing at the debug.namespace option. setNamespace() checks its new name before tearing the current namespace down, so a rejected rename leaves the console where it was.
The namespace is removed from window when the engine stops.
Built-in references
| Property | What it is |
|---|---|
sage.engine | The GameEngine |
sage.events | The engine's library bus, engine.eventBus |
sage.physics | The HavokPlugin |
sage.colliders | The ColliderDebugManager |
sage.level | The current level. A dynamic getter; it follows level transitions. |
sage.scene | The current level's scene, or null |
sage.entities | The entities view, indexed by ID |
The entities view
sage.entities is a view onto the current simulation's entities, indexed by ID. sage.entities['entity-17'] answers with the GameEntity that ID names, the same way getEntity does. An ID nothing names logs a warning and answers undefined. The view carries no other property and wraps no manager.
An entity the view answers with is the entity itself, not a copy. Reading sage.entities['entity-17'].state reads its actual state bag, and sage.entities['entity-17'].ops.unlock('1234') calls its actual operation.
Built-in commands
Entities
| Command | Does |
|---|---|
sage.list() | Every entity in the simulation, as a console.table |
sage.list('EnemyEntity') | Every entity whose class-table name matches |
sage.list(undefined, 'hostile') | Every entity carrying the tag |
sage.list('EnemyEntity', 'hostile') | Both filters together |
sage.spawn('EnemyEntity', x, y, z) | Spawns the class through the simulation with position in its initial state |
sage.inspect(idOrEntity) | Logs the entity's id, state, and node name and position |
sage.tp(idOrEntity, x, y, z) | Writes the entity's node position |
list prints one row per entity, with id, name, and node (the node's Babylon name, or (no node) for a GameEntity with none), plus displayName for an entity carrying NamedBehavior. It also returns the entities, so sage.list('EnemyEntity')[0] is the first match.
tp writes the node's position directly. For an entity that built its own node, NodeEntity.update writes state.position onto the node every fixed step, so the teleport lasts until the next step: it sticks while the game is paused and is undone once stepping resumes. To move such an entity from the console, write its state instead:
sage.entities['entity-3'].state.position = { x: 0, y: 5, z: 0 }An adopted node is never written from state, so tp on an adopted entity sticks.
Events
| Command | Does |
|---|---|
sage.logEvents('level:*') | Logs every matching event on the library bus. Returns an unsubscribe function. |
sage.logEvents() | Logs everything on the library bus |
sage.stopLogging() | Stops every active logger |
logEvents subscribes to engine.eventBus, the library bus that carries level, asset, trigger, and game events. Entity events and actions travel on the simulation bus and do not appear here. To watch those, register a subscriber on sage.engine.simulation.bus; see Events.
The loop
| Command | Does |
|---|---|
sage.pause() | Pauses the game manager: fixed steps and frame callbacks stop, rendering continues |
sage.resume() | Resumes it |
sage.slow(factor) | Sets the render engine's time step to (1 / 60) / factor |
Writing state from the console
A console line is plain JavaScript against the live object, so this runs:
sage.entities['entity-17'].state.health = 100Inside a game's own source, an un-narrowed GameEntity's state is typed object and a named field is not reachable. That check belongs to the compiler, and there is no runtime guard behind it: nothing checks a write while the simulation runs.
What such a write means:
- It notifies nobody. State is facts, and writing a field notifies nobody, whoever writes it.
- It is snapshot-visible. A snapshot deep-copies whatever a state bag holds at the fixed-step boundary it is taken on, with nothing recorded about how a field came to hold what it holds.
- It is replay-invisible. A replay runs fixed steps, and a console write is not part of one. A replay from a snapshot taken before the write never meets it.
Operations remain the way a game's own logic changes another entity's state. A write straight to .state from the console reaches past that, on purpose.
Registering game-specific commands
engine.debug.expose(name, value) registers a value, a callable command, or a dynamic getter on the namespace:
engine.debug.expose('gameState', gameState);
engine.debug.expose('godmode', () =>
{
const player = engine.simulation?.getEntity(playerID);
if(player instanceof PlayerEntity)
{
player.state.invincible = true;
console.log('[game] God mode enabled');
}
});
engine.debug.expose('player', {
get: () => engine.simulation?.getEntity(playerID),
});A dynamic getter resolves on every access, so sage.player follows a respawn. The onStart hook, after levels and entities exist, is the natural place to register:
engine.onStart(async (ge) =>
{
ge.debug.expose('reloadLevel', async () =>
{
await ge.managers.levelManager.transition('arena');
});
});A session
sage.list() // what is in the scene
sage.list('DoorEntity') // the doors
sage.entities['entity-4'].state // one door's state bag
sage.entities['entity-4'].ops.unlock('1234') // call its operation
sage.entities['entity-4'].showCollider('#ff0000') // see its collider
sage.slow(0.25) // watch it slowly
sage.logEvents('level:*') // watch the level bus
sage.slow(1)
sage.stopLogging()
sage.entities['entity-4'].hideCollider()Related pages
- Collider Debugging: per-entity and config-driven collider visualization
- Events: the two buses
logEventsand the simulation subscribe to - Entities: state, operations, and cross-entity access
