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.
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.
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.
# 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.
# 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.
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 ecouleesmath.cos(),math.sin(): composantes de vitessemath.radians(): conversion degres vers radiansabs(): valeur absolue pour forcer une direction
Code complet
"""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
