Ce que vous allez apprendre

Ce chapitre introduit un systeme de particules. Les particules sont de petits elements graphiques avec une position, une vitesse, une duree de vie, une couleur et une taille. On les utilise pour trois effets : l'explosion de briques, les etincelles sur la raquette, et la poussiere au rebond sur les murs.

Le concept

Un systeme de particules est compose de deux parties :

  • La classe Particle : un petit objet avec position (x, y), vitesse (vx, vy), duree de vie, couleur et taille. A chaque frame, la particule se deplace, subit la gravite, et vieillit. Quand sa duree de vie atteint zero, elle meurt.
  • La classe ParticleSystem : un gestionnaire qui contient toutes les particules. Il les met a jour, supprime les mortes, et les dessine. Il offre aussi des methodes d'emission pour chaque type d'effet.

La particule se dessine comme un cercle dont l'opacite diminue avec le temps et dont la taille retrecit progressivement.

Etape par etape

1. La classe Particle

Chaque particule a sa propre physique simple : deplacement lineaire plus gravite.

python
class Particle:
    def __init__(self, x, y, vx, vy, lifetime, color, size):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.vy = vy
        self.lifetime = lifetime
        self.max_lifetime = lifetime
        self.color = color
        self.size = size
        self.alive = True
        self.gravity = 120.0

    def update(self, dt):
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.vy += self.gravity * dt
        self.lifetime -= dt
        if self.lifetime <= 0:
            self.alive = False

2. La classe ParticleSystem

Le gestionnaire gere la collection de particules et offre des methodes d'emission specifiques.

python
class ParticleSystem:
    def __init__(self):
        self.particles = []

    def update(self, dt):
        for p in self.particles:
            p.update(dt)
        self.particles = [
            p for p in self.particles if p.alive]

    def draw(self, surface):
        for p in self.particles:
            p.draw(surface)

3. Explosion de briques

Quand une brique est detruite, on emet 15 particules dans toutes les directions depuis son centre. Les couleurs varient legerement autour de la couleur de la brique.

python
def emit_brick_explosion(self, rect, color):
    cx = rect.centerx
    cy = rect.centery
    for _ in range(15):
        angle = random.uniform(0, 2 * math.pi)
        speed = random.uniform(60, 200)
        vx = speed * math.cos(angle)
        vy = speed * math.sin(angle) - 50
        lifetime = random.uniform(0.3, 0.8)
        size = random.randint(2, 5)
        self.particles.append(
            Particle(cx, cy, vx, vy, lifetime, color, size))

4. Etincelles sur la raquette

Au point d'impact de la balle sur la raquette, on emet des etincelles blanc-jaune vers le haut.

5. Poussiere au rebond mur

Quand la balle rebondit sur un mur, de petites particules grises sont emises dans la direction opposee au mur.

Points cles a retenir

Astuce : limitez le nombre de particules en gardant de courtes durees de vie. 15 particules par brique avec 0.5s de vie donnent un bel effet sans ralentir le jeu.

Piege courant : oublier de supprimer les particules mortes. Sans le filtrage dans update(), la liste grandit indefiniment et le jeu ralentit.

Gravite : meme une faible gravite (120 pixels/s2) rend les particules beaucoup plus naturelles. Sans elle, les eclats semblent flotter dans le vide.

Code complet

python
"""Brick Breaker - Chapter 15: Particles"""
import pygame
import sys
import math
import array
import random

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

# Animation constants
TRAIL_LENGTH = 8
TRAIL_INTERVAL = 0.016
BRICK_SHATTER_DURATION = 0.3
PADDLE_GLOW_DURATION = 0.2


# ── 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)


# ── Particle System ─────────────────────────────────────────────────

class Particle:
    """A single particle with position, velocity, lifetime, color, size."""

    def __init__(self, x, y, vx, vy, lifetime, color, size):
        self.x = float(x)
        self.y = float(y)
        self.vx = vx
        self.vy = vy
        self.lifetime = lifetime
        self.max_lifetime = lifetime
        self.color = color
        self.size = size
        self.alive = True
        # Gravity: particles fall down slightly
        self.gravity = 120.0

    def update(self, dt):
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.vy += self.gravity * dt
        self.lifetime -= dt
        if self.lifetime <= 0:
            self.alive = False

    def draw(self, surface):
        if not self.alive:
            return
        progress = 1.0 - (self.lifetime / self.max_lifetime)
        alpha = int(255 * (1 - progress))
        current_size = max(1, int(self.size * (1 - progress * 0.5)))
        if alpha <= 0:
            return
        r, g, b = self.color
        surf = pygame.Surface((current_size * 2, current_size * 2), pygame.SRCALPHA)
        pygame.draw.circle(surf, (r, g, b, alpha),
                           (current_size, current_size), current_size)
        surface.blit(surf, (int(self.x) - current_size, int(self.y) - current_size))


class ParticleSystem:
    """Manages all particles in the game."""

    def __init__(self):
        self.particles = []

    def update(self, dt):
        for p in self.particles:
            p.update(dt)
        self.particles = [p for p in self.particles if p.alive]

    def draw(self, surface):
        for p in self.particles:
            p.draw(surface)

    def clear(self):
        self.particles.clear()

    def emit_brick_explosion(self, rect, color):
        """Emit colored particles bursting outward from a destroyed brick."""
        cx = rect.centerx
        cy = rect.centery
        for _ in range(15):
            angle = random.uniform(0, 2 * math.pi)
            speed = random.uniform(60, 200)
            vx = speed * math.cos(angle)
            vy = speed * math.sin(angle) - 50  # bias upward
            lifetime = random.uniform(0.3, 0.8)
            # Vary the brick color slightly
            r = min(255, max(0, color[0] + random.randint(-30, 30)))
            g = min(255, max(0, color[1] + random.randint(-30, 30)))
            b = min(255, max(0, color[2] + random.randint(-30, 30)))
            size = random.randint(2, 5)
            self.particles.append(
                Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))

    def emit_paddle_sparks(self, paddle_rect, ball_x):
        """Emit bright sparks at the point where the ball hits the paddle."""
        cx = ball_x
        cy = paddle_rect.top
        for _ in range(10):
            angle = random.uniform(-math.pi, 0)  # upward half
            speed = random.uniform(40, 150)
            vx = speed * math.cos(angle)
            vy = speed * math.sin(angle)
            lifetime = random.uniform(0.2, 0.5)
            # Bright white-yellow sparks
            r = random.randint(220, 255)
            g = random.randint(200, 255)
            b = random.randint(100, 200)
            size = random.randint(1, 3)
            self.particles.append(
                Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))

    def emit_wall_dust(self, x, y, direction):
        """Emit small dust particles when ball bounces off a wall.

        direction: 'left', 'right', or 'top'
        """
        for _ in range(5):
            if direction == "left":
                vx = random.uniform(20, 80)
                vy = random.uniform(-50, 50)
            elif direction == "right":
                vx = random.uniform(-80, -20)
                vy = random.uniform(-50, 50)
            else:  # top
                vx = random.uniform(-50, 50)
                vy = random.uniform(20, 80)
            lifetime = random.uniform(0.2, 0.4)
            gray = random.randint(150, 220)
            size = random.randint(1, 3)
            self.particles.append(
                Particle(x, y, vx, vy, lifetime, (gray, gray, gray), size))


# ── Animation classes ───────────────────────────────────────────────

class BrickShatter:
    """Brick shatter animation: flash white, then fade out."""

    def __init__(self, rect, color):
        self.rect = pygame.Rect(rect)
        self.color = color
        self.timer = 0.0
        self.duration = BRICK_SHATTER_DURATION
        self.alive = True

    def update(self, dt):
        self.timer += dt
        if self.timer >= self.duration:
            self.alive = False

    def draw(self, surface):
        progress = self.timer / self.duration
        if progress < 0.3:
            flash = int(255 * (1 - progress / 0.3))
            r = min(255, self.color[0] + flash)
            g = min(255, self.color[1] + flash)
            b = min(255, self.color[2] + flash)
            alpha = 255
        else:
            fade = (progress - 0.3) / 0.7
            r, g, b = self.color
            alpha = int(255 * (1 - fade))
        if alpha <= 0:
            return
        shrink = int(progress * 10)
        draw_rect = self.rect.inflate(-shrink * 2, -shrink * 2)
        if draw_rect.width <= 0 or draw_rect.height <= 0:
            return
        surf = pygame.Surface((draw_rect.width, draw_rect.height), pygame.SRCALPHA)
        surf.fill((r, g, b, alpha))
        surface.blit(surf, draw_rect.topleft)


class BallTrail:
    """Trail effect behind the ball."""

    def __init__(self):
        self.positions = []
        self.timer = 0.0

    def update(self, dt, ball_x, ball_y):
        self.timer += dt
        if self.timer >= TRAIL_INTERVAL:
            self.timer -= TRAIL_INTERVAL
            self.positions.append((ball_x, ball_y))
            if len(self.positions) > TRAIL_LENGTH:
                self.positions.pop(0)

    def draw(self, surface):
        count = len(self.positions)
        for i, (x, y) in enumerate(self.positions):
            ratio = (i + 1) / count if count > 0 else 0
            alpha = int(80 * ratio)
            radius = max(1, int(BALL_RADIUS * ratio * 0.8))
            trail_surf = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
            pygame.draw.circle(trail_surf, (100, 100, 255, alpha),
                               (radius, radius), radius)
            surface.blit(trail_surf, (int(x) - radius, int(y) - radius))

    def reset(self):
        self.positions.clear()
        self.timer = 0.0


class PaddleGlow:
    """Glow effect on the paddle when the ball bounces on it."""

    def __init__(self):
        self.timer = 0.0
        self.active = False
        self.intensity = 0.0

    def trigger(self):
        self.active = True
        self.timer = 0.0
        self.intensity = 1.0

    def update(self, dt):
        if self.active:
            self.timer += dt
            self.intensity = max(0.0, 1.0 - self.timer / PADDLE_GLOW_DURATION)
            if self.timer >= PADDLE_GLOW_DURATION:
                self.active = False

    def draw(self, surface, paddle_rect):
        if not self.active or self.intensity <= 0:
            return
        glow_alpha = int(150 * self.intensity)
        glow_expand = int(6 * self.intensity)
        glow_rect = paddle_rect.inflate(glow_expand * 2, glow_expand * 2)
        glow_surf = pygame.Surface(
            (glow_rect.width, glow_rect.height), pygame.SRCALPHA)
        glow_surf.fill((200, 200, 255, glow_alpha))
        surface.blit(glow_surf, glow_rect.topleft)


# ── 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
        wall_direction = None
        if self.float_x - BALL_RADIUS <= 0:
            self.float_x = BALL_RADIUS
            self.vx = abs(self.vx)
            self.speed += BALL_SPEED_INCREMENT
            bounced = True
            wall_direction = "left"
        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
            wall_direction = "right"
        if self.float_y - BALL_RADIUS <= 0:
            self.float_y = BALL_RADIUS
            self.vy = abs(self.vy)
            self.speed += BALL_SPEED_INCREMENT
            bounced = True
            wall_direction = "top"
        if bounced:
            self.rect.center = (int(self.float_x), int(self.float_y))
        return bounced, wall_direction

    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 15")
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)

# Animation and particle objects
ball_trail = BallTrail()
paddle_glow = PaddleGlow()
shatter_animations = []
particles = ParticleSystem()

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():
    global lives, score, level, bricks, game_state, shatter_animations
    lives = MAX_LIVES
    score = 0
    level = 1
    bricks = create_brick_group()
    ball.reset()
    ball_trail.reset()
    shatter_animations = []
    particles.clear()
    game_state = STATE_PLAYING


# ── Draw helpers ────────────────────────────────────────────────────

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, (SCREEN_WIDTH // 2 - title.get_width() // 2,
                        SCREEN_HEIGHT // 3 - 30))
    subtitle = medium_font.render("Particles Edition", True, (200, 200, 255))
    screen.blit(subtitle, (SCREEN_WIDTH // 2 - subtitle.get_width() // 2,
                           SCREEN_HEIGHT // 3 + 30))
    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))


def draw_hud():
    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))
    count_surf = small_font.render(
        "Particles: {}".format(len(particles.particles)),
        True, (100, 100, 100))
    screen.blit(count_surf, (10, 30))


def draw_overlay(title_text, title_color):
    overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
    overlay.fill((0, 0, 0, 150))
    screen.blit(overlay, (0, 0))
    t = big_font.render(title_text, True, title_color)
    screen.blit(t, (SCREEN_WIDTH // 2 - t.get_width() // 2, SCREEN_HEIGHT // 2 - 40))
    fs = medium_font.render("Final score: {}".format(score), True, (255, 255, 255))
    screen.blit(fs, (SCREEN_WIDTH // 2 - fs.get_width() // 2, SCREEN_HEIGHT // 2 + 20))
    r = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
    screen.blit(r, (SCREEN_WIDTH // 2 - r.get_width() // 2, SCREEN_HEIGHT // 2 + 60))


# ── 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
            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)

            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)

        # Update animations and particles
        ball_trail.update(dt, ball.float_x, ball.float_y)
        paddle_glow.update(dt)
        particles.update(dt)
        for anim in shatter_animations:
            anim.update(dt)
        shatter_animations = [a for a in shatter_animations if a.alive]

        # Wall bounces with dust particles
        bounced, wall_dir = ball.bounce_wall()
        if bounced:
            play_sound(snd_wall)
            if wall_dir == "left":
                particles.emit_wall_dust(
                    BALL_RADIUS, ball.float_y, "left")
            elif wall_dir == "right":
                particles.emit_wall_dust(
                    SCREEN_WIDTH - BALL_RADIUS, ball.float_y, "right")
            elif wall_dir == "top":
                particles.emit_wall_dust(
                    ball.float_x, BALL_RADIUS, "top")

        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()
                ball_trail.reset()

        if ball.bounce_off_paddle(paddle):
            play_sound(snd_paddle)
            paddle_glow.trigger()
            # Paddle sparks
            particles.emit_paddle_sparks(paddle.rect, ball.float_x)

        hit_bricks = pygame.sprite.spritecollide(ball, bricks, True)
        if hit_bricks:
            for brick in hit_bricks:
                score += brick.score_value()
                # Shatter animation
                shatter_animations.append(
                    BrickShatter(brick.rect, brick.color))
                # Particle explosion
                particles.emit_brick_explosion(brick.rect, brick.color)
            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)
        for anim in shatter_animations:
            anim.draw(screen)
        ball_trail.draw(screen)
        paddle_glow.draw(screen, paddle.rect)
        paddle_group.draw(screen)
        ball_group.draw(screen)
        particles.draw(screen)
        draw_hud()
        info = small_font.render(
            "[M] Mouse  [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)
        for anim in shatter_animations:
            anim.draw(screen)
        ball_trail.draw(screen)
        paddle_glow.draw(screen, paddle.rect)
        paddle_group.draw(screen)
        ball_group.draw(screen)
        particles.draw(screen)
        draw_hud()
        draw_overlay("PAUSED", (200, 200, 255))

    elif game_state == STATE_GAME_OVER:
        screen.fill(BG_COLOR)
        bricks.draw(screen)
        for anim in shatter_animations:
            anim.draw(screen)
        particles.draw(screen)
        paddle_group.draw(screen)
        ball_group.draw(screen)
        draw_hud()
        draw_overlay("GAME OVER", (255, 60, 60))

    elif game_state == STATE_YOU_WIN:
        screen.fill(BG_COLOR)
        for anim in shatter_animations:
            anim.draw(screen)
        particles.draw(screen)
        paddle_group.draw(screen)
        ball_group.draw(screen)
        draw_hud()
        draw_overlay("YOU WIN!", (100, 255, 100))

    pygame.display.flip()

pygame.quit()
sys.exit()

Telechargement

Telecharger le code source de ce chapitre : brick-breaker-ch15.zip