Ce que vous allez apprendre

Ce chapitre introduit la detection de collisions, le mecanisme qui transforme un simple programme graphique en un veritable jeu. Vous allez apprendre a detecter quand la balle touche la raquette, quand elle touche une brique, et comment reagir en consequence : changer l'angle de rebond ou detruire la brique.

Le concept

Pygame utilise des objets Rect (rectangles) pour representer les zones occupees par les elements du jeu. La methode Rect.colliderect(autre_rect) retourne True si les deux rectangles se chevauchent.

Meme si notre balle est un cercle, on la represente par un rectangle englobant (bounding box) pour simplifier la detection. C'est une approximation tres courante dans les jeux 2D, suffisamment precise pour un casse-briques.

Pour les briques, on les stocke dans une liste de dictionnaires. Chaque dictionnaire contient le Rect de la brique, sa couleur, son image et son numero de rangee. Quand la balle touche une brique, on la retire simplement de la liste.

L'angle de rebond sur la raquette

Dans un vrai casse-briques, l'angle de rebond de la balle sur la raquette depend de l'endroit ou elle la touche :

  • Bord gauche de la raquette : la balle repart vers la gauche (angle large)
  • Centre de la raquette : la balle repart tout droit vers le haut
  • Bord droit de la raquette : la balle repart vers la droite

On calcule la position relative de l'impact (0.0 a 1.0) et on la transforme en un angle entre 150 et 30 degres.

Etape par etape

1. Stocker les briques comme des dictionnaires

On cree une fonction qui genere la liste de toutes les briques. Chaque brique est un dictionnaire avec un Rect, une couleur, une image pre-calculee et le numero de rangee.

python
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

2. Collision balle-raquette

On cree un Rect pour la raquette et un pour la balle, puis on teste le chevauchement. On verifie aussi que la balle descend (ball_vy > 0) pour eviter les collisions multiples.

python
    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
        # Calculate bounce angle based on hit position
        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))

hit_pos vaut 0.0 si la balle touche le bord gauche de la raquette, et 1.0 si elle touche le bord droit. On interpole lineairement entre 150 degres (vers la gauche) et 30 degres (vers la droite). -abs() garantit que la balle remonte toujours apres un rebond sur la raquette.

3. Collision balle-briques

On parcourt une copie de la liste (bricks[:]) pour pouvoir supprimer des elements pendant l'iteration. On ne traite qu'une brique par frame pour eviter les comportements etranges.

python
    for brick in bricks[:]:
        if ball_rect.colliderect(brick["rect"]):
            bricks.remove(brick)
            br = brick["rect"]
            # Determine bounce direction
            if ball_x >= br.left and ball_x <= br.right:
                ball_vy = -ball_vy
            else:
                ball_vx = -ball_vx
            ball_speed += BALL_SPEED_INCREMENT
            break  # Only one brick per frame

Pour determiner la direction du rebond, on verifie si le centre de la balle est horizontalement aligne avec la brique (impact par le haut ou le bas, on inverse vy) ou non (impact lateral, on inverse vx).

Points cles a retenir

Astuce : bricks[:] cree une copie superficielle de la liste. C'est essentiel car on ne peut pas modifier une liste pendant qu'on l'itere directement.

Piege courant : oublier le test ball_vy > 0 pour la raquette peut provoquer un effet de "balle collee" ou elle rebondit plusieurs fois de suite entre la raquette et sa position precedente.

Fonctions Pygame utilisees :

  • pygame.Rect(x, y, w, h) : cree un rectangle
  • rect.colliderect(other) : detecte le chevauchement
  • rect.left, rect.right, rect.top, rect.bottom : bords du rectangle

Code complet

python
"""Brick Breaker - Chapter 7: Collision Detection"""
import pygame
import sys
import math

pygame.init()

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


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


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 7")
clock = pygame.time.Clock()
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:
                use_mouse = not use_mouse

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

    # Move ball
    ball_x += ball_vx * dt
    ball_y += ball_vy * dt

    # Bounce off left wall
    if ball_x - BALL_RADIUS <= 0:
        ball_x = BALL_RADIUS
        ball_vx = abs(ball_vx)
        ball_speed += BALL_SPEED_INCREMENT

    # Bounce off right wall
    if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
        ball_x = SCREEN_WIDTH - BALL_RADIUS
        ball_vx = -abs(ball_vx)
        ball_speed += BALL_SPEED_INCREMENT

    # Bounce off top wall
    if ball_y - BALL_RADIUS <= 0:
        ball_y = BALL_RADIUS
        ball_vy = abs(ball_vy)
        ball_speed += BALL_SPEED_INCREMENT

    # Ball falls off bottom: reset
    if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
        reset_ball()
        bricks = create_bricks()

    # Ball vs paddle collision
    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))

    # Ball vs bricks collision
    for brick in bricks[:]:
        if ball_rect.colliderect(brick["rect"]):
            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
            break

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

    bricks_text = font.render(f"Bricks: {len(bricks)}", True,
                              (150, 150, 150))
    screen.blit(bricks_text, (SCREEN_WIDTH - 150, 10))

    info = font.render("[M] Mouse/Keyboard  [Arrows] Move  [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-ch07.zip