Pause Menu

freeze the game with tree.paused and show a modal overlay.

▶ Run in browser

Tags: ui menu pause modal anchors

Setting self.tree.paused = True suspends the scene: every node whose update_mode resolves to PAUSABLE (the default for a scene) stops running on_update and stops hearing input, while it keeps drawing. A node that must keep working during the pause, such as the menu that resumes the game, is created with update_mode=UpdateMode.ALWAYS. The mode is inherited, so set it on the menu, never on the scene root, or the whole game would ignore the pause.

Here the background clock lives in the root’s on_update, so it stops by itself while paused; nothing in the game checks a pause flag. PauseMenu declares the pause action (P or ESC) in its own input_context and toggles the pause with @on_input("pause"); the Pause button in the top-right corner does the same for a mouse or a finger. Opening the menu shows a dimmed FULL_RECT scrim and a CENTER-anchored panel holding a VBoxContainer of Resume, Restart and Quit buttons. Anchors keep the scrim covering the window and the panel centred at any window size. Restart is a signal the menu emits and the game connects, so the menu knows nothing about what it pauses.

Source

  1# /// simvx
  2# tags = ["ui", "menu", "pause", "modal", "anchors"]
  3# web = { width = 800, height = 600, responsive = true }
  4# ///
  5"""Pause Menu: freeze the game with `tree.paused` and show a modal overlay.
  6
  7Setting `self.tree.paused = True` suspends the scene: every node whose
  8`update_mode` resolves to `PAUSABLE` (the default for a scene) stops running
  9`on_update` and stops hearing input, while it keeps drawing. A node that must
 10keep working during the pause, such as the menu that resumes the game, is
 11created with `update_mode=UpdateMode.ALWAYS`. The mode is inherited, so set it
 12on the menu, never on the scene root, or the whole game would ignore the pause.
 13
 14Here the background clock lives in the root's `on_update`, so it stops by
 15itself while paused; nothing in the game checks a pause flag. `PauseMenu`
 16declares the `pause` action (P or ESC) in its own `input_context` and toggles
 17the pause with `@on_input("pause")`; the Pause button in the top-right corner
 18does the same for a mouse or a finger. Opening the menu shows a dimmed
 19`FULL_RECT` scrim and a `CENTER`-anchored panel holding a `VBoxContainer` of
 20Resume, Restart and Quit buttons. Anchors keep the scrim covering the window
 21and the panel centred at any window size. Restart is a signal the menu emits
 22and the game connects, so the menu knows nothing about what it pauses.
 23"""
 24
 25import math
 26
 27from simvx.core import (
 28    Anchor,
 29    Button,
 30    Colour,
 31    Control,
 32    Key,
 33    Label,
 34    Node2D,
 35    Panel,
 36    Signal,
 37    UpdateMode,
 38    VBoxContainer,
 39    on_input,
 40)
 41from simvx.graphics import App
 42
 43
 44class PauseMenu(Control):
 45    """The pause button and the overlay it opens, alive while the scene is paused."""
 46
 47    input_context = {"pause": [Key.P, Key.ESCAPE]}
 48    restart_requested = Signal()
 49
 50    def on_ready(self):
 51        pause = self.add_child(Button("Pause", name="PauseButton", anchor=Anchor.TOP_RIGHT, inset=16, size=(96, 32)))
 52        pause.pressed.connect(self.toggle)
 53
 54        self.overlay = self.add_child(Control(name="Overlay", anchor=Anchor.FULL_RECT, visible=False))
 55        scrim = self.overlay.add_child(Panel(name="Scrim", anchor=Anchor.FULL_RECT))
 56        scrim.bg_colour = Colour.rgba(0.0, 0.0, 0.0, 0.6)
 57        panel = self.overlay.add_child(Panel(name="MenuPanel", anchor=Anchor.CENTER, size=(320, 300)))
 58        panel.bg_colour = Colour.hex("#1A1A2E")
 59        panel.add_child(
 60            Label("Paused", anchor=Anchor.TOP_WIDE, inset=(0, 16), size=(0, 36), font_size=24.0, alignment="centre")
 61        )
 62
 63        # A top inset on a CENTER anchor moves the centre down by half of it,
 64        # into the room below the title.
 65        buttons = panel.add_child(
 66            VBoxContainer(anchor=Anchor.CENTER, inset=(0, 40, 0, 0), size=(220, 140), separation=12.0)
 67        )
 68        for text, handler in (("Resume", self.toggle), ("Restart", self.restart), ("Quit", self.quit)):
 69            button = buttons.add_child(Button(text, name=f"{text}Button", size=(220, 36)))
 70            button.pressed.connect(handler)
 71
 72    @on_input("pause")
 73    def toggle(self, event=None):
 74        self.tree.paused = not self.tree.paused
 75        self.overlay.visible = self.tree.paused
 76
 77    def restart(self):
 78        self.restart_requested.emit()
 79        self.toggle()
 80
 81    def quit(self):
 82        self.app.quit()
 83
 84
 85class PauseMenuDemo(Node2D):
 86    """Moving background content under the menu; it pauses because it is PAUSABLE."""
 87
 88    dynamic = True  # on_draw reads the clock, which is not a Property
 89
 90    def on_ready(self):
 91        self.clock = 0.0
 92        hint = self.add_child(
 93            Label(
 94                "Press P or ESC to pause",
 95                anchor=Anchor.CENTER_TOP,
 96                inset=8,
 97                size=(320, 24),
 98                font_size=16.0,
 99                alignment="centre",
100            )
101        )
102        hint.text_colour = Colour.LIGHT_GRAY
103        menu = self.add_child(PauseMenu(name="PauseMenu", anchor=Anchor.FULL_RECT, update_mode=UpdateMode.ALWAYS))
104        menu.restart_requested.connect(self.restart)
105
106    def restart(self):
107        self.clock = 0.0
108
109    def on_update(self, dt):
110        self.clock += dt
111
112    def on_draw(self, renderer):
113        w, h = self.viewport_size
114        renderer.rect((0, 0), (w, h), fill=Colour.hex("#10131C"))
115        for radius, speed, size, colour in (
116            (160, 1.5, 36, "#4FC3F7"),
117            (110, -0.9, 26, "#F4A261"),
118            (215, 0.6, 18, "#9B5DE5"),
119        ):
120            x = w / 2 + math.cos(self.clock * speed) * radius
121            y = h / 2 + math.sin(self.clock * speed) * radius * 0.75
122            renderer.rect((x - size / 2, y - size / 2), (size, size), fill=Colour.hex(colour))
123
124
125if __name__ == "__main__":
126    App(title="SimVX Pause Menu", width=800, height=600).run(PauseMenuDemo())