Appearance
Debugging
When a script misbehaves, the editor gives you four ways in, from quickest to deepest:
debug.log— print what the script sees, read it in the Console.- The script debugger — breakpoints, stepping, live variables and watch expressions in the Script Editor.
- Debug drawing & overlays — see rays, targets and script state floating in the 3D world itself.
room.log— the multiplayer channel, readable inside a live room.
If nothing is wrong but everything is slow, that is a different investigation — see the Profiler.
Logging with debug.log
debug.log, debug.warn and debug.error are the workhorses. Multiple arguments join with spaces and objects are JSON-stringified, and every line is tagged with the name of the entity whose script logged it:
js
class ClockPlatform extends Behaviour {
onStart() {
debug.log('[ClockPlatform] onStart roomTime=' + this.game.getRoomTime().toFixed(2) + ' moveY=' + this.moveY)
}
onCollisionEnter(other) {
debug.log('hit by', other.name, 'at', this.entity.position)
}
}Debug.Log, Debug.LogWarning and Debug.LogError are accepted as aliases and mean the same thing.
Where the lines go
| Runtime | Where debug.log lands |
|---|---|
| Editor play-test | The Console window — always on, nothing to enable. |
| Standalone build | The browser's developer console (F12), when the map's Debug Mode is on. |
| Published game | The in-game console (the backtick ` key), when the map's Debug Mode is on. |
| Multiplayer server scripts | Nowhere a player can see — gameplay scripts run on the server there. Use room.log instead. |
Debug Mode is a checkbox in Game Settings, off by default — so a shipped game never leaks your tracing to players. You can also flip it from a script at runtime:
js
onPlayerJoined(player) {
// Open the logs for your own live sessions only — an admin gate.
if (player.username === 'YourName') {
debug.setEnabled(true)
debug.log('debug logging is now', debug.enabled ? 'on' : 'off')
}
}One distinction worth knowing: console.log is the browser's own console and is not any of this — it ships everywhere, never appears in the game's Console panel, and is best left for quick throwaway checks.
The Console window
Open it from the Windows menu. Besides your debug.log lines it shows script errors — a script that throws in onUpdate or fails to compile reports here, with the entity and script named.
- The log / warn / error buttons filter by level; the text box filters by content.
- Collapse folds identical messages into one row with a count — the cure for a chatty
onUpdate. - Clear empties the list; its ▾ menu has Clear on Play to start every play-test fresh.
- Error Pause pauses the play-test the moment an error is logged, so the frame that broke is still on screen.
- Click a row to see the full text below the list — long messages wrap badly in the narrow rows.
Breakpoints and stepping
Printing tells you what happened; the debugger lets you stop time and look around. It works in the editor play-test (published games and standalone builds never run instrumented code).
Open the Script Editor and press the ◉ Debug button in the header — it replaces the script List sidebar with the Debug panel. Then:
- Click the gutter left of a line number to set a breakpoint (or press F9).
- Press Play. When that line is about to run, the whole simulation freezes on that exact frame — physics, particles, everything — and the editor jumps to the line.
- The Debug panel shows the call stack, the variables in scope (locals and the instance's fields), and your watch expressions. Hover a variable in the code to see its value in place.
While paused, the classic keys work:
| Key | Action |
|---|---|
| F5 | Continue — run until the next breakpoint |
| F10 | Step Over — run one statement |
| F11 | Step Into — descend into a method of the same script |
| Shift+F11 | Step Out — finish the current method and stop in the caller |
Stepping is real: locals update statement by statement, and a stepped assignment to the scene (moving an entity, say) takes effect live.
Conditional breakpoints and logpoints
Right-click a line in the Script Editor for Toggle Breakpoint (F9), Add Conditional Breakpoint / Logpoint… and Run to Line — the last one runs until execution reaches the line under the cursor, with no permanent breakpoint left behind.
A condition makes a breakpoint pause only when an expression is true. It evaluates in the line's scope with this bound to the script instance, so it reads like the script itself:
js
this.hp < 20 && other.name === 'Lava'A condition that throws pauses anyway — a typo in the condition should be seen, not silently skipped.
A logpoint is the opposite trade: it never pauses, it prints. Give the breakpoint a message and any {expr} parts evaluate in scope:
hp is {this.hp}, target {this.target?.name} at t={this.time.time.toFixed(2)}The line lands in the Console tagged with the script and line number. Logpoints are throttled, so one sitting in onUpdate reports steadily instead of flooding. This is the fastest way to trace a value over time without touching the script's source at all.
Live variables — no breakpoint needed
While the play-test runs, the Debug panel's Variables section shows the selected entity's script instances with their fields updating live — and the fields are editable. Type a new value and the running script sees it immediately. Watch expressions evaluate against the same live scope, so you can keep an eye on this.velocity.magnitude without a single log line.
Multiplayer maps
In a multiplayer room, gameplay scripts run on the server, out of the debugger's reach. But the editor play-test runs those same scripts locally — so debug the logic with breakpoints in the play-test first, then verify the room's behaviour with room.log, in a Test Online session or the published room. Multiplayer scripting covers what changes between the two worlds.
Seeing it in the world
Some bugs are spatial — a raycast aimed wrong, an entity you thought was somewhere else. For those, draw the debug into the scene.
Debug lines
js
onUpdate() {
// The ground-check ray, made visible: red for one frame, every frame.
const p = this.entity.position
debug.drawLine(p, [p[0], p[1] - 2, p[2]], '#ff0000', 0)
// Or with the C#-style aliases:
Debug.DrawRay(p, this.entity.forward, '#00ff00', 0)
}debug.drawLine(start, end, color, duration) draws a world-space line — color is a hex string, number or Color; duration is seconds, and 0 means one frame (redraw it each onUpdate for a persistent ray). Debug.DrawRay(origin, direction, color, duration) and Debug.DrawLine route to the same place. Lines draw in the editor and in standalone builds, gated exactly like the log sink.
The Debug Info overlay
Editor Settings → Debug Info (Play) → Show Debug Info puts a floating label above the player and every NPC during play, with toggles for position, rotation, scale and state (animation, grounded, velocity, hp).
Scripts can add their own line to that label:
js
onUpdate() {
this.entity.debugString = 'state: ' + this.state + ' hp: ' + this.hp
}Whatever you set shows above the entity while the overlay is on — the cheapest possible state display for AI and controllers. Set it to '' to clear.
Multiplayer: room.log
In a live room, debug.log deliberately stays quiet — server logs are not a broadcast channel. The channel that does reach players is room.log: it sends the line to every player's in-game console (the backtick ` key), tagged [room] with the logging entity's name.
js
onPlayerJoined(player) {
room.log(player.username + ' joined — round ' + this.round + ', ' + players.getAll().length + ' in room')
}
onInteract(player) {
// One player's business only — why their action was refused.
if (this.claimed) {
room.forPlayer(player.id).log('flag already taken by ' + this.claimedBy)
}
}room.forPlayer(id).log(...) aims the same line at a single player — refusals, per-player state, a value you are chasing for one reporter. In a busy room a broadcast line about one player is noise for everyone else.
Both are gated on the map's Debug Mode, and debug.setEnabled(true) opens them at runtime — the admin command pattern from above. In the editor play-test, [room] lines land in the Console panel, so the same script reads the same way before it is ever published.
A workflow that scales
- Reproduce in the play-test. Almost everything — including multiplayer gameplay logic — runs locally there, with the Console always on and the debugger available.
- Log first. One
debug.login the right lifecycle hook usually names the problem. Logpoints do the same without editing the script. - Break when logs disagree with you. A breakpoint with a condition on the bad value catches the exact frame; stepping shows which statement turns good state bad.
- Draw what you can't read. Rays, targets and
debugStringlabels make spatial bugs visible at a glance. - Then go live. Publish with Debug Mode on (or flip it with an admin command), read
room.login the backtick console, and turn it off when you ship.
For performance problems — frames, spikes, leaks — the Profiler is the tool, and it works in published games too via ?profile=1.