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.
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.
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.
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 rectanglerect.colliderect(other): detecte le chevauchementrect.left,rect.right,rect.top,rect.bottom: bords du rectangle
Code complet
"""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
