Quick answer: set_timer fires on frame boundaries, accumulating sub-frame error. For long-running schedules, track elapsed time against a monotonic clock and correct.
A survival game spawns a wave every 30s via set_timer. After an hour, waves are noticeably out of sync with the displayed countdown — small per-tick rounding compounded.
Why It Drifts
set_timer posts an event every N milliseconds, but the event is only processed on the next frame boundary. At 60 FPS, that’s up to ~16ms of slop per fire. Over thousands of fires, it adds up.
Clock-Anchored Scheduling
import pygame
next_wave_at = pygame.time.get_ticks() + 30000
def update():
global next_wave_at
now = pygame.time.get_ticks()
if now >= next_wave_at:
spawn_wave()
next_wave_at += 30000 # anchor to the schedule, not "now"
Advancing next_wave_at += 30000 (not = now + 30000) keeps the schedule anchored. Drift never accumulates.
Catch-Up for Long Pauses
while now >= next_wave_at:
spawn_wave()
next_wave_at += 30000
If the game was paused or hitched, the while loop fires any missed waves. Optional — for some games you’d skip instead.
Display the Same Source
Compute the countdown UI from next_wave_at - now so the display and the spawn use one clock. They can never disagree.
Verifying
Run a long session (or fast-forward via a debug speed multiplier). Wave N fires at exactly 30N seconds. Countdown matches the spawn.
“Anchor schedules to absolute time, not to ‘now + interval’. That one change kills drift.”
For anything competitive or speedrun-timed, also use time.get_ticks() consistently — never mix it with frame-count-based timing.