Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Native extensions

Advanced. Most projects should stay on script.txt / JSON. Use a native extension only when you need custom systems the declarative surface does not cover (inventory, custom parallax math, a one-off minigame).

A native extension is a GDScript entry file your project declares in game.json. The player compiles and loads it at startup. The runtime is built with Godot; this is the one handbook place that expects you to write GDScript against a small host API.

This page is the runtime integration map — how your Node lives inside the player. Method tables live on extensions (game.json).

When to use

PreferWhen
Script / JSON / screensDialogue, flags, layouts, most FX (shake, flash, shaders)
First-class runtime feature (later)Patterns many games would copy the same way
Native extensionGame-specific logic you will maintain yourself

Mental model

Your entry Node  →  parented under reserved "Extensions" as ext_<id>
Visuals          →  children of host.get_plane("…") CanvasLayers
Story            →  host (flags, commands, jump, signals, blocker)
Screens / toast  →  host.push_screen / notify, or authored screens.json
Godot power      →  only on nodes you create (process, input, particles, …)
  • Host is the stable contract. Prefer it over digging into private player nodes.
  • Full engine power on your subtree is intentional (unsandboxed). That does not mean every scene path is supported.
  • There is no long-term stability promise beyond the documented host API. Pin runtime_min to the player version you tested.

Trust model

  • Unsandboxed: files, network, process — anything the OS user can do.
  • Author mode: toast after title listing loaded id(s) (“unrestricted code”).
  • Production / shipped: silent load; pack is part of the product you publish.
  • Author Refresh (F5) reloads extensions in author mode. Production still loads once at boot.

Example: examples/example-06-native-extension/.

Lifecycle

MomentWhat happens
Cold bootAfter core wiring: load every extensions[] entry; call vn_extension_setup(host) once per success
TitlePhase "title"; modal title is up; your nodes already exist unless you hid them
PlayPhase "playing"; story flags/commands/signals fire
Session end → titleInput blocker cleared; story reloads; your extension Node is not torn down — reset on connect_story_ended if needed
F5 (author)Free all Extensions children; unregister ext.* saves and ext_* command handlers; load_all again; toast
ProductionNo F5 reload — edit .gd → rebuild / restart the player

There is no teardown / unload API beyond F5’s full reload. Design for long-lived nodes.

Where visuals and UI sit

Planes are fixed. You choose which plane your content attaches to via host.get_plane(id).

PlaneTypical use from an extension
bgParallax / under-character FX
overlayOver panel, under HUD/modals
hudAlways-visible chrome (still under modals)
notifyCustom toasts / banners above most UI
modalDraw over title/pause (use carefully — don’t steal all input)
cursor / transitionRare; easy to break chrome

Full plane list for get_plane: extensions (game.json). Authored menus: Title, pause and settings screens + host.push_screen(id).

Title and pause are modal screens (high layer). Children on overlay stay under them. To appear above title, parent to modal or notify, or use host.notify.

Pause vs dialogue blocker

Two different gates:

MechanismWhat it doesExtension tip
Screen pauses_tree (pause menu)Tree pauses — nodes with default process mode freezeSet process_mode = Node.PROCESS_MODE_ALWAYS on your extension root and any child that must animate or take input while paused
host.set_input_blocker(true)Blocks dialogue advance and choice confirm onlyUI screens and pause still work; call host.resume() or set_input_blocker(false) to clear

host.resume() clears the input blocker only — not story history, not pause, not a return from jump.

Input

  • Dialogue UI often consumes clicks for advance. Prefer _input (not only _unhandled_input) when you need clicks during play — see ex06 click_particles.
  • While a modal screen is up, world input is usually blocked; your node still needs a process mode that runs if you want events under pause.
  • Gate by phase when FX should not run on title: host.get_phase() → "title" | "playing" | "exit_commands" | "story_ending".

Story integration

NeedUse
React to nodes / flags / endconnect_node_entered / connect_flag_changed / connect_story_ended
Private state in savesregister_save → stored as ext.<id>.<key>; restore runs after core
Drive from scriptregister_command("verb", …) → type ext_<id>_verb in Mode A or JSON
Rich command fieldsPrefer JSON / Mode B objects; Mode A mainly gives "args" plus key=value tokens
Soft lock the lineset_input_blocker(true) then jump / resume when done
Toastnotify(message) (draws above modal)

Flags are shared with the story — prefer names like ext_<id>_score. Check allowlists ext_* only for ids declared in game.json.

Recipes

Always-on FX (runs under pause)

func vn_extension_setup(host) -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS
	var plane := host.get_plane("overlay")
	var fx := CPUParticles2D.new()
	fx.process_mode = Node.PROCESS_MODE_ALWAYS
	plane.add_child(fx)

Visible over title

Parent to host.get_plane("modal") or "notify", or call host.notify("…"). Still set PROCESS_MODE_ALWAYS if the title path ever pauses the tree.

Minigame beat

  1. host.set_input_blocker(true)
  2. Show your UI on a plane (or push_screen an authored screen id)
  3. On success: host.set_flag(…), host.jump("next_node"), host.resume()

Title-only decoration

func _process(_dt: float) -> void:
	visible = (host.get_phase() == "title")

(Keep a host reference from setup; or poll only when you care.)

Command-driven effect

host.register_command("burst", func(cmd: Dictionary) -> void:
	# cmd.type == "ext_my_id_burst"; Mode A may set cmd.args
	pass
)

Story: [ext_my_id_burst power=2] or { "type": "ext_my_id_burst", "power": 2 }.

Do / don’t

DoDon’t
Parent visuals via get_planeAssume deep paths into private player UI are stable
Use Host for story, save, screens, blockerExpect Host to wrap every engine API
Reset on connect_story_endedAssume session end frees your extension
Author new menus in screens.jsonClone title by digging screen-manager internals
Namespace flags ext_<id>_*Stomp story flags by accident
Pin runtime_minRely on undocumented private methods

Failure modes (short)

Missing file, bad GDScript, missing vn_extension_setup, duplicate id, or runtime_min too new → that entry is skipped; siblings still load. Details: extensions API.

extensions (game.json) · Screens · Save, Back and story end · Build and hand off · game.json map