Introduction
Les jeux en Python sont souvent réalisés avec des bibliothèques comme Pygame, qui permet de gérer facilement les graphismes et les entrées utilisateur. Cependant, lorsqu'il s'agit de performances pour des effets visuels complexes comme des particules, il est nécessaire d'optimiser certaines parties du code pour garantir la fluidité du jeu. C'est ici que Numba, un compilateur JIT (Just-In-Time) pour Python, entre en jeu pour accélérer les calculs lourds.
Dans cet article, nous allons détailler comment créer un jeu de casse-brique basique en Python avec Pygame, et y ajouter un système de particules optimisé grâce à Numba. Les particules se déclencheront lorsque la balle touchera une brique, créant un effet d'explosion réaliste.
1. Initialisation du projet
Nous commençons par initialiser les éléments de base du jeu : la raquette, la balle, et les briques. Cela comprend la configuration des dimensions de l’écran et des couleurs, ainsi que la gestion de la raquette par l’utilisateur et les rebonds de la balle.
1.1. Initialiser Pygame et les classes de base
L’initialisation du jeu se fait avec Pygame. Le jeu dispose d’une raquette contrôlable avec les touches gauche et droite, une balle qui rebondit sur les murs et la raquette, et plusieurs briques placées en haut de l'écran.
Code pour l'initialisation de Pygame et des classes :
import pygame
import sys
# Dimensions de la fenêtre
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Initialiser Pygame
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Casse-Brique")
2. Ajout des particules avec Numba
Pour améliorer l’aspect visuel du jeu, nous allons ajouter des particules qui apparaissent lorsque la balle casse une brique. Ces particules simulent un effet d'explosion, tombant sous l'effet de la gravité. Nous utilisons Numba pour accélérer la mise à jour des particules, car cela implique des calculs répétitifs (positions, vitesses, gravité).
2.1. Création du système de particules
Nous définissons des tableaux pour stocker les positions, vitesses, et durées de vie des particules. La fonction update_particles
utilise Numba pour mettre à jour chaque particule à chaque frame.
2.2. Optimisation avec Numba
Numba permet d'accélérer les boucles en compilant le code Python en code machine au moment de l'exécution. Cela permet de traiter des centaines de particules sans ralentir le jeu.
Code pour les particules avec Numba :
import numpy as np
from numba import jit
# Nombre maximum de particules
MAX_PARTICLES = 500
# Positions et vitesses des particules
particles_pos = np.zeros((MAX_PARTICLES, 2))
particles_vel = np.zeros((MAX_PARTICLES, 2))
particles_life = np.zeros(MAX_PARTICLES)
GRAVITE = 9.8
@jit(nopython=True)
def update_particles(positions, velocities, lifetimes, delta_time):
for i in range(len(lifetimes)):
if lifetimes[i] > 0:
velocities[i][1] += GRAVITE * delta_time # Appliquer la gravité à la vitesse en y
velocities[i][0] += np.random.uniform(-0.05, 0.05) # Dispersion horizontale
positions[i] += velocities[i] * delta_time # Mettre à jour la position
lifetimes[i] -= delta_time # Réduire la durée de vie
Cette fonction est appelée à chaque frame pour mettre à jour la position et la durée de vie de chaque particule. La gravité est appliquée verticalement pour que les particules tombent naturellement.
3. Générer des particules lorsque la balle touche une brique
Lorsqu'une brique est touchée par la balle, un ensemble de 100 particules est généré à l'emplacement de la collision. Ces particules se déplacent dans des directions aléatoires avec une vitesse initiale élevée pour simuler une explosion.
3.1. Création des particules lors de la collision
Nous créons des particules en utilisant la fonction create_particles
, qui génère des vitesses initiales aléatoires dans un rayon autour du point de collision.
Code pour la création des particules :
def create_particles(x, y):
for i in range(100): # Génère 100 particules par brique
for j in range(MAX_PARTICLES):
if particles_life[j] <= 0: # Trouver une particule inactive
particles_pos[j] = [x, y]
particles_vel[j] = np.random.uniform(-200, 200, 2) # Vitesse aléatoire initiale
particles_life[j] = 2.0 # Durée de vie en secondes
break
4. Boucle principale et affichage des particules
La boucle principale du jeu gère les déplacements de la raquette, de la balle, et les collisions avec les briques. À chaque frame, nous appelons également la fonction update_particles
pour mettre à jour les particules, puis les dessiner à l'écran.
4.1. Mise à jour et affichage des particules
À chaque frame, les particules actives sont mises à jour (position, vitesse, gravité), puis dessinées si leur durée de vie n'est pas expirée.
Code pour la boucle principale et l'affichage des particules :
# Boucle principale
running = True
while running:
clock.tick(60) # Limite à 60 FPS
delta_time = clock.get_time() / 1000.0 # Temps écoulé par frame
keys = pygame.key.get_pressed()
# Gestion des événements
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Déplacement de la raquette et de la balle
raquette.move(keys)
balle.move()
# Vérification des collisions
balle.check_collision(raquette)
balle.check_collision_briques(briques)
# Mettre à jour les particules
update_particles(particles_pos, particles_vel, particles_life, delta_time)
# Effacer l'écran et dessiner les éléments
screen.fill(WHITE)
raquette.draw(screen)
balle.draw(screen)
# Dessiner les briques et les particules
for brique in briques:
brique.draw(screen)
for i in range(MAX_PARTICLES):
if particles_life[i] > 0:
pygame.draw.circle(screen, RED, (int(particles_pos[i][0]), int(particles_pos[i][1])), 3)
pygame.display.flip()
pygame.quit()
Conclusion
Grâce à l'utilisation de Numba pour optimiser le système de particules, nous avons pu intégrer des effets visuels complexes dans un jeu de casse-brique sans compromettre les performances. Ce projet montre l'intérêt de combiner Pygame pour la gestion des éléments graphiques avec Numba pour améliorer les calculs intensifs en Python.
Le programme complet :
import pygame
import sys
import numpy as np
from numba import jit
# Nombre maximum de particules
MAX_PARTICLES = 500
# Positions et vitesses des particules
particles_pos = np.zeros((MAX_PARTICLES, 2))
particles_vel = np.zeros((MAX_PARTICLES, 2))
particles_life = np.zeros(MAX_PARTICLES)
GRAVITE = 9.8
# Fonction optimisée avec Numba pour mettre à jour les particules
@jit(nopython=True)
def update_particles(positions, velocities, lifetimes, delta_time):
for i in range(len(lifetimes)):
if lifetimes[i] > 0:
velocities[i][1] += GRAVITE * delta_time # Appliquer la gravité à la vitesse en y
velocities[i][0] += np.random.uniform(-0.05, 0.05) # Dispersion horizontale
positions[i] += velocities[i] * delta_time # Mettre à jour la position
lifetimes[i] -= delta_time # Réduire la durée de vie
def create_particles(x, y):
for i in range(100): # Génère 100 particules par brique
for j in range(MAX_PARTICLES):
if particles_life[j] <= 0: # Trouver une particule inactive
particles_pos[j] = [x, y]
particles_vel[j] = np.random.uniform(-200, 200, 2) # Vitesse aléatoire initiale
particles_life[j] = 2.0 # Durée de vie en secondes
break
# Classe Raquette
class Raquette:
def __init__(self):
self.width = 100
self.height = 20
self.x = (SCREEN_WIDTH - self.width) // 2
self.y = SCREEN_HEIGHT - 50
self.speed = 10
self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
def move(self, keys):
if keys[pygame.K_LEFT] and self.rect.left > 0:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] and self.rect.right < SCREEN_WIDTH:
self.rect.x += self.speed
def draw(self, screen):
pygame.draw.rect(screen, (0, 0, 255), self.rect)
# Classe Balle
class Balle:
def __init__(self):
self.radius = 10
self.x = SCREEN_WIDTH // 2
self.y = SCREEN_HEIGHT // 2
self.speed_x = 5
self.speed_y = -5
self.rect = pygame.Rect(self.x - self.radius, self.y - self.radius, self.radius * 2, self.radius * 2)
def check_collision_briques(self, briques):
for brique in briques:
if self.rect.colliderect(brique.rect):
briques.remove(brique) # Supprimer la brique touchée
self.speed_y = -self.speed_y # Inverser la direction de la balle après collision
create_particles(brique.rect.x + brique.width // 2, brique.rect.y + brique.height // 2) # Générer des particules
break
def move(self):
self.rect.x += self.speed_x
self.rect.y += self.speed_y
# Rebonds sur les murs
if self.rect.left <= 0 or self.rect.right >= SCREEN_WIDTH:
self.speed_x = -self.speed_x
if self.rect.top <= 0:
self.speed_y = -self.speed_y
def draw(self, screen):
pygame.draw.circle(screen, (255, 0, 0), (self.rect.x + self.radius, self.rect.y + self.radius), self.radius)
def check_collision(self, raquette):
if self.rect.colliderect(raquette.rect):
self.speed_y = -self.speed_y
# Classe Brique
class Brique:
def __init__(self, x, y):
self.width = 80
self.height = 30
self.rect = pygame.Rect(x, y, self.width, self.height)
def draw(self, screen):
pygame.draw.rect(screen, (0, 0, 0), self.rect)
# Créer plusieurs briques
def creer_briques():
briques = []
for row in range(5): # 5 lignes de briques
for col in range(10): # 10 colonnes
brique = Brique(col * 80 + 10, row * 30 + 10)
briques.append(brique)
return briques
# Initialiser Pygame
pygame.init()
# Dimensions de la fenêtre
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Casse-Brique")
# Couleurs
WHITE = (255, 255, 255)
RED = (255, 0, 0)
# Initialiser l'horloge pour contrôler le FPS
clock = pygame.time.Clock()
# Initialiser les objets du jeu
raquette = Raquette()
balle = Balle()
briques = creer_briques()
# Boucle principale
running = True
while running:
clock.tick(60) # Limite à 60 FPS
delta_time = clock.get_time() / 1000.0 # Temps écoulé par frame
keys = pygame.key.get_pressed()
# Gestion des événements
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Déplacement de la raquette
raquette.move(keys)
# Déplacement de la balle
balle.move()
# Vérification des collisions
balle.check_collision(raquette)
balle.check_collision_briques(briques)
# Mettre à jour les particules
update_particles(particles_pos, particles_vel, particles_life, delta_time)
# Effacer l'écran et dessiner les éléments
screen.fill(WHITE)
raquette.draw(screen)
balle.draw(screen)
# Dessiner les briques
for brique in briques:
brique.draw(screen)
# Dessiner les particules
for i in range(MAX_PARTICLES):
if particles_life[i] > 0:
pygame.draw.circle(screen, RED, (int(particles_pos[i][0]), int(particles_pos[i][1])), 3)
pygame.display.flip()
pygame.quit()