Ce que vous allez apprendre

Ce chapitre fait passer votre jeu des formes geometriques simples a des surfaces texturees. Vous allez decouvrir le concept central de Pygame : la Surface. Vous apprendrez a creer des images programmatiquement, a gerer la transparence (canal alpha), et a utiliser blit() pour dessiner une Surface sur une autre.

Le concept

En Pygame, tout est une Surface. L'ecran est une Surface. Une image chargee depuis un fichier est une Surface. Un texte rendu est une Surface. Une Surface est simplement un tableau de pixels en memoire.

Les operations cles sont :

  • pygame.Surface((w, h)) : cree une nouvelle surface vide
  • pygame.Surface((w, h), pygame.SRCALPHA) : cree une surface avec canal alpha (transparence par pixel)
  • surface.convert() : optimise le format de la surface pour l'affichage (plus rapide a dessiner)
  • surface.convert_alpha() : comme convert() mais preserve le canal alpha
  • destination.blit(source, position) : copie la surface source sur la destination a la position donnee

En production, vous chargeriez des images depuis des fichiers PNG avec pygame.image.load("sprite.png"). Dans ce tutoriel, nous creons nos sprites programmatiquement pour ne dependre d'aucun fichier externe. Le principe est le meme : vous obtenez une Surface que vous dessinez avec blit().

Etape par etape

1. Creer l'image de la raquette

Notre raquette passe d'un simple rectangle a un degrade vertical. On cree une Surface avec SRCALPHA (transparence par pixel), puis on dessine ligne par ligne avec une luminosite qui varie.

python
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))
    # Rounded corners effect
    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()

La luminosite va de 255 (blanc, en haut) a 200 (gris clair, en bas), ce qui cree un effet 3D subtil. Les coins sont effaces avec des rectangles transparents (0, 0, 0, 0) pour un effet arrondi. L'appel a convert_alpha() a la fin optimise la Surface pour le dessin avec transparence.

2. Creer l'image de la balle

La balle gagne un halo lumineux autour d'elle. On dessine des cercles concentriques de plus en plus transparents, puis le noyau blanc, et enfin un petit reflet.

python
def create_ball_image(radius):
    size = radius * 4
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    # Glow
    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)
    # Core
    pygame.draw.circle(surf, (255, 255, 255),
                       (size // 2, size // 2), radius)
    # Highlight
    pygame.draw.circle(surf, (255, 255, 255, 200),
                       (size // 2 - 2, size // 2 - 2), radius // 3)
    return surf.convert_alpha()

La Surface est plus grande que la balle elle-meme (radius * 4) pour laisser de la place au halo. Le halo est bleu clair et semi-transparent. Le reflet donne un effet de volume.

3. Creer les images des briques

Chaque brique gagne un effet 3D avec une ligne de surbrillance en haut et a gauche, et une ombre en bas et a droite.

python
def create_brick_image(width, height, color):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    # Main color
    pygame.draw.rect(surf, color, (0, 0, width, height))
    # Highlight top and left
    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 bottom and right
    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()

On pre-calcule les images pour chaque rangee au demarrage du jeu, plutot que de les recreer a chaque frame. C'est une optimisation importante : creer des surfaces est couteux, les copier avec blit() est rapide.

4. Pre-creer les images au demarrage

python
# Pre-create images
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)

# Pre-create brick images for each row
brick_images = []
for row in range(BRICK_ROWS):
    color = rainbow_color(row, BRICK_ROWS)
    brick_images.append(create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color))

5. Dessiner avec blit()

Au lieu de pygame.draw.rect() et pygame.draw.circle(), on utilise maintenant screen.blit(image, position). La position est le coin superieur gauche de l'image.

python
# Draw bricks using images
for row in range(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)
        screen.blit(brick_images[row], (x, y))

# Draw paddle using image
screen.blit(paddle_img, (paddle_x, paddle_y))

# Draw ball using image (offset because the surface is larger)
ball_offset = BALL_RADIUS * 2
screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))

Attention au decalage de la balle : comme sa Surface est plus grande que le cercle visible (a cause du halo), on doit soustraire un decalage pour que le centre visuel corresponde a la position logique.

Points cles a retenir

Astuce : pour charger une image depuis un fichier, utilisez img = pygame.image.load("sprite.png").convert_alpha(). N'oubliez jamais convert() ou convert_alpha() : sans cette etape, le dessin sera beaucoup plus lent car Pygame devra convertir le format a chaque blit().

Piege courant : la position passee a blit() est le coin superieur gauche de la surface, pas son centre. Pour centrer un sprite, soustrayez la moitie de sa largeur et de sa hauteur.

Piege courant : si votre image a un fond noir au lieu d'etre transparente, vous avez probablement oublie pygame.SRCALPHA a la creation de la Surface, ou vous utilisez convert() au lieu de convert_alpha().

Fonctions Pygame utilisees :

  • pygame.Surface((w, h), pygame.SRCALPHA) : surface transparente
  • surface.convert_alpha() : optimise avec transparence
  • screen.blit(surface, (x, y)) : copie une surface sur une autre
  • pygame.image.load(path) : charge une image depuis un fichier

Code complet

python
"""Brick Breaker - Chapter 5: Displaying Images"""
import pygame
import sys

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_x = SCREEN_WIDTH // 2
ball_y = SCREEN_HEIGHT // 2

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)


# Create paddle image (gradient surface)
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))
    # Rounded corners effect
    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()


# Create ball image (circle with glow)
def create_ball_image(radius):
    size = radius * 4
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    # Glow
    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)
    # Core
    pygame.draw.circle(surf, (255, 255, 255),
                       (size // 2, size // 2), radius)
    # Highlight
    pygame.draw.circle(surf, (255, 255, 255, 200),
                       (size // 2 - 2, size // 2 - 2), radius // 3)
    return surf.convert_alpha()


# Create brick image
def create_brick_image(width, height, color):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    # Main color
    pygame.draw.rect(surf, color, (0, 0, width, height))
    # Highlight top
    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 bottom
    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()


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 5")
clock = pygame.time.Clock()
font = pygame.font.SysFont("monospace", 14)

# Pre-create images
paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)

# Pre-create brick images for each row
brick_images = []
for row in range(BRICK_ROWS):
    color = rainbow_color(row, BRICK_ROWS)
    brick_images.append(create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color))

use_mouse = False

running = True
while running:
    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))

    screen.fill(BG_COLOR)

    # Draw bricks using images
    for row in range(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)
            screen.blit(brick_images[row], (x, y))

    # Draw paddle using image
    screen.blit(paddle_img, (paddle_x, paddle_y))

    # Draw ball using image
    ball_offset = BALL_RADIUS * 2
    screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))

    info = font.render("[M] Mouse/Keyboard  [Arrows] Move  [ESC] Quit",
                       True, (150, 150, 150))
    screen.blit(info, (10, SCREEN_HEIGHT - 25))

    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()
sys.exit()

Telechargement

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