Skip to content

Testing and debugging ​

Testing in Opal is fast because Play mode runs the real game inside the editor. Use short edit-test loops: change one thing, press Play, inspect what happened, then adjust. Every tool on this page is built for that loop.

Play modes ​

ControlUseWhen to reach for it
Play in viewportFast iteration while keeping editor panels nearbyDefault — 90% of tests
Fullscreen playTest framing, mobile focus, and player feelBefore publishing, or when layout looks off
StopReturn to editingAfter every play
StatsFPS, draw calls, physics, runtime valuesWhen it stutters or state looks wrong
Flow overlayWatch graph nodes light up while playingWhen logic doesn't fire

Play mode runs input, physics, flow graphs, animation, components, audio, scripts, and scene changes — exactly as the published player will.

Starting Play flushes pending scene, script, and project edits. If the scene is already saved, Play uses it without creating another save checkpoint.

Start scene testing ​

By default, play starts from the project's start scene. This is correct for testing the complete game flow.

When debugging one scene:

  1. Temporarily set it as the start scene, or use scene-specific test controls where available.
  2. Keep initialization logic in Scene Flow so tests behave like the real game.
  3. Restore the real start scene before publishing.

Keep a Boot scene

For games with 3+ levels, make the start scene a tiny Boot that sets score = 0, lives = 3, then Go To Scene → Main Menu. That way every Play run resets cleanly and you never publish with a half-initialized session.

Flow overlay — see your logic run ​

The Flow overlay paints execution on top of the running game. Toggle Flow in the play overlay bar.

Use it when:

  • A tap does nothing.
  • A branch takes the wrong path.
  • A timer fires too often or not at all.
  • A custom event is emitted but no listener responds.
  • A component action runs on the wrong object.

How to read it:

  • A node that never lights up — the event or the previous white wire never reached it. Check the trigger, the wire, and whether Play is running.
  • A node that lights up but the result is wrong — execution reached it, but the data is wrong. Inspect its fields, = expressions, and target object.
  • A node that fires twice — two rules match the same event, or a timer repeats without a guard.
Debug me — try the playable and watch Flow
Recipe 1 · Tap not firing2 min debug

Symptom: coin never disappears, score never rises.

  1. Press Play in viewport, open Flow overlay, tap the coin.
  2. If On Tap never flashes: select the coin → Inspector → confirm Interactable Receives taps is on and no larger Interactable with Block taps behind on covers it. Check Problems for missing asset / broken wire / invalid target.
  3. If On Tap flashes but Add Counter never flashes: execution wire is severed between them — re-drag it.
  4. If both flash but score stays 0: open Variables → confirm score is a Number GameState var (capitalization matters). Check Add Counter (catalog) / Add to variable (registry) targets score, not Score.

Fix: re-wire, enable Interactable, or re-add the variable. One-line symptom → one-line fix.

Recipe 2 · Branch goes the wrong way3 min debug

Symptom: If score ≥ 5 → Win always takes the false arm even when the label says Score 7.

  1. With Flow overlay on, watch the condition node — does the true pin ever pulse?
  2. Open the condition's inspector. Is it reading a leftover UI Expression (self.score) where Flow expects Get Value / Variable at least, or =GameState.score? See Variables and GameState.
  3. Add a temporary Say → ="score is {score}", or bind a debug Text label with Data bindings → GameState → score, to print the live value while playing.

Fix: swap the bad expression for a Get Variable node. Remove the debug Say.

Recipe 3 · Collision never fires3 min debug

Symptom: player walks through a hazard, no damage.

  1. Flow overlay: does On Collision flash? If not, select both objects → Inspector → confirm Collider 2D exists on each, the hazard is not a sensor when you want a blocker (or is a sensor when you want Sensor overlap), and bounds actually overlap on the stage.
  2. Moving object needs Rigid Body 2D (Dynamic or Kinematic). Static décor needs none.
  3. If it's a Projectile that never welds: enable Continuous Collision on the body, wait past Arm Delay, and check Ignore Owner isn't filtering the hit.

See Physics and collisions for the full tuning table.

Fixing script errors ​

Script Studio highlights the exact name that needs attention. For example, Debug.log("ready") highlights log and offers Change 'log' to 'Log'. Corrections appear beside the problem in Script Output and through Monaco's Quick Fix menu. Suggestions use existing names and must pass the compiler; ambiguous names or code with other errors may need a manual edit.

Click a problem to select its complete source range. Click the status-bar problem count, or use F8 / Shift+F8, to move through located problems. The bottom Problems panel also opens compiler errors at their original source ranges and includes conflicts between scripts, such as duplicate component IDs.

A correction edits your current draft. Undo reverses just that correction, and Redo restores it. Moving focus to the problem list or another Studio control does not save or activate unfinished code. Use Save & Apply when ready; switching scripts or leaving the workspace still saves the draft as before. If the script or its project bindings change while a fix is open, Opal checks again instead of applying an outdated suggestion. A failed build keeps the previous working revision active.

Console — messages from your scripts ​

Open Console in the bottom shelf, or press Alt+4. OpalScript exposes a Unity-style Debug API:

csharp
class PlayerDebug : Component {
    void Start() {
        Debug.Log("Player ready");
        Debug.Log(Transform.Position);
        Debug.LogWarning("Using a default spawn point");
        Debug.Assert(GetComponent<Health>() != null, "Add Health to this player");
    }
}

Use Debug.LogError("message") for a failure. Error logs and failed assertions do not throw: code after the call still runs. Use an explicit if and return when you need to stop a method. A true assertion writes nothing. Both assertion arguments are evaluated normally.

You can log numbers, booleans, vectors, records, collections, and object or asset handles directly. Messages capture a detached snapshot when the call runs, so later edits do not change old output. Large/deep values are visibly abbreviated. Each script message records its object, script, method, scene, exact source location, and source revision. Logging also works in LocalInput and asynchronous callbacks, but is not allowed in pure/computed functions.

  • Search matches message text and source/object details. Combine it with the severity and source filters to isolate a script or an error.
  • Collapse groups identical messages from the same source location and object. Turn it off to inspect each retained occurrence in order.
  • Select a row to read the full message. Open its source to go to Script Studio, or its object to inspect the authored counterpart when available. Logs keep their captured information after an object disappears.
  • Follow keeps the newest messages in view. Scroll back to read older output without being pulled down by new messages; enable Follow to catch up.
  • Copy copies the selected message with its context. Export visible logs downloads the filtered view as text for a bug report.
  • Options → Clear on Play starts each run with a clean console. Turn it off to compare runs; Stop and scene transitions preserve output.
  • Options → Pause on error shares the Script Studio debugger's setting. An error pauses Play at a safe execution boundary and opens its source in the debugger. It does not interrupt a method halfway through a mutation.

The Console retains the most recent 2,000 messages and shows how many older messages were discarded. This bounds editor memory without stopping script execution. Exported games write Debug messages to the browser's developer console, with source information, and continue running after error logs.

See the Debug API for each method's declaration and examples.

Problems panel ​

The Problems dock lists live validation warnings. Treat it as a pre-publish gate — a warning you ignore in the editor becomes a player-facing bug.

Common warnings:

  • Missing asset reference
  • Invalid graph connection / broken wire
  • Broken prefab or component data
  • Missing target scene (Go To Scene: —)
  • Duplicate scene names (transition becomes ambiguous — rename one)
  • Project scene records unreadable
  • Missing required component (e.g. Health Bar without Health)
  • A Flow node the runtime cannot honour: an action on Self in a scene graph whose event has none (On Scene Start, On Key Down), Target on an event that carries no target, an object id that is no longer in the scene, a component action on an object without that component, a Spawn Object with no prefab, an unknown action type
  • During Play, the same misses as they happen ("Say: nothing to act on — On Scene Start has no Self here"), reported once per node; a shipped game prints them to the console

Click a row to jump to the offending object, graph, or field. Fix, then re-check — Problems re-validates as you type.

Graph Explorer — when the hierarchy isn't enough ​

Open the workbench Graph dock → Graph Explorer. It's a read-only map of relationships, not an editor. Use it when the hierarchy is correct but the connections are not.

Choose a mode with View and narrow it with search or Filters. Enter/Space selects a focused node; Ctrl/Cmd+Enter opens its source. The dock's maximize button gives the map more room without changing your saved layout.

Question you haveGraph to open
How is this scene parented?Scene hierarchy
What does this prefab contain?Prefab anatomy
What events can fire, and who listens?Event map
Which assets will break if I delete this image?Asset dependencies
Which systems talk to this component?Component map
What just ran, in order?Runtime trace

Walkthrough — trace a score bug:

  1. Open Runtime Trace after a Play run that felt wrong — it shows which Flow nodes and edges fired (counts, last value, last path). Use it to see whether your Add Counter / On Tap nodes actually ran, not as a GameState write log.
  2. Switch to Asset dependencies → select coin.png → see every scene and .ssb that depends on it before you replace it.
  3. Switch to Event map → search coin_collected → see the listeners across Scene Flow and Object Flow and why only one fired.

See Graph Explorer for every mode and filter.

Stats — performance + state at a glance ​

Toggle Stats in the play overlay.

  • FPS / frame ms — under 16 ms is 60 fps. Spikes usually mean physics sync or too many dynamic bodies.
  • Draw calls / texture swaps — health bars are render2d so they don't split WebGL batches; sprite spam does.
  • Physics bodies / contacts — crater when you spawn 50 Dynamic boxes at once.

No tuning blindly — change one thing, measure, undo with Ctrl+Z.

Debugging checklist ​

When anything does not work, run this in order:

  1. Reproduce it in Play in viewport.
  2. Confirm the correct scene is running (start scene vs isolated test scene).
  3. Confirm the object name is right — Flow, UI bindings, and Graph Explorer all use names.
  4. Check the inspector for missing components or disabled interaction (Interactable, enabled).
  5. Use Flow overlay to see whether the event fired and which white wire it followed.
  6. Inspect variables / component fields / expressions for typos, wrong scope (self vs get("player")), or literal-vs-expression mode.
  7. Check the Problems panel.
  8. Open Graph Explorer when the bug is cross-scene or cross-system.
  9. Export a .opal backup before a risky fix.

Scripts — tests, Tasks, Play review ​

OpalScript has its own Play-time tools. They sit next to Flow overlay, not instead of it.

  • [Test] methods in Script Studio — public bool for a fast check, public async Task<bool> for a disposable gameplay fixture.
  • Tasks panel — waits, parent/task identity, cancellation reasons.
  • Apply selected and Stop — copy reviewed [Property] fields back as one undoable authoring command. Private fields and unselected Properties stay in the play session.

Frame budgets, packages, and replay limits: tests, packages, debugger.

Browser dev tools — for hard bugs ​

Opal is browser-native, so the browser helps too.

  • Console — script errors, failed asset loads, network/storage messages, Flow warnings. Filter opal to quiet noise.
  • Network — confirm images, sounds, and other asset URLs actually load.

Search tip: press Ctrl+K (or Cmd+K on Mac) from any docs page and type the exact warning text — blocksBehind, missing badge, Arm Delay, Continuous Collision — to jump straight to the fix.