Pause Menu - UQcsse3200/2026-studio-3 GitHub Wiki
The pause menu provides an in-game overlay that lets the player temporarily stop gameplay, resume the current run, open pause-related settings, or return to the main menu. It can be opened during gameplay by pressing Escape. When opened, it displays a full-screen overlay above the active game screen. While the menu is open, gameplay time is paused, and both gameplay and debug input is blocked so the player cannot accidentally interact with the battle or underlying UI.
The pause menu currently supports:
- Resume gameplay
- Save & Load the run in-place (overlay, no screen change)
- Open the in-pause settings panel (audio + display settings)
- Return to the main menu (after confirmation)
- Quit to desktop (after confirmation)
- An on-screen Pause button in addition to the Escape key
- Full keyboard navigation of the menu
- Block gameplay input while paused
It is styled to match the main menu: a deep-plum dimmed overlay, a "Paused" title, and the same pixel-art framed buttons.
The pause menu UI is built from two scene2d components that live on a gameplay screen's UI entity and talk to the backend only through events (they never call the pause service directly):
| Component | Type | Responsibility |
|---|---|---|
PauseMenuDisplay |
UIComponent (scene2d) |
Renders the dimmed overlay, the buttons, the in-pause settings subview and the confirm dialog; blocks pointer input to the game behind it. |
PauseMenuInput |
InputComponent |
Escape opens the menu; swallows other input while paused. |
PauseMenuActions |
Component |
Turns the events below into pause/resume + navigation. |
All four event names are public static final constants on PauseMenuDisplay, so the frontend and backend reference the exact same strings.
| Constant | String | Direction (from the display's view) | Meaning |
|---|---|---|---|
PAUSE_EVENT |
"pause" |
fired by PauseMenuInput, received by PauseMenuDisplay
|
Open the overlay (idempotent). |
RESUME_EVENT |
"resume" |
fired by the Resume button, received by PauseMenuDisplay
|
Close the overlay / resume. |
SETTINGS_EVENT |
"settings" |
fired by the Settings button | Open the in-pause settings subview. |
EXIT_TO_MENU_EVENT |
"exitToMenu" |
fired by Confirm in the dialog | Leave the run for the main menu. |
SAVE_LOAD_EVENT |
"saveLoad" |
fired by the Save & Load button | Open the save/load overlay. |
QUIT_EVENT |
"quit" |
fired by Confirm in the quit dialog | Quit the application. |
NAV_UP_EVENT |
"navUp" |
fired by PauseMenuInput (Up/W) | Move keyboard focus to the previous option. |
NAV_DOWN_EVENT |
"navDown" |
fired by PauseMenuInput (Down/S) | Move keyboard focus to the next option. |
NAV_SELECT_EVENT |
"navSelect" |
fired by PauseMenuInput (Enter/Space) | Activate the focused option. |
NAV_BACK_EVENT |
"navBack" |
fired by PauseMenuInput (Esc while paused) | Back out of the settings/save subview or dialog. |
PAUSE_EVENT / RESUME_EVENT are the pair that drives visibility; pause opens, resume closes. Pressing Escape again while paused does nothing (open is idempotent).
All pause-menu components live on one UI entity, so they share a single EventHandler. The
frontend (Input, Display, Pause button) only fires/receives events; the backend (Actions, services)
reacts. PauseMenuFactory wires everything, including the extra listeners for Save & Load and audio.
PAUSE_EVENT is the clearest example of the fan-out, one key press reaches four listeners:
-
PauseMenuDisplayshows the overlay, -
PauseMenuActionspausesGamePauseService(time scale → 0), -
PauseButtonDisplayhides the on-screen button, -
AudioServiceswitches to the pause music.
RESUME_EVENT reverses all four. SAVE_LOAD_EVENT is routed to SaveLoadPanel.show() by a listener
the factory registers, and the nav* events are handled inside PauseMenuDisplay for keyboard focus.
flowchart LR
Esc([Escape]) --> Input[PauseMenuInput]
Btn[PauseButtonDisplay] -- pause --> EH{{shared EventHandler}}
Input -- pause / nav* --> EH
Display[PauseMenuDisplay] -- resume / saveLoad / settings / exitToMenu / quit --> EH
EH -- pause/resume --> Display
EH -- "pause/resume/settings/exitToMenu/quit" --> Actions[PauseMenuActions]
EH -- pause/resume --> Btn
EH -- pause/resume --> Audio[AudioService]
EH -- saveLoad --> Save[SaveLoadPanel]
EH -- "nav*" --> Display
Actions --> Pause[GamePauseService]
Actions --> Game[GdxGame exit / setScreen]
A pause-then-resume interaction over time:
sequenceDiagram
actor Player
participant Src as Input / Pause button
participant Bus as EventHandler
participant Display as PauseMenuDisplay
participant Actions as PauseMenuActions
participant Pause as GamePauseService
Player->>Src: Escape / click Pause
Src->>Bus: fire "pause"
Bus->>Display: pause
Display->>Display: showMenu()
Bus->>Actions: pause
Actions->>Pause: pause()
Note over Bus: also hides the Pause button<br/>and plays pause music
Player->>Display: click "Save & Load"
Display->>Bus: fire "saveLoad"
Note over Bus: SaveLoadPanel.show()
Player->>Display: click "Resume"
Display->>Bus: fire "resume"
Bus->>Display: resume
Display->>Display: hideMenu()
Bus->>Actions: resume
Actions->>Pause: resume()
Note over Bus: also shows the Pause button<br/>and resumes music
- Extends
UIComponent; adds a rootTable(setFillParent(true)) to the stage at UI layer 2 (Z_INDEX = 2f). -
Dimmed background: a generated 1×1 black texture at 70% opacity (
BACKGROUND_OPACITY) stretched across the root table, no image asset. -
Two swappable content panels inside the root table (swapped in place with
clearChildren()+add(...), so the game screen is never left or disposed):- Pause buttons: Resume, Settings, Return to Main Menu (stacked vertically, 15px vertical gap).
-
Settings subview: a "Settings" title label (skin
"title"style) + a Back button. Opened by the Settings button, closed by Back.
-
Confirm dialog: Return to Main Menu opens a scene2d
Dialog("Leave this run? Progress may be lost") with Confirm / Cancel. Only Confirm firesexitToMenu; Cancel just dismisses. -
Input blocking: the root table is
Touchable.enabledwith anInputListenerthat consumestouchDownandscrolledwhile the overlay is visible, so clicks/scroll don't fall through to the gameplay UI behind it. Combined withtoFront()on show, the overlay sits above everything and only its own buttons are interactive. -
Visibility model: starts hidden.
PAUSE_EVENT→showMenu()(reset to the pause buttons,setVisible(true),toFront()).RESUME_EVENT→hideMenu()(dismiss the dialog, reset to the pause buttons,setVisible(false)).
- Extends
InputComponentwith priority 100, above the gameplay and debug input handlers, so their shortcuts can't leak through while the menu is up. -
keyDown(ESCAPE)→ firesPAUSE_EVENTand returnstrue(consumed). - While paused,
keyDown/keyTyped/keyUpall returntrue(checked viaGamePauseService.isPaused()), swallowing gameplay input behind the overlay.
A small always-visible Pause button shown during gameplay, so the menu is reachable by mouse as
well as Escape. Clicking it fires the same PAUSE_EVENT. It hides while the menu is open and
reappears on resume, and its label is scaled to fit the frame (BUTTON_WIDTH 170, LABEL_SCALE
0.6). The full-screen container is Touchable.childrenOnly, so only the button catches clicks.
Placement is configurable via Align and set per screen by the factory (see Wiring):
- Battle / Encounter: top-right (default)
- MainGame: top-left
- Map: no on-screen button (it didn't fit the map HUD); Escape still opens the menu.
A Save & Load button fires SAVE_LOAD_EVENT, which opens the reusable SaveLoadPanel
(components/save) as an in-place overlay, no screen switch, so the run is never disposed. The
factory builds the panel from the current run state, starts it hidden, and its Back button hides it
again to return to the pause buttons.
A Quit Game button opens a confirmation dialog; only Confirm fires QUIT_EVENT, which
PauseMenuActions turns into game.exit(). Like "Return to Main Menu", it's gated behind the
dialog so a stray click can't close the game.
While paused the menu is fully keyboard-driven, handled inside PauseMenuDisplay:
-
Up / W and Down / S move the highlight (
NAV_UP_EVENT/NAV_DOWN_EVENT), -
Enter / Space activates the focused option (
NAV_SELECT_EVENT), -
Esc backs out of the settings/save subview or dialog (
NAV_BACK_EVENT).
It opens in mouse mode (hover only); the first navigation key activates the keyboard highlight, and moving the mouse clears it again. Navigation also covers the settings panel's buttons and the dialog's Confirm / Cancel.
The overlay is dimmed with MenuTheme.deepPlum() at OVERLAY_OPACITY (0.7), the buttons use
MenuTheme (pixel-art frame + large font, sized MenuTheme.BUTTON_WIDTH × BUTTON_HEIGHT), and a
"Paused" title sits above them. The in-pause settings view hosts the shared SettingsPanel
(audio + display settings). This keeps the pause menu visually consistent with the main menu.
Screens no longer add the components by hand, PauseMenuFactory assembles the whole cluster
(display + input + actions + save panel + optional pause button), wires the events, and registers a
GamePauseService if the screen hasn't:
// top-right pause button (default)
SaveLoadPanel savePanel = PauseMenuFactory.attach(uiEntity, game);
savePanel.hide(); // save overlay starts hidden
Overloads:
-
attach(uiEntity, game): top-right pause button. -
attach(uiEntity, game, align)/attach(uiEntity, game, align, edgePad): pick the corner. -
attachWithoutButton(uiEntity, game): Escape-only, no on-screen button (used by the Map).
Currently wired into: BattleScreen and EncounterScreen (top-right), MainGameScreen (top-left), and MapScreen (no button, Escape only).
- Overlay: full screen, black at 70% opacity.
- Buttons: shared
flat-earthskinTextButtons, stacked and centered, 15px gap. - Settings subview:
"title"-style label + Back button. - Confirm dialog: default skin
Dialog, centered, Confirm + Cancel (10px pad). - Styling uses the shared skin only, no custom PNG yet. A themed panel / caution styling for the confirm dialog is a possible enhancement.
Headless (GameExtension + a real Stage backed by a mocked SpriteBatch; buttons are "clicked"
via actor.fire(new ChangeListener.ChangeEvent()), nav keys via a nav(event) helper).
PauseMenuDisplayTest, starts hidden; pause opens and is idempotent; Resume hides and fires
resume; Save & Load fires saveLoad; Settings opens the in-pause settings view and Back returns;
Resume closes the settings view too; the overlay is touchable and blocks underlying actors; Return
to Main Menu shows the dialog and only fires exitToMenu on Confirm (Cancel dismisses); Quit shows
the dialog and only quits on Confirm; saveEnabled = false omits the Save button. Keyboard nav:
opens in mouse mode with no focus; first Nav-Down focuses the first button; Nav-Down twice focuses
the second; Nav-Up wraps to the last; Nav-Select activates the focused button; Nav-Select in mouse
mode does nothing; mouse movement clears keyboard focus; keyboard navigating into Settings opens the
view and reaches all settings actions; Nav-Back returns from settings and cancels the confirm dialog.
PauseMenuInputTest, Escape fires pause and is consumed; other keys ignored when not paused;
other keys consumed while paused; arrows/Enter fire the nav* events while paused; Escape while
paused fires navBack (not pause); priority is above gameplay/debug input.
PauseButtonDisplayTest, the button starts visible; clicking it fires pause; it hides while
paused and reappears on resume.
-
Add a button: create a
TextButton, add aChangeListenerthat triggers a new event constant, add it inbuildMenuTable(), and add a matching listener inPauseMenuActions. -
Real settings controls: go in
buildSettingsTable()(currently just a title + Back). -
Restyle: swap the skin styles or the background texture; keep the input-blocking listener and the
toFront()call on show.
classDiagram
class GamePauseService {
-PAUSED_TIME_SCALE : float
-RUNNING_TIME_SCALE : float
-timeSource : GameTime
-paused : boolean
+GamePauseService(GameTime)
+pause() void
+resume() void
+isPaused() boolean
}
class PauseMenuActions {
-logger : Logger
-game : GdxGame
+PauseMenuActions(GdxGame)
+create() void
-onResume() void
-onSettings() void
-onExitToMenu() void
-onQuit() void
-onPause() void
-getPauseService() GamePauseService
}
class PauseMenuInput {
-PAUSE_INPUT_PRIORITY : int
+PauseMenuInput()
+keyDown(int) boolean
+keyTyped(char) boolean
+keyUp(int) boolean
-isPaused() boolean
}
class PauseMenuDisplay {
+RESUME_EVENT : String
+SAVE_LOAD_EVENT : String
+SETTINGS_EVENT : String
+EXIT_TO_MENU_EVENT : String
+QUIT_EVENT : String
+PAUSE_EVENT : String
+NAV_UP_EVENT : String
+NAV_DOWN_EVENT : String
+NAV_SELECT_EVENT : String
+NAV_BACK_EVENT : String
-saveEnabled : boolean
-focusables : List~TextButton~
-focusedIndex : int
-settingsPanel : SettingsPanel
+PauseMenuDisplay()
+PauseMenuDisplay(boolean)
+create() void
+draw(SpriteBatch) void
+getZIndex() float
+dispose() void
-showMenu() void
-hideMenu() void
-showSettings() void
-showPauseButtons() void
-showConfirm(String, Runnable) void
-focusNext() void
-focusPrevious() void
-activateFocused() void
-navigateBack() void
}
class PauseButtonDisplay {
-BUTTON_WIDTH : float
-BUTTON_HEIGHT : float
-LABEL_SCALE : float
-align : int
-edgePad : float
-pauseButton : TextButton
+PauseButtonDisplay()
+PauseButtonDisplay(int)
+PauseButtonDisplay(int, float)
+create() void
+draw(SpriteBatch) void
+getZIndex() float
+dispose() void
-show() void
-hide() void
}
class PauseMenuFactory {
-SAVE_SLOT_IDS : List~Integer~
+attach(Entity, GdxGame) SaveLoadPanel
+attach(Entity, GdxGame, int) SaveLoadPanel
+attach(Entity, GdxGame, int, float) SaveLoadPanel
+attachWithoutButton(Entity, GdxGame) SaveLoadPanel
-ensurePauseService() void
-loadMenuAssets() void
-buildSavePanel(GdxGame) SaveLoadPanel
}
PauseMenuActions --> GamePauseService
PauseMenuInput ..> GamePauseService
PauseMenuDisplay --> SettingsPanel
PauseMenuFactory ..> PauseMenuDisplay
PauseMenuFactory ..> PauseMenuInput
PauseMenuFactory ..> PauseMenuActions
PauseMenuFactory ..> PauseButtonDisplay
PauseMenuFactory ..> SaveLoadPanel
PauseMenuFactory ..> GamePauseService
GamePauseService owns the paused state of the game.
Responsibilities:
- Tracks whether the game is currently paused.
- Pauses gameplay by setting
GameTimetime scale to0f. - Resumes gameplay by setting
GameTimetime scale back to1f. - Makes repeated pause and resume calls safe.
This service is registered through ServiceLocator, allowing pause-related components to access shared pause state.
PauseMenuActions listens for pause menu events and performs backend actions.
Handled events:
-
pause: pauses gameplay throughGamePauseService. -
resume: resumes gameplay throughGamePauseService. -
settings: reserved for the in-pause settings view. -
exitToMenu: resumes gameplay state, then navigates back to the main menu. -
quit: exits the application viagame.exit().
The settings event intentionally does not switch to SettingsScreen, because changing screens would dispose the active gameplay screen. Instead, settings are shown inside the pause overlay.
PauseMenuInput listens for keyboard input related to pausing.
Behaviour:
- Pressing Escape triggers the pause event.
- While paused, non-Escape keyboard input is consumed.
- The component uses a high input priority so pause handling runs before gameplay/debug shortcuts.
This prevents gameplay inputs, card/debug shortcuts, and other keyboard actions from leaking through while the pause menu is active.
The pause menu backend is covered by unit tests:
GamePauseServiceTestPauseMenuActionsTestPauseMenuInputTestPauseMenuDisplayTestPauseButtonDisplayTestPauseSettingsIntegrationTest
Test coverage includes:
- Pause service state changes.
- Time scale changes on pause/resume.
- Pause menu event routing.
- Settings not disposing the gameplay screen.
- Input blocking while paused, and Escape vs. nav-key handling.
- Overlay blocking clicks to underlying actors.
- Save & Load button wiring and the save-disabled case.
- Full keyboard navigation and mouse/keyboard mode switching.
- The in-pause settings view keeping gameplay paused until Resume (
PauseSettingsIntegrationTest). - Pause menu visibility and confirmation behaviour.