10 KiB
UI System
Scene structure
RootUI (Control, fullscreen, always visible)
├── DebugMenu
├── GameMenu
│ ├── GameMenuPartyTab
│ └── GameMenuItemsTab
├── ChatBoxContainer
│ └── InteractIndicator
├── ModalBackdrop ← shared backdrop; z_index managed by FocusStack
├── PauseMenu
│ ├── PauseMain
│ └── PauseSettings
├── QuitConfirmDialog
└── MainMenuConfirmDialog
Child order matters — later siblings render on top. ModalBackdrop stays at a fixed tree position; its z_index is driven automatically by the FocusStack (see ModalBackdrop).
RootUI is a permanent child of RootScene and registers itself with the UI singleton on _enter_tree. Access its children through the UI singleton.
UI singleton
UI (autoload) is the global access point.
| Accessor | Type | Notes |
|---|---|---|
UI.DEBUG_MENU |
DebugMenu |
Dev scene-jump overlay |
UI.GAME_MENU |
GameMenu |
JRPG-style in-game menu |
UI.PAUSE_MENU |
PauseMenu |
Pause overlay; also pauses the scene tree |
UI.QUIT_DIALOG |
QuitConfirmDialog |
"Quit to desktop?" confirm; call .open() to show |
UI.MAIN_MENU_DIALOG |
ConfirmDialog |
"Return to main menu?" confirm; call .open() to show |
UI.FOCUS_STACK |
UIFocusStack |
Ordered stack of open ClosableMenu layers; only the top layer processes input |
UI.BACKDROP |
ModalBackdrop |
Shared semi-transparent backdrop; driven by FOCUS_STACK.activeLayerChanged |
UI.dialogueActive |
bool |
true for the entire duration of a DialogueAction |
UI.activeConversation |
bool |
true only during a CONVERSATION-mode dialogue |
UI.chatBoxContainer |
Control |
Parent node for world-space dialogue textboxes |
dialogueActive is set by DialogueManager signals — it is broader than any single textbox being visible. Movement and camera orbit block when UI.FOCUS_STACK.top() != null or UI.activeConversation is true.
ClosableMenu
Base class for all togglable panels. Extends Control; the isOpen:bool export drives visible.
menu.open() # shows, pushes to FocusStack (if canClose), emits opened
menu.close() # pops from FocusStack (if canClose), hides, emits closed
menu.toggle()
Key exports / properties:
| Name | Default | Notes |
|---|---|---|
isOpen:bool |
false |
Property — read directly (menu.isOpen), not a method |
canClose:bool |
true |
When true, open/close interact with UI.FOCUS_STACK and input is only processed while on top. When false, the menu is passive — shown/hidden externally, never enters the stack. |
Signals: opened, closed, focusGained, focusLost.
Focus management (only relevant when canClose = true):
_grabInitialFocus()— virtual; override to place focus on the correct element on first open._savedFocusNode— focus owner is captured on_onFocusLost()and restored on_onFocusGained(), so pressing back from a sub-dialog returns focus to the button that opened it.- A
gui_focus_changedfocus trap runs while the layer is on top — if focus escapes to a node outside this layer, it is snapped back. set_process_unhandled_input(false)is set in_ready()for canClose menus; input is only re-enabled via_onFocusGained()while the layer is on top of the stack.
All new menus that need standard show/hide behaviour should extend ClosableMenu.
ConfirmDialog
Reusable "Yes / No" confirmation overlay at res://ui/component/ConfirmDialog.gd. Extends ClosableMenu.
dialog.open() # shows, focuses No (safe default), emits opened
dialog.confirmed # signal — fires after Yes is pressed, before close
Focus locking: focus_neighbor_top/bottom on both buttons is wired so controller navigation cannot escape the dialog to elements behind it.
ui_cancel closes the dialog the same as pressing No.
Subclassing:
class_name MyConfirmDialog extends ConfirmDialog
func _ready() -> void:
super._ready()
confirmed.connect(func(): do_the_thing())
QuitConfirmDialog uses this pattern — it extends ConfirmDialog and connects confirmed → get_tree().quit() in its own _ready().
Adding a new confirm dialog:
- Create a
.tscnwithConfirmDialog.gdas script; set label text in the scene - Add
btnYes/btnNonode path exports pointing to your two buttons - Add the instance to
RootUI.tscnafterPauseMenu(so it renders on top) - Expose via
RootUI.gdexport +UISingleton.gdaccessor if other systems need it - Connect
myDialog.confirmedwherever the action should fire
No manual backdrop registration needed — ModalBackdrop activates automatically when any ClosableMenu in RootUI enters the FocusStack.
ModalBackdrop
res://ui/component/ModalBackdrop.gd — a single fullscreen semi-transparent ColorRect shared across all modal overlays.
How it works: ModalBackdrop connects to UI.FOCUS_STACK.activeLayerChanged in _ready(). When a layer becomes active it checks whether that layer is a direct sibling (i.e. a child of RootUI). If yes: backdrop becomes visible, sets mouse_filter = MOUSE_FILTER_STOP (blocking all clicks on anything behind it), and sets z_index = layer.z_index - 5. If the top layer is from a different parent (e.g. settings inside the main menu scene), or the stack empties, the backdrop hides.
Result: only one backdrop is ever visible, it always renders between the game world and the frontmost RootUI-level overlay, and it blocks mouse events from reaching anything behind it.
No registration is required — ModalBackdrop is self-contained. Do not call register() on it.
AdvancedRichText
RichTextLabel subclass (@tool) used inside world-space dialogue textboxes. Handles:
- Smart word-wrap (
TextServer.AUTOWRAP_WORD_SMART) - Pagination via
maxLines/startLineexports - Inline input icons:
[input action=interact]text[/input]→ replaced with the icon image fromres://ui/input/{action}.tres - Optional auto-translation via Godot's
tr()
Supported icon actions: interact, pause, debug, up, down, left, right.
Game menu
JRPG-style in-game menu at res://ui/gamemenu/. Open with the menu input (Tab on keyboard, Y on controller). Blocks player movement while open via EntityMovement._canMove().
Structure:
- Left sidebar:
ItemListtab selector (Party, Items) - Right panel: active tab content
| Tab | Content |
|---|---|
| Party | One card per PartyMember — name, status, HP/MP, ATK/DEF/SPD/MAG/LCK |
| Items | One row per ItemStack in PARTY.BACKPACK — item name + quantity |
Both tabs are populated dynamically on open; call refresh() on the active tab directly if data changes while the menu is already open.
Key members on GameMenu:
| Member | Notes |
|---|---|
open() |
Shows menu, refreshes active tab, pushes to FocusStack, grabs sidebar focus |
close() |
Pops from FocusStack, hides menu |
isOpen:bool |
Property — read directly, not a method |
ui_cancel or menu closes the menu. menu is handled in _input (always fires) and opens the menu only when UI.FOCUS_STACK.top() == null and UI.dialogueActive is false.
To add a new tab: add a value to GameMenu.Tab, create a tab scene/script under ui/gamemenu/, instance it in GameMenu.tscn as a sibling of the other tabs, add an @export for it in GameMenu.gd, and add the match branch in _selectTab().
Pause menu
PauseMenu wraps PauseMain (button list) and PauseSettings (settings tabs). Opening it calls get_tree().paused = true; closing restores it.
| Member | Notes |
|---|---|
PauseMenu.open() |
Pushes to FocusStack, pauses tree, opens PauseMain, emits opened |
PauseMenu.close() |
Unpauses tree, closes sub-panels, pops from FocusStack, emits closed |
PauseMenu.isOpen:bool |
Property — read directly, not a method |
ui_cancel behaviour inside PauseMenu:
- If
PauseSettingsis open → closes settings, reopens PauseMain - Otherwise → closes PauseMenu
(QuitConfirmDialog and MainMenuConfirmDialog sit above PauseMenu on the FocusStack and consume ui_cancel themselves — PauseMenu's _unhandled_input does not fire while they are open.)
PauseMain buttons:
| Button | Behaviour |
|---|---|
| Resume | Closes PauseMenu |
| Settings | Opens PauseSettings |
| Main Menu | Opens MainMenuConfirmDialog; on confirm → SCENE.setScene(INITIAL) |
| Quit Game | Opens QuitConfirmDialog; on confirm → get_tree().quit() |
When either confirm dialog closes, focus automatically returns to the button that opened it via _savedFocusNode in the FocusStack.
Cannot open on main menu: Pause.gd checks SCENE.currentScene == INITIAL and skips opening.
Main menu
res://ui/mainmenu/MainMenu.tscn. Buttons: New Game, Settings, Quit Game.
- New Game →
SCENE.setScene(OVERWORLD)+OVERWORLD.mapChange(...) - Settings → opens the
MainMenuSettingsoverlay - Quit Game → opens
UI.QUIT_DIALOG; on cancel, focus returns to the Quit button
Settings menu
SettingsMenu is a shared tabbed component instanced in both MainMenuSettings and PauseSettings. Three tabs: Gameplay, Sound, Graphics — currently contain placeholder labels only.
Debug menu
DebugMenu (toggle via debug input — F1) provides four scene-jump buttons:
| Button | Action |
|---|---|
| Overworld | SCENE.setScene(OVERWORLD) |
| Battle | SCENE.setScene(BATTLE) |
| Cooking | SCENE.setScene(COOKING) |
| Initial | SCENE.setScene(INITIAL) |
Access via UI.DEBUG_MENU. Starts hidden.
Theme & assets
- Global theme:
res://ui/UI Theme.tres— applied to all UI nodes - Input icons:
res://ui/input/{action}.tres— one file per action name - Font: configured globally in project settings
Adding a new menu
- Create a scene whose root extends
ClosableMenu(orControlif open/close isn't needed) - Override
_grabInitialFocus()to place focus on the first interactive element on open - Add it as a child of
RootUI.tscn— position afterPauseMenuif it should render above it - Export a typed reference on
RootUI.gdand wire it in the Inspector - Expose via a getter on
UISingleton.gdif other systems need access (follow thePAUSE_MENU/GAME_MENUpattern)
ModalBackdrop activates automatically for any ClosableMenu that is a direct child of RootUI. No registration step needed. Internal sub-panels (like PauseSettings) should extend Control directly and not enter the FocusStack.