Ce que vous allez apprendre

Ce chapitre ajoute du texte a votre jeu : score, vies, niveau, et messages de fin de partie. Vous allez decouvrir le systeme de polices de Pygame, comment afficher du texte a l'ecran, et comment creer une interface utilisateur (HUD) complete.

Le concept

En Pygame, le texte n'est pas dessine directement : il est d'abord converti en une Surface (une image), puis colle (blit) sur l'ecran. Le processus en deux etapes est le suivant :

  1. Creer une police : pygame.font.SysFont(nom, taille) charge une police installee sur le systeme.
  2. Rendre le texte : font.render(texte, antialias, couleur) cree une Surface contenant le texte.

Le parametre antialias (True/False) active le lissage des bords. Utilisez toujours True pour un rendu agreable.

Pour centrer un texte, on utilise surface.get_width() pour connaitre sa largeur en pixels, puis on calcule la position :

x = SCREEN_WIDTH // 2 - surface.get_width() // 2

Etats du jeu

Ce chapitre introduit aussi la notion d'etats du jeu. Notre jeu a maintenant trois etats possibles :

  • STATE_PLAYING : le jeu est en cours
  • STATE_GAME_OVER : le joueur a perdu toutes ses vies
  • STATE_YOU_WIN : toutes les briques sont detruites

Seul l'etat PLAYING met a jour la logique du jeu. Les autres affichent un ecran de fin et attendent que le joueur appuie sur R pour recommencer.

Etape par etape

1. Creer les polices

On cree plusieurs polices de tailles differentes pour chaque usage : HUD, titres, messages.

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

SysFont utilise les polices installees sur votre systeme. "monospace" est disponible partout. Le parametre bold=True active le gras.

2. Afficher le score, les vies et le niveau

On dessine le HUD (Head-Up Display) en haut de l'ecran a chaque frame. Le score est a gauche, les vies au centre, le niveau a droite.

python
    # HUD
    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))

3. Systeme de score par rangee

Les briques du haut valent plus de points que celles du bas. C'est plus juste, car elles sont plus difficiles a atteindre.

python
def score_for_row(row):
    """Higher rows (closer to top) give more points."""
    return (BRICK_ROWS - row) * 10

La rangee 0 (tout en haut) vaut 60 points, la rangee 5 (en bas) vaut 10 points.

4. Systeme de vies

Le joueur commence avec 3 vies. Quand la balle tombe, il perd une vie. A zero vies, c'est le Game Over.

python
    # Ball falls off bottom: lose a life
    if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
        lives -= 1
        if lives <= 0:
            game_state = STATE_GAME_OVER
        else:
            reset_ball()

5. Ecrans de fin avec superposition

Pour les ecrans GAME OVER et YOU WIN, on dessine d'abord une surface semi-transparente noire par-dessus tout le jeu, puis on affiche le texte au centre.

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

Points cles a retenir

Astuce : ne creez pas de police a chaque frame ! SysFont() est lent. Creez vos polices une seule fois avant la boucle et reutilisez-les.

Piege courant : font.render() ne gere pas les retours a la ligne. Pour du texte multi-lignes, vous devez rendre chaque ligne separement et les positionner manuellement.

Fonctions Pygame utilisees :

  • pygame.font.SysFont(name, size) : charge une police systeme
  • font.render(text, antialias, color) : cree une Surface de texte
  • surface.get_width() : largeur en pixels (utile pour centrer)
  • surface.get_height() : hauteur en pixels

Code complet

python
"""Brick Breaker - Chapter 8: Text and Fonts"""
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

MAX_LIVES = 3
lives = MAX_LIVES
score = 0
level = 1

# Game states
STATE_PLAYING = 0
STATE_GAME_OVER = 1
STATE_YOU_WIN = 2
game_state = STATE_PLAYING


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


def score_for_row(row):
    """Higher rows (closer to top) give more points."""
    return (BRICK_ROWS - row) * 10


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 8")
clock = pygame.time.Clock()

# Fonts
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_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 event.key == pygame.K_r and game_state != STATE_PLAYING:
                lives = MAX_LIVES
                score = 0
                level = 1
                bricks = create_bricks()
                reset_ball()
                game_state = STATE_PLAYING

    if game_state == STATE_PLAYING:
        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: lose a life
        if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
            lives -= 1
            if lives <= 0:
                game_state = STATE_GAME_OVER
            else:
                reset_ball()

        # 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"]):
                score += score_for_row(brick["row"])
                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

        # Check if all bricks destroyed
        if len(bricks) == 0:
            game_state = STATE_YOU_WIN

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

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

    # Game state overlays
    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/Keyboard  [Arrows] Move  [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-ch08.zip