Skip to content

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:

ValueEffect
omittedEnabled as window.sage
{ namespace: 'myGame' }Enabled as window.myGame
falseDisabled. engine.debug.expose becomes a no-op.
typescript
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

PropertyWhat it is
sage.engineThe GameEngine
sage.eventsThe engine's library bus, engine.eventBus
sage.physicsThe HavokPlugin
sage.collidersThe ColliderDebugManager
sage.levelThe current level. A dynamic getter; it follows level transitions.
sage.sceneThe current level's scene, or null
sage.entitiesThe 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

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

javascript
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

CommandDoes
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

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

javascript
sage.entities['entity-17'].state.health = 100

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

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

typescript
engine.onStart(async (ge) =>
{
    ge.debug.expose('reloadLevel', async () =>
    {
        await ge.managers.levelManager.transition('arena');
    });
});

A session

javascript
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()
  • Collider Debugging: per-entity and config-driven collider visualization
  • Events: the two buses logEvents and the simulation subscribe to
  • Entities: state, operations, and cross-entity access

Released under the MIT License.