Jeu vidéo en Python à l’aide de la librairie Pygame

Projets

Dans le cadre du projet libre de fin d’année en NSI, je vous présente mon premier jeux vidéo en python fait à l’aide de pygame, une librairie spécialisé pour la création de jeux vidéo.

Librairie Pygame

Pygame est une bibliothèque de jeu open-source pour Python, qui permet de créer des jeux et des applications multimédias. Elle fournit des outils pour gérer les graphismes, le son, les entrées utilisateur, le réseau et plus encore.

Pygame utilise la bibliothèque SDL (Simple DirectMedia Layer) pour accéder aux fonctions du système d’exploitation, ce qui signifie que les jeux créés avec Pygame peuvent fonctionner sur de nombreuses plateformes, notamment Windows, macOS et Linux.

Pygame est une excellente option pour les débutants qui cherchent à créer des jeux simples (des base en programmation sont cependant nécessaire), mais il peut également être utilisé pour créer des jeux professionnels et des applications multimédias complexes.

Le Projet

Mon objectif sur ce projet a été de faire mes premiers pas avec la bibliothèque Pygame en Python. Cela a été un véritable défi qui m’aura fait découvrir de nombreuses mécaniques en Python, telles que le système de classe ou la programmation orientée objet, qui ont représenté pour moi un pas dans l’inconnu. Mon défi a donc été de créer un jeu simple pour débuter. Le but est de contrôler un personnage avec sa souris et d’éviter des briques qui apparaissent aléatoirement sur le côté de l’écran, tout cela en ayant le choix entre trois niveaux de difficulté différents.

Structure du Script

Pour la réalisation de mon jeu, j’ai décidé de me lancer un défi supplémentaire : l’utilisation de la Programmation Orientée Objet. Mais qu’est-ce que la Programmation Orientée Objet ?

La Programmation Orientée Objet est un paradigme de programmation qui permet de structurer un programme en utilisant des objets, qui sont des entités possédant des caractéristiques et des comportements spécifiques. Cette approche permet de modéliser des concepts du monde réel de manière plus naturelle et intuitive, en regroupant des fonctionnalités liées dans des classes, et en instanciant ces classes pour créer des objets.

En résumé, cela consiste à organiser son code de manière à ce qu’il soit ordonné à l’aide de classes contenant des fonctions et des constantes générales, ce qui conduit à un code plus lisible, plus maintenable et plus ordonné. Cependant, cela peut être relativement perturbant au début.

Voici un simple scripte sans utilisation de Programmation Orienté Objet qui permet de faire bougé une image nommé « ball.png » à l’aide des flèches directionnelles :

import pygame

pygame.init()
screen = pygame.display.set_mode((400, 400))
running = True

image = pygame.image.load("ball.png")

x = 0
y = 0

clock = pygame.time.Clock()

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    pressed = pygame.key.get_pressed()
    if pressed[pygame.K_LEFT]:
        x -= 1
    if pressed[pygame.K_RIGHT]:
        x += 1
    if pressed[pygame.K_UP]:
        y -= 1
    if pressed[pygame.K_DOWN]:
        y += 1

    screen.fill((0, 0, 0))
    screen.blit(image, (x, y))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

code by TNtube

Le scripte est simple et efficace, pour autant si il venais à se complexifier cela deviendrait vite illisible.
voici maintenant le même scripte avec l’utilisation de la Programmation Orienté Objet :

player.py

import pygame

class Player:
    def __init__(self, x, y):
        self.image = pygame.image.load("player.png")
        self.image
        self.rect = self.image.get_rect(x=x, y=y)
        self.speed = 5
        self.velocity = [0, 0]

    def move(self):
        self.rect.move_ip(self.velocity[0] * self.speed, self.velocity[1] * self.speed)

    def draw(self, screen):
        screen.blit(self.image, self.rect)

Main.py

import pygame
from player import Player


class Game:
    def __init__(self, screen):
        self.screen = screen
        self.running = True
        self.clock = pygame.time.Clock()
        self.player = Player(0, 0)

    def handling_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.player.velocity[0] = -1
        elif keys[pygame.K_RIGHT]:
            self.player.velocity[0] = 1
        else:
            self.player.velocity[0] = 0

        if keys[pygame.K_UP]:
            self.player.velocity[1] = -1
        elif keys[pygame.K_DOWN]:
            self.player.velocity[1] = 1
        else:
            self.player.velocity[1] = 0

    def update(self):
        pass

    def display(self):
        self.screen.fill("white")
        pygame.display.flip()

    def run(self):
        while self.running:
            self.handling_events()
            self.update()
            self.display()
            self.clock.tick(60)


pygame.init()
screen = pygame.display.set_mode((1080, 720))
game = Game(screen)
game.run()

pygame.quit()

code by TNtube

La particularité ici réside dans la clarté de chaque fonction et de leur utilité respective : « display » pour l’affichage, « handling_event » pour les interactions claviers-souris, « move » pour les mouvements du joueur… De plus, le programme est divisé en deux fichiers distincts : l’un pour le programme principal et l’autre pour le joueur. Ainsi, il est possible d’ajouter aisément de nouvelles fonctions pour des améliorations futures. Par exemple, la fonction « Update », actuellement inactive, pourrait être employée pour effectuer des vérifications, telles que les collisions.

Analyse du script

Passons maintenant à l’analyse du code.

Commençons par mon fichier « player.py », avec l’appel du module Pygame, la création de la classe et la fonction d’initialisation.

import pygame #On importe le module Pygame

class Player: #On crée la classe Player
    
# La fonction init est une fonction d'initialisation qui permet de déclarer toutes nos variables.
# Elle prend comme arguments self, qui est pris dans chaque fonction de la classe, x et y qui servent à définir la position initiale du joueur sur l'écran.
def init(self, x, y):
self.image = pygame.image.load("asset/Player.png")# On charge l'image du joueur
#Pour que vous puissiez être familiarisé avec le concept de rect, ce sont des rectangles que l'on ne voit pas, qui ont une taille que l'on leur définit et c'est grâce à ces rect, que l'on pourrait appeler hitbox, que l'on peut détecter des collisions.
self.rect = self.image.get_rect(center=(x, y))# On récupère le rectangle de l'image et on le centre en (x,y)
self.velocity = [0, 0]# On initialise la vitesse à 0 dans les deux directions

Une fois la mise en place de notre classe et de la fonction d’initialisation effectuée, nous créons la fonction de mouvement du joueur appelée « move », ainsi qu’une fonction « draw » qui permettra d’afficher notre personnage avec son rectangle.

def move(self, mouse_pos):# La fonction move permet de déplacer le joueur en fonction de la position de la souris, l'argument mouse_pos représente la position de la souris sur l'écran. Cette position est utilisée pour calculer la vitesse de déplacement du joueur, en soustrayant la position de la souris à la position actuelle du joueur.
    # On calcule la différence entre la position de la souris et la position actuelle du joueur
    self.velocity[0] = (mouse_pos[0] - self.rect.centerx)
    self.velocity[1] = (mouse_pos[1] - self.rect.centery)
    self.rect.move_ip(self.velocity)# On déplace le rectangle du joueur en fonction de la vitesse calculée

def draw(self, screen):# La fonction draw permet d'afficher le joueur sur l'écran
    screen.blit(self.image, self.rect)# On blitte (st utilisée pour dessiner une surface sur une autre surface.) l'image du joueur sur le rectangle de celui-ci

Maintenait que je vous ai introduis mon fichier joueur passons au fichier jeu.

import pygame
import random
from player import Player

On importe les modules nécessaires au fonctionnement du jeu : Pygame pour gérer l’affichage graphique et l’interaction avec l’utilisateur, random pour générer des nombres aléatoires, et Player qui est une classe définie dans un fichier séparé et qui représente le joueur.

class Game:
    def __init__(self, screen):
        self.screen = screen
        self.running = True
        self.clock = pygame.time.Clock()
        self.player = Player(60, 60)
        self.background = pygame.image.load("asset/background.jpg")
        self.brique = pygame.image.load("asset/brique.jpg")
        self.brique_rects = []
        self.brique_positions = []
        self.brique_speed = 3
        self.brique_spawn_rate = 120
        self.brique_spawn_counter = 0
        self.brique_spawn_augmentation = 15
        self.brique_spawn_increase_counter = 0
        self.total_time = 0
        self.total_time_max = 0
        self.difficulté = 20
        pygame.mouse.set_visible(False)
        
        choix = input("_____________________________________________________________________________ \n choisissez votre difficulté : \n 1- Facile \n 2- Normal \n 3- Difficile \n _____________________________________________________________________________ \n Votre choix : ")
        choix = int(choix)
        if choix == 1:
            self.difficulté = 1
        elif choix == 2:
            self.difficulté = 5
        elif choix == 3:
            self.difficulté = 10
        else:
            print("Error : s'il vous plait entré un entier positif entre 1 et 3")
            self.running = False

On définit la classe Game, qui représente le jeu en lui-même. Son constructeur prend en paramètre l’objet screen qui représente la surface sur laquelle le jeu sera affiché.

On initialise différentes variables, comme self.running qui permet de savoir si le jeu est en cours d’exécution ou non, self.player qui représente le joueur, self.background qui est l’image de fond, self.brique qui est l’image représentant les obstacles à éviter, et plusieurs variables liées à ces obstacles (self.brique_positions, self.brique_speed, self.brique_spawn_rate, self.brique_spawn_counter, self.brique_spawn_augmentation, self.brique_spawn_increase_counter).

On définit également self.total_time et self.total_time_max pour gérer le temps de jeu, et self.difficulté qui est utilisé pour régler la difficulté du jeu en fonction de la réponse de l’utilisateur à une question posée par input.

Enfin, on masque le curseur de la souris avec pygame.mouse.set_visible(False).

    def gestion_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

        self.player.move(pygame.mouse.get_pos())

La méthode gestion_events permet de gérer les événements liés à l’interaction de l’utilisateur avec la fenêtre Pygame.

Le code utilise une boucle for pour récupérer chaque événement dans la liste des événements de Pygame. Si l’un des événements est de type QUIT, cela signifie que l’utilisateur a cliqué sur le bouton de fermeture de la fenêtre. Dans ce cas, la variable self.running est définie sur False, ce qui interrompt la boucle de jeu.

La méthode appelle également la méthode move de l’objet Player pour mettre à jour sa position en fonction de la position de la souris de l’utilisateur.

   def adaptative_brique_rects(self):
        self.brique_rects = []

        for pos in self.brique_positions:
            rect = pygame.Rect(pos[0], pos[1], self.brique.get_width(), self.brique.get_height())
            self.brique_rects.append(rect)

La méthode adaptative_brique_rects est appelée pour mettre à jour les rects de collision des briques en fonction de leurs nouvelles positions.

 def spawn_brique(self):
        y = random.randint(0, self.screen.get_height() - self.brique.get_height())
        self.brique_positions.append((-self.brique.get_width(), y))

La méthode spawn_brique est appelée pour générer une nouvelle brique à une position aléatoire sur l’axe y et avec une position initiale en dehors de la fenêtre sur l’axe x.

    def move_briques(self):
        for i in range(len(self.brique_positions)):
            pos = list(self.brique_positions[i])
            pos[0] += self.brique_speed
            self.brique_positions[i] = tuple(pos)

        self.adaptative_brique_rects()

La méthode move_briques met à jour la position des briques en ajoutant la valeur de self.brique_speed à la coordonnée x de chaque position dans self.brique_positions.

def update(self):
    # Incrémenter le compteur de temps de spawn des briques par la difficulté du jeu
    self.brique_spawn_counter += self.difficulté

    # Augmenter la vitesse des briques si le compteur de spawn des briques est un multiple de 120
    if self.brique_spawn_counter % 120 == 0:
        self.brique_speed += 0.2

    # Vérifier si le compteur d'augmentation de spawn des briques a atteint sa limite
    self.brique_spawn_increase_counter += 1
    if self.brique_spawn_increase_counter >= self.brique_spawn_augmentation * 60:
        self.brique_spawn_increase_counter = 0
        self.brique_spawn_rate -= 10

    # Ajouter une nouvelle brique à la liste des positions de briques si le compteur de spawn des briques a atteint le taux de spawn des briques
    if self.brique_spawn_counter >= self.brique_spawn_rate:
        self.brique_spawn_counter = 0
        self.spawn_brique()

    # Déplacer toutes les briques dans la direction de la vitesse actuelle des briques
    self.move_briques()

    # Incrémenter le temps total de jeu de 1
    self.total_time += 1  

    # Vérifier si le joueur a perdu en entrant en collision avec une brique
    for rect in self.brique_rects:
        if rect.colliderect(self.player.rect):
            print("_____________________________________________________________________________\n")
            print("    Vous avez perdu, Votre temps est de", self.total_time/100, "secondes, bien joué à vous ;)")
            print("_____________________________________________________________________________\n")
            self.running = False
            break
        else:
            pass

La méthode update est appelée à chaque boucle de jeu pour mettre à jour les positions des briques, vérifier si le joueur a perdu en entrant en collision avec une brique et pour augmenter la difficulté du jeu.

Elle commence par incrémenter le compteur de temps de spawn des briques par la difficulté du jeu, pour que les briques apparaissent plus rapidement avec des niveaux de difficulté plus élevés.

Ensuite, si le compteur de spawn des briques est un multiple de 120, la vitesse de déplacement des briques est augmentée de 0,2.

La méthode vérifie également si le compteur d’augmentation de spawn des briques a atteint sa limite, qui est définie par self.brique_spawn_augmentation * 60. Si c’est le cas, le compteur est réinitialisé et le taux de spawn des briques est diminué de 10.

Si le compteur de spawn des briques atteint le taux de spawn des briques défini par self.brique_spawn_rate, la méthode spawn_brique est appelée pour ajouter une nouvelle brique à la liste des positions de briques.

La méthode move_briques est ensuite appelée pour déplacer toutes les briques dans la direction de la vitesse actuelle des briques.

Le temps total de jeu est ensuite incrémenté de 1.

Enfin, pour chaque rect de brique dans la liste self.brique_rects, la méthode vérifie s’il y a une collision avec le rectangle de la zone de collision du joueur. Si c’est le cas, la méthode affiche le message de fin de jeu et met la variable self.running à False pour arrêter la boucle de jeu.

def display(self):
    # Afficher le fond d'écran
    self.screen.blit(self.background, (-200, -350))

    # Parcourir toutes les positions des briques et les afficher
    for pos in self.brique_positions:
        self.screen.blit(self.brique, pos)

    # Afficher le joueur
    self.player.draw(self.screen)

    # Mettre à jour l'affichage
    pygame.display.flip()

La méthode display(self) affiche les éléments du jeu sur l’écran. Elle utilise la méthode blit pour afficher le fond d’écran, chaque brique de la liste brique_positions et le joueur (player) sur l’écran. Ensuite, elle appelle la méthode flip de la classe pygame.display pour mettre à jour l’affichage.

def run(self):
    # Boucle principale du jeu
    while self.running:
        # Gérer les événements (ex: appuyer sur une touche, quitter le jeu)
        self.gestion_events()

        # Mettre à jour le jeu (ex: déplacer les briques, vérifier les collisions)
        self.update()

        # Afficher le jeu
        self.display()

        # Limiter le nombre de frames par seconde à 60
        self.clock.tick(60)

    # Réafficher la souris et quitter Pygame
    pygame.mouse.set_visible(True)
    pygame.quit()

La méthode run(self) contient la boucle principale du jeu. Elle s’exécute tant que l’attribut running est vrai. À chaque itération de la boucle, elle gère les événements (appel à gestion_events()), met à jour les éléments du jeu (appel à update()), affiche le contenu du jeu sur l’écran (appel à display()) et attend un certain temps défini par la méthode tick de pygame.time.Clock. Enfin, elle quitte le jeu (pygame.quit()) lorsque la boucle est terminée.

# Initialiser Pygame
pygame.init()

# Créer une fenêtre de jeu
screen = pygame.display.set_mode((1080, 720))

# Créer une instance de la classe Game en lui passant la fenêtre de jeu en argument
game = Game(screen)

# Lancer le jeu
game.run()

# Quitter Pygame
pygame.quit()

La partie en dehors de la classe initialise la bibliothèque Pygame (pygame.init()) et crée une fenêtre de jeu (pygame.display.set_mode((1080, 720))). Ensuite, elle crée une instance de la classe Game avec l’écran en tant qu’argument et appelle la méthode run de l’instance pour démarrer le jeu. Enfin, elle quitte la bibliothèque Pygame (pygame.quit()) après la fin du jeu.

Sources :

Conclusion

En conclusion, ce projet est un petit jeu simple mais amusant, il est sur l’univers de Mario. Si vous voulez essayer ce jeu par vous-même, vous pouvez le télécharger en cliquant sur le lien ci-dessous. Pour lancer le jeu, assurez-vous d’écrire le niveau de difficulté souhaité (1, 2 ou 3) dans la console Python. Si vous ne le faites pas, la fenêtre restera noire et le jeu ne se lancera pas. On commande le personnage avec sa souris.

Amusez-vous bien !

1 commentaire
  • Victor E.
    Répondre

    ez

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.