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.