Ce que vous allez apprendre

Ce chapitre est le moment ou votre jeu prend vie : la balle va enfin bouger ! Vous allez decouvrir comment animer un objet a l'ecran, comment le faire rebondir contre les murs, et pourquoi il est essentiel d'utiliser le delta time pour un mouvement fluide et independant de la vitesse du processeur.

Le concept

Pour deplacer un objet, on modifie sa position a chaque frame en lui ajoutant une velocite (vitesse avec une direction). La velocite est decomposee en deux composantes :

  • vx : vitesse horizontale (positive = vers la droite)
  • vy : vitesse verticale (positive = vers le bas, car l'axe Y pointe vers le bas en Pygame)

A chaque frame, on fait : ball_x += vx et ball_y += vy. Mais il y a un probleme : si le jeu tourne a 60 FPS sur un PC rapide et a 30 FPS sur un PC lent, la balle ira deux fois plus vite sur le PC rapide !

La solution est le delta time (dt) : le temps ecoule depuis la derniere frame, en secondes. On multiplie la velocite par dt : ball_x += vx * dt. Ainsi, la vitesse est exprimee en pixels par seconde et reste constante quelle que soit la frequence d'images.

clock.tick(FPS) retourne le nombre de millisecondes ecoulees depuis le dernier appel. On divise par 1000 pour obtenir des secondes : dt = clock.tick(FPS) / 1000.0.

Les rebonds

Pour faire rebondir la balle contre un mur, il suffit d'inverser la composante de velocite correspondante :

  • Mur gauche ou droit : on inverse vx
  • Mur du haut : on inverse vy
  • En bas : la balle est perdue (on la remet au centre)

On utilise abs() pour garantir la direction correcte apres le rebond : ball_vx = abs(ball_vx) pour aller vers la droite apres un rebond sur le mur gauche.

Etape par etape

1. Velocite et delta time

On definit la vitesse de base en pixels par seconde et on calcule les composantes initiales avec la trigonometrie. L'angle de -60 degres lance la balle vers le haut-droite.

python
import math

BALL_BASE_SPEED = 300  # pixels per second
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
BALL_SPEED_INCREMENT = 10  # speed increase per bounce

math.radians() convertit des degres en radians, car math.cos() et math.sin() attendent des radians. Un angle de -60 degres donne une trajectoire ascendante vers la droite.

2. Obtenir le delta time

Au debut de chaque iteration de la boucle, on recupere le temps ecoule depuis la derniere frame.

python
while running:
    dt = clock.tick(FPS) / 1000.0  # seconds since last frame
    # ...

3. Deplacer la balle

On multiplie la velocite par dt pour obtenir un mouvement independant du framerate.

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

4. Rebonds contre les murs

On verifie chaque bord de l'ecran. Quand la balle touche un mur, on la repositionne juste a la limite et on inverse la composante de velocite appropriee. On augmente aussi legerement la vitesse a chaque rebond.

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

5. Fonction de remise a zero

Quand la balle tombe en bas de l'ecran, on la remet au centre avec sa vitesse initiale.

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

Points cles a retenir

Astuce : utilisez des float pour les positions de la balle. Les entiers causeraient des erreurs d'arrondi qui rendraient le mouvement saccade, surtout a faible vitesse.

Piege courant : si vous oubliez de repositionner la balle apres un rebond (ball_x = BALL_RADIUS), elle pourrait rester coincee dans le mur et rebondir indefiniment.

Fonctions utilisees :

  • clock.tick(FPS) : retourne les millisecondes ecoulees
  • math.cos(), math.sin() : composantes de vitesse
  • math.radians() : conversion degres vers radians
  • abs() : valeur absolue pour forcer une direction

Code complet

python
"""Brick Breaker - Chapter 6: Moving Things"""
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_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
BALL_BASE_SPEED = 300  # pixels per second
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))
BALL_SPEED_INCREMENT = 10  # speed increase per bounce

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

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

    # Draw
    screen.fill(BG_COLOR)

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

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

    speed_text = font.render(f"Speed: {int(ball_speed)}", True,
                             (150, 150, 150))
    screen.blit(speed_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-ch06.zip