Ce que vous allez apprendre
Ce chapitre est une etape majeure : on refactorise tout le code en utilisant le systeme de Sprites de Pygame. Au lieu de gerer des variables globales et des dictionnaires, chaque element du jeu devient un objet avec ses propres donnees et comportements. C'est la base de la programmation orientee objet appliquee au jeu video.
Le concept
Un Sprite dans Pygame est un objet qui possede deux attributs essentiels :
self.image: la Surface (image) a dessinerself.rect: le rectangle qui definit la position et la taille (pour le dessin et les collisions)
Pour creer un Sprite, on herite de pygame.sprite.Sprite et on definit self.image et self.rect dans le constructeur __init__.
Les Groups (groupes) sont des conteneurs de sprites. Ils offrent des avantages puissants :
group.draw(screen): dessine tous les sprites du groupe en un seul appelgroup.update(): appelle la methodeupdate()de chaque spritepygame.sprite.spritecollide(sprite, group, dokill): detecte les collisions entre un sprite et un groupe entier
GroupSingle est un groupe special qui ne contient qu'un seul sprite. Il est parfait pour la raquette et la balle.
Etape par etape
1. La classe Paddle
On encapsule toute la logique de la raquette dans une classe. Le constructeur cree l'image et positionne le rectangle. La methode update() gere le deplacement.
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))
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)
super().__init__() appelle le constructeur de pygame.sprite.Sprite. C'est obligatoire. Sans cet appel, les groupes ne fonctionneront pas correctement.
2. La classe Ball
La balle encapsule son mouvement, ses rebonds et sa remise a zero. On garde des coordonnees float separees pour la precision.
class Ball(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# ... (creation de l'image)
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
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):
"""Handle wall bounces. Returns True if bounced."""
# ... (logique de rebond)
def bounce_off_paddle(self, paddle):
"""Bounce off paddle. Returns True if bounced."""
# ... (logique de rebond)
3. La classe Brick
Chaque brique est un Sprite independant avec sa propre image et sa propre position. La methode score_value() retourne les points gagnes quand cette brique est detruite.
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 and shadow)
self.rect = self.image.get_rect(topleft=(x, y))
self.row = row
def score_value(self):
return (BRICK_ROWS - self.row) * 10
4. Creer les groupes
On utilise GroupSingle pour la raquette et la balle (un seul sprite chacun) et Group pour les briques.
paddle = Paddle()
ball = Ball()
bricks = create_brick_group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
5. Dessiner et detecter les collisions
Le dessin se fait en une seule ligne par groupe. La detection de collisions utilise spritecollide qui retourne la liste des sprites touches. Le parametre True supprime automatiquement les sprites touches du groupe.
# Draw all sprites
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
# Collision detection
hit_bricks = pygame.sprite.spritecollide(
ball, bricks, True)
if hit_bricks:
for brick in hit_bricks:
score += brick.score_value()
ball.vy = -ball.vy
Points cles a retenir
Astuce : le troisieme parametre de spritecollide (dokill) est tres pratique. Quand il vaut True, les sprites en collision sont automatiquement retires de tous leurs groupes. Plus besoin de gerer la suppression manuellement.
Piege courant : oublier super().__init__() dans le constructeur du Sprite causera une erreur quand vous essayerez d'ajouter le sprite a un groupe.
Avantage des Sprites : le code est mieux organise, plus facile a lire et a maintenir. Chaque objet du jeu est autonome : il sait se dessiner, se deplacer et reagir.
Fonctions Pygame utilisees :
pygame.sprite.Sprite: classe de base pour les spritespygame.sprite.Group: conteneur de spritespygame.sprite.GroupSingle: groupe a un seul spritegroup.draw(surface): dessine tous les spritesgroup.update(): met a jour tous les spritespygame.sprite.spritecollide(): collision sprite vs groupe
Code complet
"""Brick Breaker - Chapter 10: Sprites and Groups"""
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
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)
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)
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
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 10")
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
STATE_PLAYING = 0
STATE_GAME_OVER = 1
STATE_YOU_WIN = 2
game_state = STATE_PLAYING
master_volume = 0.5
sound_muted = False
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
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:
paddle.use_mouse = not paddle.use_mouse
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_brick_group()
ball.reset()
game_state = STATE_PLAYING
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)
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
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-ch10.zip
