[practice]Game Development with Pygame
Game State Management
# theory
state management
Right now your games probably have a single game loop that handles everything. But real games have multiple screens: a title screen, the actual gameplay, a pause menu, a game-over screen. Without state management, your code turns into a tangled mess of if show_menu and if game_over flags scattered everywhere.
The fix is simple: use a state variable and organize your code around it.
state pattern
# Define your states
STATE_MENU = "menu"
STATE_PLAYING = "playing"
STATE_PAUSED = "paused"
STATE_GAME_OVER = "game_over"
STATE_WIN = "win"
# Current state
game_state = STATE_MENU
Then in your game loop, check the state and run only the relevant code:
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Handle input differently per state
if game_state == STATE_MENU:
handle_menu_input(event)
elif game_state == STATE_PLAYING:
handle_game_input(event)
elif game_state == STATE_PAUSED:
handle_pause_input(event)
elif game_state == STATE_GAME_OVER:
handle_game_over_input(event)
# Update
if game_state == STATE_PLAYING:
update_game(dt)
# Draw
screen.fill((0, 0, 0))
if game_state == STATE_MENU:
draw_menu(screen)
elif game_state == STATE_PLAYING:
draw_game(screen)
elif game_state == STATE_PAUSED:
draw_game(screen) # draw game underneath
draw_pause_overlay(screen) # then overlay
elif game_state == STATE_GAME_OVER:
draw_game_over(screen)
pygame.display.flip()
That's the entire pattern. Each state gets its own input handler, its own update logic, and its own draw function. Clean separation.
state transitions
States change when specific things happen:
# Menu → Playing (player presses Enter)
def handle_menu_input(event):
global game_state
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
game_state = STATE_PLAYING
reset_game() # initialize game variables
# Playing → Paused (player presses Escape)
def handle_game_input(event):
global game_state
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
game_state = STATE_PAUSED
# Paused → Playing (player presses Escape again)
def handle_pause_input(event):
global game_state
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
game_state = STATE_PLAYING # resume
# Playing → Game Over (player dies)
def update_game(dt):
global game_state
# ... game logic ...
if lives <= 0:
game_state = STATE_GAME_OVER
# Game Over → Menu (player presses R to restart)
def handle_game_over_input(event):
global game_state
if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
game_state = STATE_MENU
drawing each state
Menu Screen
def draw_menu(screen):
title_font = pygame.font.SysFont(None, 72)
sub_font = pygame.font.SysFont(None, 28)
title = title_font.render("MY GAME", True, (255, 255, 255))
title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 200))
screen.blit(title, title_rect)
prompt = sub_font.render("Press ENTER to start", True, (180, 180, 180))
prompt_rect = prompt.get_rect(center=(SCREEN_WIDTH // 2, 350))
screen.blit(prompt, prompt_rect)
Pause Overlay
def draw_pause_overlay(screen):
# Semi-transparent dark overlay
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
overlay.set_alpha(128)
overlay.fill((0, 0, 0))
screen.blit(overlay, (0, 0))
font = pygame.font.SysFont(None, 48)
text = font.render("PAUSED", True, (255, 255, 255))
text_rect = text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
screen.blit(text, text_rect)
sub = pygame.font.SysFont(None, 24)
hint = sub.render("Press ESC to resume", True, (180, 180, 180))
hint_rect = hint.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 40))
screen.blit(hint, hint_rect)
Game Over Screen
def draw_game_over(screen):
font = pygame.font.SysFont(None, 64)
text = font.render("GAME OVER", True, (255, 50, 50))
text_rect = text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 30))
screen.blit(text, text_rect)
score_font = pygame.font.SysFont(None, 32)
score_text = score_font.render(f"Final Score: {score}", True, (255, 255, 255))
score_rect = score_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 20))
screen.blit(score_text, score_rect)
hint = score_font.render("Press R to restart", True, (180, 180, 180))
hint_rect = hint.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 60))
screen.blit(hint, hint_rect)
reset_game(): the restart function
When transitioning from Game Over back to Playing, you need to reset everything:
def reset_game():
global score, lives, level, ball_pos, ball_vel
score = 0
lives = 3
level = 1
ball_pos = pygame.math.Vector2(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
ball_vel = pygame.math.Vector2(3, -3)
# Reset any other game objects, sprites, etc.
Common mistake: Forgetting to reset something. The player restarts and an old brick is still missing, or the score isn't zero. Always reset EVERYTHING.
Gerber tip: keep it simple
You don't need a fancy state machine library. A string variable and some if/elif blocks is all most games need. Don't over-engineer it. The pattern above handles menus, pausing, game over, and win screens for any Pygame project.
If your game gets really complex (multiple levels, cutscenes, shops), you might want to use a class-based approach where each state is its own class with handle_input(), update(), and draw() methods. But for the projects in this course, the simple approach works perfectly.
# examples [2]
Draw the game underneath, then a dark semi-transparent surface on top. Looks polished with almost no effort.
pygame needs a real window — copy this into a .py file and run it locally.
Map out which states can transition to which. Helps you think through the flow before coding.
pygame needs a real window — copy this into a .py file and run it locally.
# challenges [4]