Développement d'un jeu de course de dinosaure avec Pygame en Python

Cet article détaille la création d'un jeu de type "infinite runner" similaire au jeu du dinosaure intégré dans Google Chrome, en utilisant Python et la bibliothèque Pygame. L'objectif est de construire les composants fondamentaux d'un tel jeu : initialisation, gestion des événements, mise à jour des éléments, rendu graphique et détection de collisions.

Le joueur contrôle un dinosaure avec la touche Haut pour sauter par-dessus des obstacles. Le jeu accélère progressivement et le score augmente avec le temps.

Structure du projet et configurasion

Le projet est organisé en plusieurs modules pour une meilleure lisibilité. Un fichier de configuration centralise les paramètres clés.

Fichier de configuration

"""Configuration et constantes du jeu"""
import os

TAILLE_ECRAN = (600, 150)
FREQUENCE_IMAGES = 60
COULEUR_FOND = (235, 235, 235)
COULEUR_NOIR = (0, 0, 0)
COULEUR_BLANC = (255, 255, 255)

CHEMINS_AUDIO = {
    'mort': os.path.join(os.getcwd(), 'ressources/audios/mort.wav'),
    'saut': os.path.join(os.getcwd(), 'ressources/audios/saut.wav'),
    'point': os.path.join(os.getcwd(), 'ressources/audios/point.wav')
}

CHEMINS_IMAGES = {
    'cactus': [os.path.join(os.getcwd(), 'ressources/images/cactus_grand.png'),
               os.path.join(os.getcwd(), 'ressources/images/cactus_petit.png')],
    'nuage': os.path.join(os.getcwd(), 'ressources/images/nuage.png'),
    'dino': [os.path.join(os.getcwd(), 'ressources/images/dino.png'),
             os.path.join(os.getcwd(), 'ressources/images/dino_baisse.png')],
    'gameover': os.path.join(os.getcwd(), 'ressources/images/gameover.png'),
    'sol': os.path.join(os.getcwd(), 'ressources/images/sol.png'),
    'chiffres': os.path.join(os.getcwd(), 'ressources/images/chiffres.png'),
    'ptera': os.path.join(os.getcwd(), 'ressources/images/ptera.png'),
    'rejouer': os.path.join(os.getcwd(), 'ressources/images/rejouer.png')
}

Implémentation des sprites du jeu

Chaque élément du jeu (personnage, obstacles, décor) est encapsulé dans une classe héritant de pygame.sprite.Sprite.

Classe du Dinosaure

"""Module définissant le personnage principal"""
import pygame


class DinosaurSprite(pygame.sprite.Sprite):
    """Gère le sprite du dinosaure avec ses animations et mouvements."""
    
    def __init__(self, fichiers_images, position_initiale=(40, 147), dimensions=[(44, 47), (59, 47)], **kwargs):
        super().__init__()
        self._charger_animations(fichiers_images, dimensions)
        self.index_animation = 0
        self.image = self.sprites[self.index_animation]
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.bottom = position_initiale
        self.masque = pygame.mask.from_surface(self.image)
        
        self.position_base = position_initiale
        self.frequence_anim = 5
        self.compteur_anim = 0
        self.vitesse_verticale = 11.5
        self.gravite = 0.6
        self.en_saut = False
        self.est_baisse = False
        self.est_mort = False
        self.deplacement = [0, 0]

    def _charger_animations(self, fichiers, dimensions):
        """Charge et redimensionne les images d'animation."""
        self.sprites = []
        image1 = pygame.image.load(fichiers[0])
        for i in range(5):
            zone = image1.subsurface((i * 88, 0), (88, 95))
            self.sprites.append(pygame.transform.scale(zone, dimensions[0]))
        image2 = pygame.image.load(fichiers[1])
        for i in range(2):
            zone = image2.subsurface((i * 118, 0), (118, 95))
            self.sprites.append(pygame.transform.scale(zone, dimensions[1]))

    def executer_saut(self, sons):
        """Déclenche le saut si les conditions le permettent."""
        if self.est_mort or self.en_saut:
            return
        sons['saut'].play()
        self.en_saut = True
        self.deplacement[1] = -self.vitesse_verticale

    def baisser(self):
        """Active la position basse si possible."""
        if self.en_saut or self.est_mort:
            return
        self.est_baisse = True

    def relever(self):
        self.est_baisse = False

    def declarer_mort(self, sons):
        if self.est_mort:
            return
        sons['mort'].play()
        self.est_mort = True

    def dessiner(self, surface_cible):
        surface_cible.blit(self.image, self.rect)

    def _mettre_a_jour_image(self):
        self.image = self.sprites[self.index_animation]
        ancien_rect = self.rect
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = ancien_rect.left, ancien_rect.top
        self.masque = pygame.mask.from_surface(self.image)

    def mettre_a_jour_etat(self):
        if self.est_mort:
            self.index_animation = 4
            self._mettre_a_jour_image()
            return
            
        if self.en_saut:
            self.deplacement[1] += self.gravite
            self.index_animation = 0
            self._mettre_a_jour_image()
            self.rect = self.rect.move(self.deplacement)
            if self.rect.bottom >= self.position_base[1]:
                self.rect.bottom = self.position_base[1]
                self.en_saut = False
        elif self.est_baisse:
            if self.compteur_anim % self.frequence_anim == 0:
                self.compteur_anim = 0
                self.index_animation = 5 if self.index_animation == 6 else 6
                self._mettre_a_jour_image()
        else:
            if self.compteur_anim % self.frequence_anim == 0:
                self.compteur_anim = 0
                if self.index_animation == 1:
                    self.index_animation = 2
                elif self.index_animation == 2:
                    self.index_animation = 3
                else:
                    self.index_animation = 1
                self._mettre_a_jour_image()
        self.compteur_anim += 1

Classes des obstacles

"""Module pour les obstacles (cactus et ptérodactyles)"""
import pygame


class CactusSprite(pygame.sprite.Sprite):
    """Représente un obstacle fixe de type cactus."""
    
    def __init__(self, fichier_image, position, taille=(50, 50), **kwargs):
        super().__init__()
        self.image = pygame.image.load(fichier_image)
        self.image = pygame.transform.scale(self.image, taille)
        self.rect = self.image.get_rect()
        self.rect.bottom = position[1]
        self.masque = pygame.mask.from_surface(self.image)
        self.vitesse = -10

    def dessiner(self, surface_cible):
        surface_cible.blit(self.image, self.rect)

    def mettre_a_jour(self):
        self.rect = self.rect.move([self.vitesse, 0])
        if self.rect.right < 0:
            self.kill()


class PterodactyleSprite(pygame.sprite.Sprite):
    """Représente un obstacle animé de type ptérodactyle."""
    
    def __init__(self, fichier_image, position, taille=(46, 40), **kwargs):
        super().__init__()
        self.images = []
        image_source = pygame.image.load(fichier_image)
        for i in range(2):
            zone = image_source.subsurface((i * 92, 0), (92, 81))
            self.images.append(pygame.transform.scale(zone, taille))
        self.index_anim = 0
        self.image = self.images[self.index_anim]
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.centery = position
        self.masque = pygame.mask.from_surface(self.image)
        self.vitesse = -10
        self.freq_anim = 10
        self.cpt_anim = 0

    def dessiner(self, surface_cible):
        surface_cible.blit(self.image, self.rect)

    def mettre_a_jour(self):
        if self.cpt_anim % self.freq_anim == 0:
            self.cpt_anim = 0
            self.index_anim = (self.index_anim + 1) % len(self.images)
            self._actualiser_image()
        self.rect = self.rect.move([self.vitesse, 0])
        if self.rect.right < 0:
            self.kill()
        self.cpt_anim += 1

    def _actualiser_image(self):
        self.image = self.images[self.index_anim]
        ancien_rect = self.rect
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = ancien_rect.left, ancien_rect.top
        self.masque = pygame.mask.from_surface(self.image)

Gestion des interfaces de jeu

Les écrans de démarrage et de fin de partie sont gérés par des fonctions dédiées qui contrôlent leur propre boucle d'événements.

Écran de démarrage

"""Interface de l'écran de démarrage"""
import sys
import pygame
from modules.sprites.dinosaure import DinosaurSprite

def afficher_ecran_demarrage(surface, sons, configuration):
    """Affiche l'écran de démarrage et attend que le joueur initie la partie."""
    dino_sprite = DinosaurSprite(configuration.CHEMINS_IMAGES['dino'])
    image_sol = pygame.image.load(configuration.CHEMINS_IMAGES['sol']).subsurface((0, 0), (83, 19))
    rect_sol = image_sol.get_rect()
    rect_sol.left, rect_sol.bottom = configuration.TAILLE_ECRAN[0] / 20, configuration.TAILLE_ECRAN[1]
    horloge = pygame.time.Clock()
    action_declenchee = False
    
    while True:
        for evenement in pygame.event.get():
            if evenement.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif evenement.type == pygame.KEYDOWN:
                if evenement.key in (pygame.K_SPACE, pygame.K_UP):
                    action_declenchee = True
                    dino_sprite.executer_saut(sons)
        
        dino_sprite.mettre_a_jour_etat()
        
        surface.fill(configuration.COULEUR_FOND)
        surface.blit(image_sol, rect_sol)
        dino_sprite.dessiner(surface)
        pygame.display.update()
        horloge.tick(configuration.FREQUENCE_IMAGES)
        
        if not dino_sprite.en_saut and action_declenchee:
            return True

Résolution de problèmes techniques courants

Pendant le développement, plusieurs défis techniques ont été rencontrés et résolus :

  • Installation de Pygame : L'installation via pip a échoué dans certains environnements. La solution a été d'utiliser un gestionnaire de paquets intégré à un IDE comme Anaconda.
  • Organisation du code : Pour gérer la complexité, le projet a été divisé en modules logiques (sprites, interfaces, configuration).
  • Immmersion du joueur : L'ajout d'effets sonores pour le saut, les points et la mort a significativement amélioré l'expérience utilisateur.
  • Chemins d'accès aux ressources : Des erreurs de chargement des images/sons ont été corrigées en vérifiant la validité des chemins relatifs depuis le répertoire de travail.

Étiquettes: Python Pygame Jeu 2D Programmation orientée objet Développement de jeux

Publié le 2 juin à 21h08