Quick answer: Replace clock.tick_busy_loop(60) with clock.tick(60). Busy-loop spins to hit the deadline exactly; regular tick sleeps the thread and frees the core for other work.
Your turn-based game spends 90% of its time waiting for the player’s input. Yet a CPU core is permanently at 100%, the laptop’s fan is running, the battery drains in 2 hours. The culprit is the frame pacer not sleeping the thread.
How Pygame Schedules Frames
Pygame’s Clock object has two pacing methods:
- tick(fps) — calls OS sleep until enough time has passed for the next frame. ~1ms accuracy on most platforms.
- tick_busy_loop(fps) — spins in a tight while loop reading the clock until the deadline. Sub-microsecond accuracy, but the CPU is occupied the entire time.
Some online tutorials recommend tick_busy_loop because it produces smoother frame times in microbenchmarks. The reality is that on any modern OS, the 1ms jitter of tick is invisible to players, and the CPU savings are dramatic.
The Fix
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# update and draw...
pygame.display.flip()
clock.tick(60) # NOT tick_busy_loop
pygame.quit()
clock.tick(60) sleeps the thread for whatever fraction of 1/60 second remains. The CPU is free for other processes (or for staying idle). Frame time variance increases from ~0.05ms to ~1ms, which is invisible on a 60 Hz display refresh.
VSYNC: Even Better
For lowest CPU and best visual smoothness, enable hardware vsync at display setup:
screen = pygame.display.set_mode(
(800, 600),
pygame.SCALED | pygame.DOUBLEBUF,
vsync=1
)
With vsync=1, display.flip() blocks until the next monitor refresh. You can still call clock.tick(60) for delta-time measurement, but the actual frame pacing is dictated by the monitor — resulting in tear-free output and minimum CPU.
Profiling Frame Time
If you switched to tick but CPU is still high, your loop body is the bottleneck. Measure:
import time
frame_start = time.perf_counter()
# ... your update and draw ...
frame_end = time.perf_counter()
frame_ms = (frame_end - frame_start) * 1000
if frame_ms > 14:
print(f"slow frame: {frame_ms:.1f}ms")
Frame times consistently >15ms mean tick has nothing to wait on; the work is filling the budget. Use cProfile to find slow code:
python -m cProfile -s cumtime your_game.py | head -30
When tick_busy_loop Might Make Sense
Niche case: a rhythm game that synchronizes audio to sub-millisecond input. Even there, audio-driven timing (callback from pygame.mixer.music) outperforms frame-locked timing for the same goal. The honest answer is: stop using tick_busy_loop for game frame pacing.
Verifying
Open Task Manager / Activity Monitor / top. The Python process CPU should drop from ~100% per core to under 10% when the game is idle on a menu screen. The fan should quiet down within seconds. Frame counter readouts should remain at 60 FPS.
“A CPU core at 100% when nothing’s happening on screen is a tick_busy_loop. Switch to tick and watch the temperature drop.”
Default to vsync=1 + clock.tick(60). Reserve tick_busy_loop for cases you can articulate a specific reason.