8.7 KiB
UI System
Scene structure
RootUI (Control, fullscreen, always visible)
├── DebugMenu
├── GameMenu
│ ├── GameMenuPartyTab
│ └── GameMenuItemsTab
├── ChatBoxContainer
│ └── InteractIndicator
├── ModalBackdrop ← shared backdrop; repositions dynamically
├── PauseMenu
│ ├── PauseMain
│ └── PauseSettings
├── QuitConfirmDialog
└── MainMenuConfirmDialog
Child order matters — later siblings render on top. ModalBackdrop shifts its own position in the tree at runtime to sit just below whichever modal overlay is currently topmost (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.BACKDROP |
ModalBackdrop |
Shared semi-transparent backdrop; managed automatically |
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 blocks on dialogueActive and on UI.GAME_MENU.isOpen().
ClosableMenu
Base class for any togglable panel. Extends Control; the isOpen:bool export drives visible.
menu.open() # shows, emits opened
menu.close() # hides, emits closed
menu.toggle()
Signals: opened, closed.
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) - Register it with the backdrop in
RootUI._ready():modalBackdrop.register(myDialog) - Expose via
RootUI.gdexport +UISingleton.gdaccessor if other systems need it - Connect
myDialog.confirmedwherever the action should fire
ModalBackdrop
res://ui/component/ModalBackdrop.gd — a single fullscreen semi-transparent ColorRect shared across all modal overlays.
How it works: each registered overlay's opened/closed signals are connected. When any overlay opens, the backdrop becomes visible and move_child()s itself in the scene tree to sit immediately before the highest-index open overlay. When all overlays close, it hides.
Result: only one backdrop is ever visible, and it always sits between the game world (or lower overlays) and the frontmost open overlay — even when overlays are stacked (e.g. QuitConfirmDialog over PauseMenu).
Registering a new overlay:
# In RootUI._ready() — overlay must be a direct child of RootUI and have opened/closed signals
modalBackdrop.register(myOverlay)
PauseMenu, QuitConfirmDialog, and MainMenuConfirmDialog are all registered.
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 methods on GameMenu:
| Method | Effect |
|---|---|
open() |
Shows menu, refreshes active tab, grabs sidebar focus |
close() |
Hides menu |
isOpen() -> bool |
Visibility state |
ui_cancel or menu closes the menu. The menu input opens it only when UI.dialogueActive is false and the textbox is closed.
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.
| Method | Effect |
|---|---|
PauseMenu.open() |
Pauses tree, shows container, opens PauseMain, emits opened |
PauseMenu.close() |
Unpauses tree, hides everything, emits closed |
PauseMenu.isOpen() -> bool |
Visibility state |
ui_cancel behaviour inside PauseMenu:
- If
QuitConfirmDialogorMainMenuConfirmDialogis open → ignored (the dialog handles it) - If
PauseSettingsis open → closes settings, reopens PauseMain - Otherwise → closes PauseMenu
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 cancels, focus returns to the button that opened it.
Cannot open on main menu: Pause.gd checks SCENE.currentScene == INITIAL and skips opening when already closed.
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) - 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) - If it needs a backdrop, register it:
modalBackdrop.register(myMenu)inRootUI._ready()