Ce que vous allez apprendre
Ce chapitre ajoute une dimension essentielle a votre jeu : le son. Vous allez decouvrir comment generer des effets sonores de facon programmatique (sans fichier audio), comment les jouer lors des evenements du jeu, et comment offrir au joueur un controle du volume et un bouton muet.
Le concept
Pygame possede un module pygame.mixer dedie au son. Il gere deux types d'audio :
- Sound (
pygame.mixer.Sound) : courts effets sonores charges en memoire. Ils peuvent etre joues simultanement sur plusieurs canaux. - Music (
pygame.mixer.music) : musique de fond lue en streaming depuis un fichier. Un seul morceau a la fois.
Pour eviter de dependre de fichiers audio externes, nous allons generer nos sons en creant directement des ondes sinusoidales. Le module array de Python permet de creer un buffer d'echantillons audio que Pygame peut lire directement.
Chaque son est defini par sa frequence (grave = basse frequence, aigu = haute frequence) et sa duree.
Etape par etape
1. Initialiser le mixer
On initialise le mixer avec des parametres specifiques. La frequence de 44100 Hz est le standard CD. On utilise un seul canal (mono) pour simplifier la generation de sons.
import array
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
size=-16 signifie des echantillons signes de 16 bits. buffer=512 reduit la latence (temps entre l'evenement et le son joue).
2. Generer un son par programmation
Notre fonction generate_beep cree une onde sinusoidale. On applique un fondu en sortie (fade-out) pour eviter les clics audibles a la fin du son.
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
"""Generate a simple sine wave beep sound."""
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))
# Apply a short fade-out to avoid clicks
fade_samples = min(500, n_samples // 4)
if i >= n_samples - fade_samples:
value = int(value * (n_samples - i) / fade_samples)
buf[i] = value
sound = pygame.mixer.Sound(buffer=buf)
return sound
32767 est la valeur maximale d'un entier signe de 16 bits. Le type "h" dans array.array represente un short signe (2 octets). Chaque echantillon est une valeur entre -32768 et 32767.
3. Creer les effets sonores
On cree un son different pour chaque evenement du jeu. Les sons aigus sont pour les actions positives, les sons graves pour les actions negatives.
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)
4. Jouer un son avec controle du volume
La fonction play_sound respecte le volume global et l'etat muet avant de jouer un son.
def play_sound(sound):
"""Play a sound respecting volume and mute settings."""
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
5. Controles du volume
On ajoute des touches pour controler le son : F1 pour couper/reactiver le son, +/- pour ajuster le volume.
if event.key == pygame.K_F1:
sound_muted = not sound_muted
if (event.key == pygame.K_PLUS
or event.key == pygame.K_KP_PLUS
or event.key == pygame.K_EQUALS):
master_volume = min(1.0, master_volume + 0.1)
if (event.key == pygame.K_MINUS
or event.key == pygame.K_KP_MINUS):
master_volume = max(0.0, master_volume - 0.1)
Points cles a retenir
Astuce : pour jouer une musique de fond depuis un fichier, utilisez pygame.mixer.music.load("musique.ogg") puis pygame.mixer.music.play(-1) (-1 pour boucler).
Piege courant : n'appelez pas mixer.init() avant pygame.init(), sinon les parametres par defaut du mixer pourraient ne pas correspondre a vos sons generes.
Formats supportes : Pygame supporte WAV, OGG et (selon la version) MP3. Le format OGG est recommande pour la compatibilite.
Fonctions Pygame utilisees :
pygame.mixer.init(): initialise le systeme audiopygame.mixer.Sound(buffer=...): cree un son depuis un buffersound.play(): joue le sonsound.set_volume(v): regle le volume (0.0 a 1.0)
Code complet
"""Brick Breaker - Chapter 9: Sound and Music"""
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
paddle_x = SCREEN_WIDTH // 2 - PADDLE_WIDTH // 2
paddle_y = SCREEN_HEIGHT - 40
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
lives = MAX_LIVES
score = 0
level = 1
STATE_PLAYING = 0
STATE_GAME_OVER = 1
STATE_YOU_WIN = 2
game_state = STATE_PLAYING
master_volume = 0.5
sound_muted = False
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
"""Generate a simple sine wave beep sound."""
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))
# Apply a short fade-out to avoid clicks
fade_samples = min(500, n_samples // 4)
if i >= n_samples - fade_samples:
value = int(value * (n_samples - i) / fade_samples)
buf[i] = value
sound = pygame.mixer.Sound(buffer=buf)
return sound
# Generate sound effects
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)
def play_sound(sound):
"""Play a sound respecting volume and mute settings."""
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
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)
def create_paddle_image(width, height):
surf = pygame.Surface((width, height), pygame.SRCALPHA)
for y in range(height):
brightness = 200 + int(55 * (1 - y / height))
pygame.draw.line(surf, (brightness, brightness, brightness),
(0, y), (width, y))
pygame.draw.rect(surf, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(surf, (0, 0, 0, 0), (width - 3, 0, 3, 3))
return surf.convert_alpha()
def create_ball_image(radius):
size = radius * 4
surf = pygame.Surface((size, size), pygame.SRCALPHA)
for r in range(radius * 2, radius, -1):
alpha = int(100 * (1 - (r - radius) / radius))
pygame.draw.circle(surf, (100, 100, 255, alpha),
(size // 2, size // 2), r)
pygame.draw.circle(surf, (255, 255, 255),
(size // 2, size // 2), radius)
pygame.draw.circle(surf, (255, 255, 255, 200),
(size // 2 - 2, size // 2 - 2), radius // 3)
return surf.convert_alpha()
def create_brick_image(width, height, color):
surf = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(surf, color, (0, 0, width, height))
highlight = tuple(min(255, c + 50) for c in color)
pygame.draw.line(surf, highlight, (1, 1), (width - 2, 1))
pygame.draw.line(surf, highlight, (1, 1), (1, height - 2))
shadow = tuple(max(0, c - 50) for c in color)
pygame.draw.line(surf, shadow, (1, height - 1), (width - 1, height - 1))
pygame.draw.line(surf, shadow, (width - 1, 1), (width - 1, height - 1))
return surf.convert_alpha()
def create_bricks():
bricks = []
for row in range(BRICK_ROWS):
color = rainbow_color(row, BRICK_ROWS)
image = create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color)
for col in range(BRICK_COLS):
x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
brick = {
"rect": pygame.Rect(x, y, BRICK_WIDTH, BRICK_HEIGHT),
"color": color,
"image": image,
"row": row,
}
bricks.append(brick)
return bricks
def reset_ball():
global ball_x, ball_y, ball_vx, ball_vy, ball_speed
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
def score_for_row(row):
return (BRICK_ROWS - row) * 10
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 9")
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_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)
bricks = create_bricks()
ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
use_mouse = False
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
if event.key == pygame.K_m:
if game_state == STATE_PLAYING:
use_mouse = not use_mouse
else:
sound_muted = not sound_muted
if event.key == pygame.K_F1:
sound_muted = not sound_muted
if (event.key == pygame.K_PLUS
or event.key == pygame.K_KP_PLUS
or event.key == pygame.K_EQUALS):
master_volume = min(1.0, master_volume + 0.1)
if (event.key == pygame.K_MINUS
or event.key == pygame.K_KP_MINUS):
master_volume = max(0.0, master_volume - 0.1)
if event.key == pygame.K_r and game_state != STATE_PLAYING:
lives = MAX_LIVES
score = 0
level = 1
bricks = create_bricks()
reset_ball()
game_state = STATE_PLAYING
if game_state == STATE_PLAYING:
if use_mouse:
mx, _ = pygame.mouse.get_pos()
paddle_x = mx - PADDLE_WIDTH // 2
else:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
paddle_x -= PADDLE_SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
paddle_x += PADDLE_SPEED
paddle_x = max(0, min(paddle_x, SCREEN_WIDTH - PADDLE_WIDTH))
ball_x += ball_vx * dt
ball_y += ball_vy * dt
if ball_x - BALL_RADIUS <= 0:
ball_x = BALL_RADIUS
ball_vx = abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
play_sound(snd_wall)
if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
ball_x = SCREEN_WIDTH - BALL_RADIUS
ball_vx = -abs(ball_vx)
ball_speed += BALL_SPEED_INCREMENT
play_sound(snd_wall)
if ball_y - BALL_RADIUS <= 0:
ball_y = BALL_RADIUS
ball_vy = abs(ball_vy)
ball_speed += BALL_SPEED_INCREMENT
play_sound(snd_wall)
if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
else:
play_sound(snd_lose_life)
reset_ball()
paddle_rect = pygame.Rect(paddle_x, paddle_y,
PADDLE_WIDTH, PADDLE_HEIGHT)
ball_rect = pygame.Rect(ball_x - BALL_RADIUS,
ball_y - BALL_RADIUS,
BALL_RADIUS * 2, BALL_RADIUS * 2)
if ball_rect.colliderect(paddle_rect) and ball_vy > 0:
ball_y = paddle_y - BALL_RADIUS
hit_pos = (ball_x - paddle_x) / PADDLE_WIDTH
angle = 150 - hit_pos * 120
rad = math.radians(angle)
ball_speed += BALL_SPEED_INCREMENT
ball_vx = ball_speed * math.cos(rad)
ball_vy = -abs(ball_speed * math.sin(rad))
play_sound(snd_paddle)
for brick in bricks[:]:
if ball_rect.colliderect(brick["rect"]):
score += score_for_row(brick["row"])
bricks.remove(brick)
br = brick["rect"]
if ball_x >= br.left and ball_x <= br.right:
ball_vy = -ball_vy
else:
ball_vx = -ball_vx
ball_speed += BALL_SPEED_INCREMENT
play_sound(snd_brick)
break
if len(bricks) == 0:
game_state = STATE_YOU_WIN
play_sound(snd_win)
screen.fill(BG_COLOR)
for brick in bricks:
screen.blit(brick["image"], brick["rect"])
screen.blit(paddle_img, (paddle_x, paddle_y))
ball_offset = BALL_RADIUS * 2
screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))
score_surf = hud_font.render(f"Score: {score}", True, (255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render(f"Lives: {lives}", True, (255, 100, 100))
screen.blit(lives_surf,
(SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render(f"Level: {level}", True, (100, 200, 255))
screen.blit(level_surf,
(SCREEN_WIDTH - level_surf.get_width() - 10, 10))
vol_label = "MUTED" if sound_muted else f"Vol: {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))
if game_state == STATE_GAME_OVER:
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(f"Final score: {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 R to restart",
True, (200, 200, 200))
screen.blit(restart,
(SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
elif game_state == STATE_YOU_WIN:
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(f"Final score: {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 R to restart",
True, (200, 200, 200))
screen.blit(restart,
(SCREEN_WIDTH // 2 - restart.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
info = small_font.render(
"[M] Mouse/Kbd [F1] Mute [+/-] Volume [R] Restart [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
pygame.display.flip()
pygame.quit()
sys.exit()
Telechargement
Telecharger le code source de ce chapitre : brick-breaker-ch09.zip
