Quick answer: Use pygame.time.get_ticks() as the authoritative clock; accumulating frame deltas drifts. For fixed-rate simulation, use an accumulator pattern: accumulate dt, step physics by fixed dt until spent.
Here is how to fix Pygame clock tick drifting over time. Your rhythm game has a 120 BPM metronome based on accumulating frame deltas. After a minute, notes are half a beat off. After five, they are off by seconds. Frame deltas from Clock.tick are imprecise, and accumulating imprecision produces drift.
The Symptom
In-game time drifts from real time. Rhythm game notes fall out of sync with music. Timer-based effects expire too early or late. Comparing in-game seconds to stopwatch seconds shows growing divergence.
What Causes This
Frame delta imprecision. dt = clock.tick(60) / 1000.0 gives the milliseconds since last tick divided by 1000 — each tick value is an integer millisecond. Fractional milliseconds are truncated. Over 10000 ticks at 16-17 ms each, rounding error accumulates to 100+ ms drift.
OS sleep granularity. Clock.tick uses sleep to throttle framerate. On Windows, sleep precision is ~15 ms. Frames do not land exactly at 16.67 ms intervals — they land at 15, 16, or 31 ms. Accumulating over hours produces noticeable drift.
Delta-based time tracking. total_time += dt every frame. Each dt is approximate. Sum grows imprecise. Using total_time for game logic inherits all that imprecision.
The Fix
Step 1: Use absolute ticks as clock.
import pygame
pygame.init()
start_time_ms = pygame.time.get_ticks()
clock = pygame.time.Clock()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Absolute game time — never drifts
game_time = (pygame.time.get_ticks() - start_time_ms) / 1000.0
# Use game_time for logic, not accumulated dt
beat_index = int(game_time * beats_per_second)
# Render at whatever FPS we can
screen.fill((0, 0, 0))
render(screen, beat_index)
pygame.display.flip()
clock.tick(60)
get_ticks() is the authoritative monotonic clock. Derive game time from it, not from accumulating dt. Zero drift.
Step 2: Fixed timestep for physics. For simulation that must be deterministic:
FIXED_DT = 1.0 / 60.0 # 60 Hz physics
accumulator = 0.0
last_ms = pygame.time.get_ticks()
while running:
now_ms = pygame.time.get_ticks()
frame_dt = (now_ms - last_ms) / 1000.0
last_ms = now_ms
accumulator += frame_dt
while accumulator >= FIXED_DT:
physics_step(FIXED_DT)
accumulator -= FIXED_DT
# Render with remaining alpha for interpolation
alpha = accumulator / FIXED_DT
render_interpolated(alpha)
pygame.display.flip()
clock.tick(60)
Physics runs at fixed 60 Hz regardless of render rate. Accumulator handles variable frame times. Render interpolates between physics states for visual smoothness.
Step 3: Use tick_busy_loop for precise timing. If low-CPU is not a priority:
clock.tick_busy_loop(60) # precise but CPU-intensive
Spins the CPU instead of sleeping. Eliminates sleep granularity issues. Use for short timing-critical sessions, not for shipped games (battery drain).
Step 4: For music sync, use audio library’s clock. If syncing to music playback (rhythm game), pygame.mixer.Sound.play() or pygame.mixer.music.get_pos() provide audio-accurate positions. These follow the sound card’s clock, not the game loop, so they stay in sync with what the player hears.
pygame.mixer.music.load("beat.ogg")
pygame.mixer.music.play()
# Later, in your game loop:
music_pos_ms = pygame.mixer.music.get_pos()
if music_pos_ms >= 0: # -1 if not playing
music_time_s = music_pos_ms / 1000.0
# Sync visual beats to music_time_s, not to render dt
“Delta accumulation lies over time. Absolute clock doesn’t. For anything that must stay in sync, query the absolute clock.”
Related Issues
For Pygame alpha rendering, see Surface Blit Alpha Not Transparent. For similar timestep concepts, Unity FixedUpdate Multiple Times Per Frame.
pygame.time.get_ticks() for absolute. Accumulator for simulation. music.get_pos() for audio sync.