pyodide: loading…

[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]

# example 01 · semi-transparent pause overlay

Draw the game underneath, then a dark semi-transparent surface on top. Looks polished with almost no effort.

1
2
3
4
5
6
7
8
9
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# example 02 · state transition diagram

Map out which states can transition to which. Helps you think through the flow before coding.

1
2
3
4
5
6
7
8
9
10
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# challenges [4]

# challenge 01/04todo
What's the advantage of using a state variable (like game_state = 'menu') instead of multiple boolean flags (show_menu = True, game_over = False)?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
🐍
Loading PythonSetting up pandas & numpy...
# challenge 02/04todo
When you pause the game, why do you still call draw_game() before drawing the pause overlay?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
🐍
Loading PythonSetting up pandas & numpy...
# challenge 03/04todo
What's the purpose of the reset_game() function? When should it be called?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
🐍
Loading PythonSetting up pandas & numpy...
# challenge 04/04todo
Why should you only call update_game(dt) when game_state is PLAYING, not during PAUSED or MENU?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
🐍
Loading PythonSetting up pandas & numpy...