Remote debugging for Godot 4 games — a pure GDScript addon that connects your running game to OmniDebugLink, so AI tools can inspect the live scene tree, inject real input and key events, and capture screenshots on any export platform.
The Godot client is a zero-dependency GDScript addon: WebSocketPeer, JSON and image handling are all engine built-ins, so there is nothing to resolve, no native code and no threads. Drop the addon into a project, start it with a device token, and a game running on desktop, mobile or web exports becomes inspectable and drivable from your AI coding tool — traverse the scene tree, read logs, tap buttons, type text, fire InputMap actions, even inject hardware-level key events like Escape.
No third-party packages, no native plugins, no C# required. The addon is plain GDScript source you can read in one sitting, and it ships under the MIT license.
Desktop, mobile and web exports all run the same addon — the connection, heartbeat and reconnect logic never forks per platform.
Everything pumps from _process on the main thread, so task handlers can safely touch any Godot API — and handlers may await frames or timers without blocking other tasks.
Injection goes through the real event pipeline (Viewport.push_input / Input.parse_input_event), so GUI, _unhandled_input and InputMap actions behave exactly as with a physical user. send_key delivers genuine key events — Escape, arrows, Enter — not soft approximations.
Flat tree dumps carrying paths, types, scripts, displayed text and visibility; per-node deep views with properties, signal connections and groups; property read/write with type coercion.
OmniDebugLink.actions_enabled gates every write task. Set it false and the SDK becomes a read-only observer, announcing that mode when it connects.
The Godot editor has no "install from git URL" flow, so the addon is copied into your project. Two options:
Download the repository as a ZIP or clone it, then copy only the addons/omni_debug_link/ folder into your project, so your project contains res://addons/omni_debug_link/plugin.cfg. Do not drop the whole repository into addons/ — the plugin is discovered by that exact path.
git clone https://github.com/omnidebuglink/omnidebuglink_godot.git
Once listed there: AssetLib tab → search "OmniDebugLink" → Download.
Project → Project Settings → Plugins → OmniDebugLink → Enable. Enabling registers the OmniDebugLink autoload in your project settings automatically. If you would rather not enable the plugin, add the autoload by hand instead: Project Settings → Autoload → path res://addons/omni_debug_link/runtime/omnidebug_link.gd.
That is all. The SDK keeps the connection, announces its capabilities, and answers tasks. When you are done — or on a release build's title screen — call OmniDebugLink.stop().
func _ready() -> void:
OmniDebugLink.start("your-client-token")
Recommended for released games: keep the SDK disabled unless a debug flag is set.
if OS.get_environment("OMNIDEBUGLINK_TOKEN") != "":
OmniDebugLink.start(OS.get_environment("OMNIDEBUGLINK_TOKEN"))
else:
OmniDebugLink.actions_enabled = false # or don't start() at all
| Member | Meaning |
|---|---|
OmniDebugLink.start(client_token, url := "") | Connect (the optional url overrides the endpoint, for self-hosting) |
OmniDebugLink.stop() | Disconnect and stop all timers |
OmniDebugLink.actions_enabled | Master switch for every write task; false = read-only observation mode (reported on connect) |
OmniDebugLink.connected() / state_changed | Connection state and signal for your own UI |
OmniDebugLink.log / log_warning / log_error(msg) | Feed your own entries into read_logs |
OmniDebugLink.tasks.register(type, handler, description, payload_schema) | Register custom tasks |
OmniDebugLink.tasks.unregister(type) | Remove a task |
Everything below ships in the addon. The AI sees the full list — with per-task payload schemas — automatically; you never need to configure anything server-side. Write tasks are all gated by actions_enabled.
| Task | What it does |
|---|---|
scene_traverse | Flat dump of the scene tree: paths, types, scripts, displayed text and visibility, capped at 3000 nodes. |
find_objects | Search by node name (substring or regex), displayed text, or type; matches return centers and a click_target that feeds ui_click. |
view_component | One node in depth: properties, signal connections, groups and children. |
get_prop | Read a property with JSON-friendly conversion (nodes become paths, vectors become arrays). |
read_logs | Filtered query (level / substring / limit / time) over SDK and engine logs. |
wait_for | Poll every 200 ms until a node appears or a property reaches a value; timeouts return found: false, not errors. Keeps working under time_scale = 0 and while the tree is paused. |
get_perf | FPS, process/physics times, draw calls, memory and video memory, your custom performance monitors, optional frame-time percentiles (p50/p95/p99). |
screenshot | Viewport capture as JPEG, long edge capped at 1920 px and auto-compressed to fit the frame budget. |
| Task | What it does |
|---|---|
ui_click | Click a Control by path or displayed text (exact match first, substring fallback, index to disambiguate) through the real GUI event pipeline; returns the path of the node that actually received it. |
tap_screen | Tap normalized 0-1 coordinates (origin top-left); an optional touch flag adds ScreenTouch events for mobile paths. |
swipe | Drag with per-frame deltas and relative motion, so inertia and velocity tracking behave; the optional touch flag uses ScreenDrag. |
long_press | Press and hold (default 800 ms), by coordinates or node path. |
input_text | Type into LineEdit / TextEdit (or any node with a text property) and fire the change signals. |
send_key | Inject key events — Escape, arrows, Enter… — as real InputEventKey through Input.parse_input_event, so they reach GUI focus and _unhandled_input exactly like a physical keyboard. Key by name or keycode, with tap / press / release modes. |
send_action | Fire an InputMap action by name via InputEventAction. |
set_prop | Property write with type coercion — JSON arrays become Vector2 / Color / … — and the value is read back to confirm. |
set_time_scale | Set Engine.time_scale and/or SceneTree.paused: slow motion, or freeze for inspection. |
call_method | Call any method on any node — the escape hatch that lets the AI drive game logic directly. |
change_scene / reload_scene | Switch or reload the current scene. |
list_dir / read_file | Read-only access under user://, size-capped; res:// game source never leaves the device. |
| Task | What it does |
|---|---|
echo / ping | Round-trip connectivity checks. |
get_stats | Uptime, task count, connection state, library and Godot versions, platform, current scene. |
Register your own task with a type, handler, description and payload schema. Handlers run on the main thread, may await (frames, timers), and return any JSON-friendly Dictionary; signal failure by returning OmniDebugLink.task_error("message"). Registering or unregistering automatically re-announces capabilities — no changes needed on the server side.
func _ready() -> void:
OmniDebugLink.start("your-client-token")
OmniDebugLink.tasks.register(
"give_gold",
func(payload: Dictionary) -> Dictionary:
var amount := int(payload.get("amount", 100))
GameState.gold += amount
return {"gold": GameState.gold},
"Adds gold to the player wallet.",
{
"type": "object",
"properties": {"amount": {"type": "integer", "minimum": 1}},
"required": ["amount"],
}
)
| Source | Captured? |
|---|---|
OmniDebugLink.log / log_warning / log_error | always |
Everything the engine prints — print, push_error, engine errors | enable Project Settings → Debug → File Logging; the SDK tails user://logs/godot.log and merges it in |
start() or task handlers from threads; for cross-thread logging, push messages through call_deferred.GetNode("/root/OmniDebugLink").Call("start", token).time_scale = 0 and while the SceneTree is paused — a debugging tool must not be frozen by the game it debugs.