Pause Menu - UQcsse3200/2026-studio-3 GitHub Wiki

Pause Menu

Overview

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.

Frontend / UI

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.

Event contract (the FE ↔ BE seam)

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).

Event flow

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:

  • PauseMenuDisplay shows the overlay,
  • PauseMenuActions pauses GamePauseService (time scale → 0),
  • PauseButtonDisplay hides the on-screen button,
  • AudioService switches 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]
Loading

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
Loading

PauseMenuDisplay (the overlay)

  • Extends UIComponent; adds a root Table (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 fires exitToMenu; Cancel just dismisses.
  • Input blocking: the root table is Touchable.enabled with an InputListener that consumes touchDown and scrolled while the overlay is visible, so clicks/scroll don't fall through to the gameplay UI behind it. Combined with toFront() 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)).

PauseMenuInput (the Escape key)

  • Extends InputComponent with priority 100, above the gameplay and debug input handlers, so their shortcuts can't leak through while the menu is up.
  • keyDown(ESCAPE) → fires PAUSE_EVENT and returns true (consumed).
  • While paused, keyDown / keyTyped / keyUp all return true (checked via GamePauseService.isPaused()), swallowing gameplay input behind the overlay.

On-screen Pause button (PauseButtonDisplay)

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.

Save / Load from the pause 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.

Quit to desktop

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.

Keyboard navigation

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.

Main-menu styling

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.

Wiring the pause menu into a screen

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).

Visual / layout summary

  • Overlay: full screen, black at 70% opacity.
  • Buttons: shared flat-earth skin TextButtons, 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.

Frontend tests

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.

Extending the UI

  • Add a button: create a TextButton, add a ChangeListener that triggers a new event constant, add it in buildMenuTable(), and add a matching listener in PauseMenuActions.
  • 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.

Backend Structure

UML Diagram

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
Loading

GamePauseService

GamePauseService owns the paused state of the game.

Responsibilities:

  • Tracks whether the game is currently paused.
  • Pauses gameplay by setting GameTime time scale to 0f.
  • Resumes gameplay by setting GameTime time scale back to 1f.
  • Makes repeated pause and resume calls safe.

This service is registered through ServiceLocator, allowing pause-related components to access shared pause state.

PauseMenuActions

PauseMenuActions listens for pause menu events and performs backend actions.

Handled events:

  • pause: pauses gameplay through GamePauseService.
  • resume: resumes gameplay through GamePauseService.
  • settings: reserved for the in-pause settings view.
  • exitToMenu: resumes gameplay state, then navigates back to the main menu.
  • quit: exits the application via game.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.

Input Handling

PauseMenuInput

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.

Testing

The pause menu backend is covered by unit tests:

  • GamePauseServiceTest
  • PauseMenuActionsTest
  • PauseMenuInputTest
  • PauseMenuDisplayTest
  • PauseButtonDisplayTest
  • PauseSettingsIntegrationTest

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.
⚠️ **GitHub.com Fallback** ⚠️