Game speed control: pause and speed presets #113

Closed
opened 2026-09-18 13:42:32 +01:00 by liamjd · 0 comments
Owner

GameClock has carried speed and paused since #65, and nothing in the game sets either of them. There is no speed control, and the only thing that touches paused is the main menu.

This is a core control, not a debug affordance. The colony sim spends most of its time waiting: at TICKS_PER_DAY = 1200 and ticks_per_second = 10, one in-game day is 120 real seconds, so a sensor tower (build_days = 3) takes 6 real minutes to raise with one pawn and an ore mine (10) takes 20. Threat cycles are measured in days and seasons. A player who cannot skip ahead is watching an idle screen for most of the run, and a player who cannot pause cannot react to a forecast.

Two sources of pause

GameClock._on_main_menu_changed() assigns paused directly. If the player pauses deliberately and then opens and closes the menu, closing it will silently unpause them — the menu's state overwrites the player's.

Pause therefore needs two independent inputs and one derived answer:

  • menu_paused — the menu is open
  • player_paused — the player pressed pause
  • effective paused is either of them; leaving the menu restores whatever the player had chosen

Speed and determinism

speed scales how many ticks a real second produces; it does not change what a tick is. The tick sequence is identical at any speed, which is what keeps the deterministic threat timeline (and any future replay) intact. ticks_per_second stays locked after the clock starts, per #65 — speed is the runtime knob and the two should not be confused in the UI or the code.

One thing to watch: advance()'s while _accumulator >= 1.0 loop emits every due tick within the frame, so a high speed means a burst of ticks in one frame and, once #75 lands, a burst of work loops with them. Decide whether to cap ticks-per-frame (smooth, but the clock falls behind wall time under load) or leave it uncapped (honest, but can hitch). Note the decision in the PR either way.

Scope

  • Presets rather than a slider: pause, 1x, 3x, 10x. Exact values are a balance question; 10x turns a 20-minute mine into two minutes.
  • Keyboard first, following the existing action naming (camera_pan_left, place_building, system_debug): game_pause on space, game_speed_up / game_speed_down, or direct game_speed_1/_2/_3 on the number row.
  • On-screen control in CalendarPanel, which already owns the date display and already listens to GameClock.tick.
  • GameClock needs speed_changed(speed: float) and paused_changed(paused: bool) signals so the panel reflects changes it did not make itself — the menu being one of them.

AC

  • player_paused and menu_paused are separate; closing the menu restores the player's choice rather than resuming unconditionally
  • Speed presets selectable by keyboard and by on-screen control, both reflecting the current value
  • New input actions registered in project.godot, named to match the existing ones
  • speed_changed / paused_changed emitted, and the panel updates from them rather than polling
  • Changing speed does not change the tick sequence: a unit test advancing the clock by the same simulated duration at 1x and at 10x produces the same number of ticks
  • Pausing stops GameClock.now advancing and delta_for() returning anything but zero
  • Decision on ticks-per-frame capping recorded in the PR

Unblocks watching #75's work loop and #71's construction states actually happen.

`GameClock` has carried `speed` and `paused` since #65, and **nothing in the game sets either of them**. There is no speed control, and the only thing that touches `paused` is the main menu. This is a core control, not a debug affordance. The colony sim spends most of its time waiting: at `TICKS_PER_DAY = 1200` and `ticks_per_second = 10`, one in-game day is **120 real seconds**, so a sensor tower (`build_days = 3`) takes 6 real minutes to raise with one pawn and an ore mine (10) takes 20. Threat cycles are measured in days and seasons. A player who cannot skip ahead is watching an idle screen for most of the run, and a player who cannot pause cannot react to a forecast. ## Two sources of pause `GameClock._on_main_menu_changed()` assigns `paused` directly. If the player pauses deliberately and then opens and closes the menu, closing it will silently *unpause* them — the menu's state overwrites the player's. Pause therefore needs two independent inputs and one derived answer: - `menu_paused` — the menu is open - `player_paused` — the player pressed pause - effective `paused` is either of them; leaving the menu restores whatever the player had chosen ## Speed and determinism `speed` scales how many ticks a real second produces; it does not change what a tick *is*. The tick sequence is identical at any speed, which is what keeps the deterministic threat timeline (and any future replay) intact. `ticks_per_second` stays locked after the clock starts, per #65 — `speed` is the runtime knob and the two should not be confused in the UI or the code. One thing to watch: `advance()`'s `while _accumulator >= 1.0` loop emits every due tick within the frame, so a high speed means a burst of ticks in one frame and, once #75 lands, a burst of work loops with them. Decide whether to cap ticks-per-frame (smooth, but the clock falls behind wall time under load) or leave it uncapped (honest, but can hitch). Note the decision in the PR either way. ## Scope - Presets rather than a slider: pause, 1x, 3x, 10x. Exact values are a balance question; 10x turns a 20-minute mine into two minutes. - Keyboard first, following the existing action naming (`camera_pan_left`, `place_building`, `system_debug`): `game_pause` on space, `game_speed_up` / `game_speed_down`, or direct `game_speed_1`/`_2`/`_3` on the number row. - On-screen control in `CalendarPanel`, which already owns the date display and already listens to `GameClock.tick`. - `GameClock` needs `speed_changed(speed: float)` and `paused_changed(paused: bool)` signals so the panel reflects changes it did not make itself — the menu being one of them. ## AC - `player_paused` and `menu_paused` are separate; closing the menu restores the player's choice rather than resuming unconditionally - Speed presets selectable by keyboard and by on-screen control, both reflecting the current value - New input actions registered in `project.godot`, named to match the existing ones - `speed_changed` / `paused_changed` emitted, and the panel updates from them rather than polling - Changing speed does not change the tick sequence: a unit test advancing the clock by the same simulated duration at 1x and at 10x produces the same number of ticks - Pausing stops `GameClock.now` advancing and `delta_for()` returning anything but zero - Decision on ticks-per-frame capping recorded in the PR Unblocks watching #75's work loop and #71's construction states actually happen.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder#113
No description provided.