Optimiser votre jeu de casse-brique en Python avec des particules et Numba

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

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *