Editor-first hosted screen routing for Godot 4 app shells.
Use this addon when your project has a persistent main scene and wants to route between screen scenes inside a RouteHost. GdRouter owns navigation state, params, and history; RouteHost owns the mounted screen node.
gdam install @aviorstudio/gd-router
Copy addon/ into res://addons/@aviorstudio_gd-router/ and enable the plugin.
The plugin installs an autoload named GdRouter and adds editor types for RouteHost, RouteMap, RouteDefinition, RouteTransition, and RouteLink.
Create a main scene like this:
Main.tscn
RouteHost
Create screens like this:
res://src/screens/home_screen/home_screen.tscn
res://src/screens/game_screen/game_screen.tscn
Select RouteHost in the editor and set:
initial_route:homeauto_discover:trueroutes_dir:res://src/screensroute_dir_suffix:_screen
Navigate from code:
func _on_play_button_pressed() -> void:
GdRouter.go_to("game", {"level": "level_01"})Or add a RouteLink button and set its route_name in the Inspector.
For a production route map, use Godot's top menu:
Project > Tools > GD Router: Create Route Map From Screens
This scans res://src/screens/*_screen/*_screen.tscn and creates res://src/static/config/main_route_map.tres.
res://src/main/main.tscn
res://src/screens/home_screen/home_screen.tscn
res://src/screens/game_screen/game_screen.tscn
res://src/static/config/main_route_map.tres
main.tscn should stay persistent for app startup, autoload coordination, telemetry, audio, save systems, and other shell-level lifecycle. Routed screens should be mounted under a RouteHost child.
var navigation = GdRouter.go_to("settings", {"tab": "audio"})
if navigation.is_pending():
await navigation.completed
if navigation.is_success():
print("settings mounted")
GdRouter.replace("home")
GdRouter.go_back()Navigation is transactional and latest-wins. go_to, replace, and go_back return a RouteResult whose status is PENDING, SUCCEEDED, FAILED, or SUPERSEDED. Route, params, and history commit only after the matching generation mounts successfully. A newer valid navigation supersedes an older pending request; stale resource or transition completions cannot mount or commit. Immediate failures (unknown route, blocked guard, or no back history) are already settled when returned, so inspect status before awaiting completed.
GdRouter: autoload navigation API, route table, params, and history.RouteHost: scene-tree outlet that mounts the active screen as a child.RouteMap: editor-visible route list resource for production projects.RouteDefinition: route name, screen scene path, metadata, and optional guard.RouteTransition: assignable transition resource.InstantRouteTransition: no-animation transition.CrossfadeRouteTransition: simple screen crossfade and slide transition.RouteLink: button node that navigates to a route from Inspector data.
The router can auto-discover scenes that follow this convention:
res://src/screens/*_screen/*_screen.tscn
For example, res://src/screens/home_screen/home_screen.tscn becomes route home.
Auto-discovery is useful while prototyping. A committed RouteMap.tres is recommended for larger projects because routes become inspectable and reviewable in the Godot editor.
Create a RouteMap resource and assign it to RouteHost.route_map when you want explicit editor-authored routes. Each RouteDefinition can set:
route_namescene_pathtitlemetadataguard
When a RouteMap is assigned, RouteHost uses it instead of auto-discovery.
For production projects, prefer a committed route map over auto-discovery. Auto-discovery is excellent for early prototyping, but a RouteMap.tres gives designers and reviewers an explicit source of truth in the editor.
If RouteHost.initial_route is empty, the host uses RouteMap.initial_route.
The editor tool menu action creates a route map from the standard screen layout:
res://src/screens/home_screen/home_screen.tscn -> home
res://src/screens/game_screen/game_screen.tscn -> game
When updating an existing route map, the generator preserves route titles, metadata, and guards for matching route names while refreshing discovered scene paths. This keeps route maps editor-authored without making designers manually re-enter obvious paths.
Assign a RouteTransition resource to RouteHost.transition.
Built-in transitions:
InstantRouteTransition: swaps screens without animation.CrossfadeRouteTransition: fades between screens with a small slide-in.
Custom transitions should extend RouteTransition and emit finished when the host may free the previous screen.
Preset resources are included at:
res://addons/@aviorstudio_gd-router/presets/instant_route_transition.tres
res://addons/@aviorstudio_gd-router/presets/crossfade_route_transition.tres
Assign a RouteGuard resource to RouteDefinition.guard when a route needs to block entry.
extends RouteGuard
func can_enter(context: RouteContext) -> bool:
return context.params.get("unlocked", false)Guards run before the host loads the target scene. A blocked guard leaves the current route and history unchanged.
RouteLink is a Button subclass for editor-authored navigation. Set its action in the Inspector:
GO_TO: callsGdRouter.go_to(route_name, params).REPLACE: callsGdRouter.replace(route_name, params).BACK: callsGdRouter.go_back().
Use RouteLink for simple menu buttons and keep direct GdRouter calls for screen-specific behavior that needs custom code.
RouteHost surfaces configuration warnings in the editor when:
- no route map is assigned and auto-discovery is disabled
routes_dirdoes not exist- no routes are discovered
- the initial route is missing
- route map entries point at missing scenes
- the transition resource does not implement
RouteTransition
gd-router is designed around a persistent app shell:
Main scene: app startup, autoload coordination, observability, layout shell
RouteHost: mounted active screen
Screens: authored destination scenes
Components: reusable parts inside screens
The router does not replace the whole SceneTree.current_scene by default. Whole-scene replacement is intentionally not the primary model because it makes global app lifecycle and editor-authored shells harder to manage.
This repository includes a small app-shell example:
examples/app_shell/main.tscn
examples/app_shell/src/static/config/main_route_map.tres
examples/app_shell/src/screens/home_screen/home_screen.tscn
examples/app_shell/src/screens/game_screen/game_screen.tscn
It demonstrates RouteHost, RouteMap, and RouteLink together.
Use GDAM links to test unreleased addon changes in a game project:
gdam link @aviorstudio/gd-router /path/to/gd-router/addon
gdam installKeep gdam.link.json local. If it lives under res://, exclude it from exports so local paths are never packed into builds.
- Works in Godot 4.x native and web exports.
- Game-specific guards, loading screens, and feature lifecycle should live in your game code.
addon/: Godot plugin source packaged for GDAM and manual installation.addon/plugin.cfg: plugin name, version, description, and entry script.addon/src/core/: navigation state and request objects.addon/src/editor/: editor automation helpers.addon/src/nodes/: editor-visible routing nodes.addon/src/resources/: editor-visible route, guard, and transition resources.addon/src/discovery/: screen route discovery.addon/presets/: built-in transition preset resources.tests/: Godot test project/scripts for addon behavior..github/workflows/ci.yml: validates package shape and runs tests..github/workflows/release.yml: creates GitHub release ZIPs and publishes to GDAM.
The version in addon/plugin.cfg is the addon package version. Releases are created from main with the manual release workflow and plain semver tags like v0.0.1. The workflow reruns the complete common gate, uploads the already-tested @aviorstudio_gd-router.zip without rebuilding it, records its SHA-256 and installed-tree digest, creates the GitHub Release, and publishes those bytes to GDAM.
Run locally with:
./tests/test.shCI and release both require Godot 4.7.2, run negative/restored runner controls, execute every *_test.gd with an assertion-reach sentinel and runtime-error/timeout checks, validate the closed release manifest, and test the exact ZIP through plugin enable, editor restart, smoke, disable, restart, and consumer-owned autoload preservation.
Every shipped GDScript must have a committed .gd.uid sidecar, even if both the
sidecar and its manifest entry are accidentally removed. This is a script-only
coverage rule, not a requirement to add sidecars to every Godot resource. The
two legacy orphan sidecars remain explicitly declared; this check does not
silently delete or migrate them.
The lifecycle gate compares every addon-relative file path and byte against the exact ZIP before startup and after each editor invocation, native smoke, and Web export. Generated UIDs are not ignored, and no reinstall masks import changes. Disposable contract tests exercise missing, mutated, and extra UID and non-UID files, reject changed archive paths and bytes, and restore the known-good fixtures. To run these checks separately:
python3 tests/package_contract_test.py
./scripts/package_addon.py
./tests/package_lifecycle_test.sh dist/@aviorstudio_gd-router.zipCorrection (fieldsofrevik#152): the earlier text said CI ran tests/test.sh “when available.” That conditional description overstated the gate: a missing suite could be skipped, runtime errors followed by exit zero were not rejected, releases rebuilt untested bytes, and editor/package lifecycle was not exercised. The suite and exact-package lifecycle are now mandatory in both CI and release.
MIT