Ce que vous allez apprendre
Dans ce chapitre, nous ajoutons une machine a etats au jeu. Au lieu d'un seul mode de fonctionnement, le programme connait maintenant cinq etats distincts : MENU, PLAYING, PAUSED, GAME_OVER et YOU_WIN. Chaque etat a son propre comportement d'affichage et de gestion des touches.
Le concept
Une machine a etats (state machine) est un patron de conception tres courant dans les jeux video. L'idee est simple : le jeu se trouve toujours dans exactement un etat. Les entrees de l'utilisateur et les evenements du jeu provoquent des transitions d'un etat a un autre.
Par exemple :
- MENU : le joueur voit le titre et appuie sur ESPACE pour passer a PLAYING.
- PLAYING : le jeu tourne normalement. Appuyer sur P passe a PAUSED. Perdre toutes ses vies passe a GAME_OVER. Detruire toutes les briques passe a YOU_WIN.
- PAUSED : le jeu est fige. Appuyer sur P reprend le jeu (retour a PLAYING).
- GAME_OVER et YOU_WIN : ecrans de fin. ESPACE retourne au MENU.
L'implementation la plus simple utilise des constantes entieres et une variable game_state :
STATE_MENU = 0
STATE_PLAYING = 1
STATE_PAUSED = 2
STATE_GAME_OVER = 3
STATE_YOU_WIN = 4
game_state = STATE_MENU
Etape par etape
1. Definir les etats
On definit cinq constantes au debut du programme. La variable game_state commence a STATE_MENU pour que le joueur voie le menu au lancement.
2. Gestion des touches par etat
La gestion des evenements clavier depend de l'etat courant. On utilise des blocs if/elif pour separer la logique de chaque etat :
if game_state == STATE_MENU:
if event.key == pygame.K_SPACE:
start_game()
elif game_state == STATE_PLAYING:
if event.key == pygame.K_p:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
if event.key == pygame.K_p:
game_state = STATE_PLAYING
elif game_state in (STATE_GAME_OVER, STATE_YOU_WIN):
if event.key == pygame.K_SPACE:
game_state = STATE_MENU
3. L'ecran du menu
Le menu affiche le titre du jeu et un texte clignotant invitant le joueur a appuyer sur ESPACE. Le clignotement est obtenu en alternant l'affichage toutes les demi-secondes.
def draw_menu():
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
screen.fill(BG_COLOR)
title = big_font.render("BRICK BREAKER",
True, (255, 200, 50))
screen.blit(title, ...)
# Blinking text
if int(menu_blink_timer * 2) % 2 == 0:
start_text = medium_font.render(
"Press SPACE to start", True, (255, 255, 255))
screen.blit(start_text, ...)
4. La pause
Quand l'etat est PAUSED, on dessine le jeu normalement puis on superpose un voile semi-transparent avec le texte «PAUSED». Le jeu ne se met plus a jour (pas d'appel a update ni de physique).
5. Dessiner selon l'etat
La section de dessin utilise la meme structure if/elif :
if game_state == STATE_MENU:
draw_menu()
elif game_state == STATE_PLAYING:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
elif game_state == STATE_PAUSED:
# ... draw game then overlay
draw_paused()
elif game_state == STATE_GAME_OVER:
# ... draw game then overlay
draw_game_over()
Points cles a retenir
Astuce : en separant chaque etat dans sa propre fonction de dessin, le code reste lisible meme avec beaucoup d'etats.
Piege courant : oublier de bloquer les mises a jour pendant la pause. Si on oublie le if game_state == STATE_PLAYING autour du code de physique, le jeu continuera de tourner en arriere-plan.
Patron utile : la machine a etats peut evoluer vers un dictionnaire de fonctions ou meme des classes d'etat, mais pour un casse-briques, de simples constantes suffisent.
Code complet
"""Brick Breaker - Chapter 11: Game States"""
import pygame
import sys
import math
import array
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
BALL_RADIUS = 8
BALL_BASE_SPEED = 300
BALL_SPEED_INCREMENT = 10
BRICK_ROWS = 6
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50
MAX_LIVES = 3
# ── Game states ────────────────────────────────────────────────────
STATE_MENU = 0
STATE_PLAYING = 1
STATE_PAUSED = 2
STATE_GAME_OVER = 3
STATE_YOU_WIN = 4
# ── Sound generation ────────────────────────────────────────────────
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
sample_rate = 44100
n_samples = int(sample_rate * duration_ms / 1000)
buf = array.array("h", [0] * n_samples)
max_amp = int(32767 * volume)
for i in range(n_samples):
t = i / sample_rate
value = int(max_amp * math.sin(2 * math.pi * frequency * t))
fade_samples = min(500, n_samples // 4)
if i >= n_samples - fade_samples:
value = int(value * (n_samples - i) / fade_samples)
buf[i] = value
return pygame.mixer.Sound(buffer=buf)
snd_paddle = generate_beep(frequency=500, duration_ms=60, volume=0.3)
snd_wall = generate_beep(frequency=300, duration_ms=50, volume=0.2)
snd_brick = generate_beep(frequency=700, duration_ms=80, volume=0.3)
snd_lose_life = generate_beep(frequency=150, duration_ms=300, volume=0.4)
snd_game_over = generate_beep(frequency=100, duration_ms=500, volume=0.5)
snd_win = generate_beep(frequency=880, duration_ms=400, volume=0.4)
# ── Helper functions ────────────────────────────────────────────────
def rainbow_color(row, total_rows):
hue = int(360 * row / total_rows)
color = pygame.Color(0)
color.hsva = (hue, 100, 100, 100)
return (color.r, color.g, color.b)
# ── Sprite classes ──────────────────────────────────────────────────
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((PADDLE_WIDTH, PADDLE_HEIGHT), pygame.SRCALPHA)
for y in range(PADDLE_HEIGHT):
brightness = 200 + int(55 * (1 - y / PADDLE_HEIGHT))
pygame.draw.line(self.image, (brightness, brightness, brightness),
(0, y), (PADDLE_WIDTH, y))
pygame.draw.rect(self.image, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(self.image, (0, 0, 0, 0), (PADDLE_WIDTH - 3, 0, 3, 3))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.y = SCREEN_HEIGHT - 40
self.use_mouse = False
def update(self):
if self.use_mouse:
mx, _ = pygame.mouse.get_pos()
self.rect.centerx = mx
else:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.rect.x -= PADDLE_SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.rect.x += PADDLE_SPEED
self.rect.left = max(0, self.rect.left)
self.rect.right = min(SCREEN_WIDTH, self.rect.right)
class Ball(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
size = BALL_RADIUS * 4
self.image = pygame.Surface((size, size), pygame.SRCALPHA)
center = size // 2
for r in range(BALL_RADIUS * 2, BALL_RADIUS, -1):
alpha = int(100 * (1 - (r - BALL_RADIUS) / BALL_RADIUS))
pygame.draw.circle(self.image, (100, 100, 255, alpha), (center, center), r)
pygame.draw.circle(self.image, (255, 255, 255), (center, center), BALL_RADIUS)
pygame.draw.circle(self.image, (255, 255, 255, 200),
(center - 2, center - 2), BALL_RADIUS // 3)
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def reset(self):
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def update(self, dt):
self.float_x += self.vx * dt
self.float_y += self.vy * dt
self.rect.center = (int(self.float_x), int(self.float_y))
def bounce_wall(self):
bounced = False
if self.float_x - BALL_RADIUS <= 0:
self.float_x = BALL_RADIUS
self.vx = abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if self.float_x + BALL_RADIUS >= SCREEN_WIDTH:
self.float_x = SCREEN_WIDTH - BALL_RADIUS
self.vx = -abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if self.float_y - BALL_RADIUS <= 0:
self.float_y = BALL_RADIUS
self.vy = abs(self.vy)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if bounced:
self.rect.center = (int(self.float_x), int(self.float_y))
return bounced
def fell_off_bottom(self):
return self.float_y - BALL_RADIUS > SCREEN_HEIGHT
def bounce_off_paddle(self, paddle):
if self.rect.colliderect(paddle.rect) and self.vy > 0:
self.float_y = paddle.rect.top - BALL_RADIUS
hit_pos = (self.float_x - paddle.rect.left) / PADDLE_WIDTH
angle = 150 - hit_pos * 120
rad = math.radians(angle)
self.speed += BALL_SPEED_INCREMENT
self.vx = self.speed * math.cos(rad)
self.vy = -abs(self.speed * math.sin(rad))
self.rect.center = (int(self.float_x), int(self.float_y))
return True
return False
class Brick(pygame.sprite.Sprite):
def __init__(self, x, y, color, row):
super().__init__()
self.image = pygame.Surface((BRICK_WIDTH, BRICK_HEIGHT), pygame.SRCALPHA)
pygame.draw.rect(self.image, color, (0, 0, BRICK_WIDTH, BRICK_HEIGHT))
highlight = tuple(min(255, c + 50) for c in color)
pygame.draw.line(self.image, highlight, (1, 1), (BRICK_WIDTH - 2, 1))
pygame.draw.line(self.image, highlight, (1, 1), (1, BRICK_HEIGHT - 2))
shadow = tuple(max(0, c - 50) for c in color)
pygame.draw.line(self.image, shadow, (1, BRICK_HEIGHT - 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
pygame.draw.line(self.image, shadow, (BRICK_WIDTH - 1, 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect(topleft=(x, y))
self.row = row
self.color = color
def score_value(self):
return (BRICK_ROWS - self.row) * 10
def create_brick_group():
group = pygame.sprite.Group()
for row in range(BRICK_ROWS):
color = rainbow_color(row, BRICK_ROWS)
for col in range(BRICK_COLS):
x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
group.add(Brick(x, y, color, row))
return group
# ── Game setup ──────────────────────────────────────────────────────
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 11")
clock = pygame.time.Clock()
hud_font = pygame.font.SysFont("monospace", 18)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 14)
paddle = Paddle()
ball = Ball()
bricks = create_brick_group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
lives = MAX_LIVES
score = 0
level = 1
game_state = STATE_MENU
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
def start_game():
"""Reset everything and switch to PLAYING state."""
global lives, score, level, bricks, game_state
lives = MAX_LIVES
score = 0
level = 1
bricks = create_brick_group()
ball.reset()
game_state = STATE_PLAYING
# ── Draw helpers for each state ─────────────────────────────────────
def draw_menu():
"""Draw the title menu screen."""
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
screen.fill(BG_COLOR)
# Title
title = big_font.render("BRICK BREAKER", True, (255, 200, 50))
screen.blit(title, (SCREEN_WIDTH // 2 - title.get_width() // 2,
SCREEN_HEIGHT // 3 - 30))
# Blinking "Press SPACE to start"
if int(menu_blink_timer * 2) % 2 == 0:
start_text = medium_font.render("Press SPACE to start", True, (255, 255, 255))
screen.blit(start_text, (SCREEN_WIDTH // 2 - start_text.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
# Instructions
instr_lines = [
"[LEFT/RIGHT] or [A/D] Move paddle",
"[M] Toggle mouse control",
"[P] Pause game",
"[F1] Mute [+/-] Volume",
"[ESC] Quit",
]
y_offset = SCREEN_HEIGHT // 2 + 80
for line in instr_lines:
surf = small_font.render(line, True, (150, 150, 150))
screen.blit(surf, (SCREEN_WIDTH // 2 - surf.get_width() // 2, y_offset))
y_offset += 20
def draw_paused():
"""Draw the pause overlay on top of the game."""
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
pause_text = big_font.render("PAUSED", True, (200, 200, 255))
screen.blit(pause_text, (SCREEN_WIDTH // 2 - pause_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
resume_text = medium_font.render("Press P to resume", True, (200, 200, 200))
screen.blit(resume_text, (SCREEN_WIDTH // 2 - resume_text.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
def draw_game_over():
"""Draw the game over overlay."""
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
go_text = big_font.render("GAME OVER", True, (255, 60, 60))
screen.blit(go_text, (SCREEN_WIDTH // 2 - go_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
final_score = medium_font.render("Final score: {}".format(score), True, (255, 255, 255))
screen.blit(final_score, (SCREEN_WIDTH // 2 - final_score.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
restart = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
screen.blit(restart, (SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
def draw_you_win():
"""Draw the victory overlay."""
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
win_text = big_font.render("YOU WIN!", True, (100, 255, 100))
screen.blit(win_text, (SCREEN_WIDTH // 2 - win_text.get_width() // 2,
SCREEN_HEIGHT // 2 - 40))
final_score = medium_font.render("Final score: {}".format(score), True, (255, 255, 255))
screen.blit(final_score, (SCREEN_WIDTH // 2 - final_score.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
restart = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
screen.blit(restart, (SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
def draw_hud():
"""Draw the heads-up display."""
score_surf = hud_font.render("Score: {}".format(score), True, (255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render("Lives: {}".format(lives), True, (255, 100, 100))
screen.blit(lives_surf, (SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render("Level: {}".format(level), True, (100, 200, 255))
screen.blit(level_surf, (SCREEN_WIDTH - level_surf.get_width() - 10, 10))
vol_label = "MUTED" if sound_muted else "Vol: {}".format(int(master_volume * 100))
vol_color = (255, 80, 80) if sound_muted else (150, 150, 150)
vol_surf = small_font.render(vol_label, True, vol_color)
screen.blit(vol_surf, (SCREEN_WIDTH - vol_surf.get_width() - 10, 30))
# ── Main loop ───────────────────────────────────────────────────────
running = True
while running:
dt = clock.tick(FPS) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
# Volume controls (always active)
if event.key == pygame.K_F1:
sound_muted = not sound_muted
if event.key in (pygame.K_PLUS, pygame.K_KP_PLUS, pygame.K_EQUALS):
master_volume = min(1.0, master_volume + 0.1)
if event.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
master_volume = max(0.0, master_volume - 0.1)
# State-specific key handling
if game_state == STATE_MENU:
if event.key == pygame.K_SPACE:
start_game()
elif game_state == STATE_PLAYING:
if event.key == pygame.K_m:
paddle.use_mouse = not paddle.use_mouse
if event.key == pygame.K_p:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
if event.key == pygame.K_p:
game_state = STATE_PLAYING
elif game_state in (STATE_GAME_OVER, STATE_YOU_WIN):
if event.key == pygame.K_SPACE:
game_state = STATE_MENU
# ── Update ──────────────────────────────────────────────────
if game_state == STATE_PLAYING:
paddle_group.update()
ball.update(dt)
if ball.bounce_wall():
play_sound(snd_wall)
if ball.fell_off_bottom():
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
else:
play_sound(snd_lose_life)
ball.reset()
if ball.bounce_off_paddle(paddle):
play_sound(snd_paddle)
hit_bricks = pygame.sprite.spritecollide(ball, bricks, True)
if hit_bricks:
for brick in hit_bricks:
score += brick.score_value()
ball.vy = -ball.vy
ball.speed += BALL_SPEED_INCREMENT
play_sound(snd_brick)
if len(bricks) == 0:
game_state = STATE_YOU_WIN
play_sound(snd_win)
# ── Draw ────────────────────────────────────────────────────
if game_state == STATE_MENU:
draw_menu()
elif game_state == STATE_PLAYING:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
info = small_font.render(
"[M] Mouse/Kbd [P] Pause [F1] Mute [+/-] Volume [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
elif game_state == STATE_PAUSED:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_paused()
elif game_state == STATE_GAME_OVER:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_game_over()
elif game_state == STATE_YOU_WIN:
screen.fill(BG_COLOR)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_you_win()
pygame.display.flip()
pygame.quit()
sys.exit()
Telechargement
Telecharger le code source de ce chapitre : brick-breaker-ch11.zip
