Timer - UQcsse3200/2026-studio-4 GitHub Wiki
Timer System
Overview
The timer system tracks how long a run has lasted and how long the player has spent in each dungeon. It feeds the HUD, achievements, the save file, and the load screen.
| Class | Package | Role |
|---|---|---|
GameTime |
services |
Frame delta and time scale |
RunTimer |
services |
Total run time and per-dungeon times |
TimerDisplay |
components.gamearea |
HUD panel showing the times |
RoomManager |
components.rooms |
Starts and stops dungeon timing as rooms change |
GameTime
GameTime wraps libGDX's frame delta.
| Method | Returns |
|---|---|
getDeltaTime() |
Seconds since the last frame, multiplied by the time scale |
getRawDeltaTime() |
Seconds since the last frame, ignoring the time scale |
getTime() |
Milliseconds since this GameTime was created |
getTimeSince(lastTime) |
getTime() - lastTime |
setTimeScale(scale) |
1.0 is normal speed and 0.0 stops time |
RunTimer
State
- Run clock (
totalTime): seconds elapsed in the run. - Dungeon clock (
dungeonTime): seconds in the dungeon currently being timed. - Dungeon times (
dungeonTimes): a map of dungeon id to seconds, kept in insertion order.
Lifecycle
| Method | Behaviour |
|---|---|
startRun() |
Resets everything and starts the run clock. Does nothing if the run is already running. |
registerDungeon(id) |
Adds the dungeon with 0 seconds so unvisited dungeons still show in the save list. Ignores null or blank ids. |
startDungeon(id) |
Stops any running dungeon, then continues from that dungeon's stored time. Does nothing if that dungeon is already running. |
stopDungeon() |
Stores the current dungeon's time and clears the current dungeon id. |
update() |
Called once per frame from MainGameScreen.render(). Adds the delta to the run clock and recalculates the dungeon clock. |
stopRun() |
Stops the run clock and the current dungeon. Called when the player dies. |
restoreRun(seconds, dungeonTimes) |
Loads saved times, then continues counting. Throws IllegalArgumentException for negative, NaN or infinite values, or for blank dungeon ids. |
How dungeon time works
The dungeon clock is derived from the run clock rather than counted separately:
dungeonStartTotal = totalTime - savedDungeonTime // set in startDungeon
dungeonTime = totalTime - dungeonStartTotal // set every update
So re-entering a dungeon continues from its previous time, and both clocks always advance together.
Delta clamp
update() caps each frame's delta at 0.25f seconds (MAX_DELTA). A long hitch, such as dragging the window, therefore adds at most a quarter of a second.
Pausing
requestPause() sets a flag and requestResume() clears it. While paused, update() returns early and neither clock advances. Pausing is separate from GameTime.setTimeScale.
Synced display time
getDungeonSyncedTime() returns the dungeon time aligned to the run clock's whole-second boundaries, so the run and dungeon labels tick over in the same frame.
Formatting
formatTimeWithNoMilliSec(seconds)returnsMM:SS.formatTime(seconds)returnsMM:SS.hh.
Minutes do not roll over into hours, so 100 minutes displays as 100:00.
TimerDisplay (HUD)
- It is a panel in the bottom-left corner of the screen, scaled to 0.9.
- It shows
RUN:with the total time, andDUNGEONwith the dungeon time. - Outside a dungeon the dungeon row shows
--:--. Inside one the caption becomesDUNGEON: <n>, using the mapping below. - Initial visibility comes from
UserSettings.get().showTimer. - Pressing T (handled by
TimerDisplay.ToggleInput) shows or hides the panel. The timers keep running while it is hidden, and the choice is not saved to settings.
| Dungeon id | Label |
|---|---|
dungeonOne to dungeonFour |
1 to 4 |
finalDungeon |
END |
| anything else | empty |
Integration
RoomManager
- The constructor registers every room's
dungeonId. create()starts timing the starting room's dungeon.switchToRoom(...)callsstopDungeon()and thenstartDungeon(...)when the dungeon id changes.completeDungeon()passesgetDungeonTime()to the achievement system as the clear time.
Saving and loading
- On save,
playTimeSecondsis set togetTotalTime()anddungeonTimesSecondstogetDungeonTimes(). That method includes the dungeon currently being timed, so nostopDungeon()call is needed first. - On load,
MainGameScreencallsrestoreRun(playTimeSeconds, dungeonTimesSeconds). - The load screen shows play time as
MM:SSand the per-dungeon times as a|-separated list. See [Save System](Save-System).
UML diagrams
Class diagram
classDiagram
direction LR
class GameTime {
-long startTime
-float timeScale
+setTimeScale(float)
+getDeltaTime() float
+getRawDeltaTime() float
+getTime() long
+getTimeSince(long) long
}
class RunTimer {
-float totalTime
-float dungeonTime
-Map~String,Float~ dungeonTimes
-String currentDungeonId
-boolean runRunning
-boolean dungeonRunning
-boolean paused
-float dungeonStartTotal
-float dungeonSyncedTime
-MAX_DELTA = 0.25f
+startRun()
+restoreRun(float, Map)
+registerDungeon(String)
+startDungeon(String)
+stopDungeon()
+stopRun()
+update()
+requestPause()
+requestResume()
+getTotalTime() float
+getDungeonTime() float
+getDungeonTimes() Map
+getDungeonSyncedTime() float
+getCurrentDungeonId() String
+formatTime(float) String
+formatTimeWithNoMilliSec(float) String
}
class TimerDisplay {
-RunTimer runTimer
-boolean visible
-Label runTime
-Label dungeonTime
-Label dungeonCaption
+create()
+draw(SpriteBatch)
+toggle()
+dispose()
}
class ToggleInput["TimerDisplay.ToggleInput"] {
+keyDown(int) boolean
}
class UIComponent
class InputComponent
class RoomManager {
-RunTimer runTimer
+create()
-switchToRoom(RoomConfig, PositionConfig)
-completeDungeon()
}
class MainGameScreen {
-RunTimer runTimer
+render(float)
-saveRun()
}
class GameSaveData {
+float playTimeSeconds
+Map~String,Float~ dungeonTimesSeconds
}
class LoadGameScreen {
-formatPlayTime(float) String
-formatDungeonTimes(Map) String
}
class ServiceLocator {
+getRunTimer() RunTimer
+registerRunTimer(RunTimer)
}
RunTimer --> GameTime : reads delta from
TimerDisplay --|> UIComponent
ToggleInput --|> InputComponent
TimerDisplay *-- ToggleInput : toggled by
ToggleInput --> TimerDisplay : toggle()
TimerDisplay ..> RunTimer : reads times
RoomManager ..> RunTimer : start/stop dungeon
MainGameScreen *-- RunTimer : creates and updates
MainGameScreen ..> GameSaveData : writes times on save
MainGameScreen ..> RunTimer : restoreRun on load
LoadGameScreen ..> GameSaveData : displays times
ServiceLocator o-- RunTimer
RoomManager ..> ServiceLocator : getRunTimer()
TimerDisplay ..> ServiceLocator : getRunTimer()
Sequence diagram: timer lifecycle
sequenceDiagram
participant MGS as MainGameScreen
participant RT as RunTimer
participant GT as GameTime
participant RM as RoomManager
participant TD as TimerDisplay
alt new game
MGS->>RT: startRun()
else loaded save
MGS->>RT: restoreRun(playTimeSeconds, dungeonTimesSeconds)
end
MGS->>RM: new RoomManager(...)
RM->>RT: registerDungeon(id) for each room
MGS->>RM: create()
RM->>RT: startDungeon(startRoom.dungeonId)
loop every frame
MGS->>RM: update()
MGS->>RT: update()
RT->>GT: getDeltaTime()
GT-->>RT: delta (clamped to 0.25s)
TD->>RT: getTotalTime() / getDungeonSyncedTime()
TD->>TD: refresh labels
end
Note over RM,RT: player changes room
RM->>RT: stopDungeon()
RM->>RT: startDungeon(newDungeonId)
Note over MGS,RT: Save and exit
MGS->>RT: getTotalTime()
MGS->>RT: getDungeonTimes()
MGS->>MGS: write to GameSaveData
State diagram: timer states
stateDiagram-v2
[*] --> Idle
Idle --> RunRunning : startRun() / restoreRun()
RunRunning --> DungeonRunning : startDungeon(id)
DungeonRunning --> DungeonRunning : startDungeon(other id), stops the old one first
DungeonRunning --> RunRunning : stopDungeon()
RunRunning --> Stopped : stopRun()
DungeonRunning --> Stopped : stopRun()
Stopped --> RunRunning : restoreRun()