Accueil

Traduction

Tutoriel Python - sommaire

Tutoriel Python - recherche

L'auteur : Patrick Darcheville

Vous pouvez me contacter via Facebook pour questions & suggestions : Page Facebook relative à mon site

Pygame et la POO - deux jeux vidéo célèbres

Les techniques que nous avons utilisé jusqu'a présent ne seront pas efficaces car chacun de ces jeux comprend des dizaines de sprites. Aussi pour ces deux jeux nous allons recourir à la POO (Programmation Orientée Objet).
Dans chacun des jeux nous définiront des classes qui héritent de la classe pygame.sprite.Sprite puis créer des instances de ces classes.
Nous allons aussi créer des groupes de sprites pour simplifier encore davantage la programmation.

La POO fait peur à de nombreux développeurs débutants mais qu'ils se rassurent ; dans le cadre de Pygame la POO c'est très concret et le module pygame.sprite propose des fonctions très performantes.

Je vous invite de rafraichir vos connaissances de base en POO en relisant le chapitre qui est en lien : La POO en Python

le jeu "casse briques"

Vous connaissez le jeu "casse-briques" et son fameux mur de briques.
Je vous présente d'abord la première étape de ce jeu : la construction du mur de briques.

Étape 1 : création du mur de briques

Le code

# nom du prog : mur_briques.py
import pygame
largeur = 600
hauteur = 300

class Briques(pygame.sprite.Sprite) :
    def __init__(self) :
        super().__init__()
        self.image = pygame.Surface((58,18))
        self.image.fill('red')
        self.rect = self.image.get_rect()
# fin définition de la classe

pygame.init()
fenetre = pygame.display.set_mode((largeur, hauteur))
pygame.display.set_caption("pygame et POO")
frequence = pygame.time.Clock()
# créer un groupe de sprites
mur = pygame.sprite.Group()
col = 0
lig = 0
# boucles imbriquées pour construire un mur de briques : 5 rangées de 10 briques
for rangee in  range(1,6) :
    for  n_brique in range(1,11) :
       brique = Briques()
       brique.rect.x = col
       brique.rect.y = lig
       mur.add(brique)
       col +=60   # espacement entre deux briques
    col =0  # pour nouvelle rangée de briques
    lig += 20

encore = True
# ------------boucle de jeu -------------
while encore :
    for event in pygame.event.get() :
        if event.type ==pygame. QUIT :
            encore = False
    fenetre.fill('black')
    # Dessiner le mur de briques
    mur.draw(fenetre)
    pygame.display.flip()
    frequence.tick(8)
# ----------------
pygame.quit()

Le rendu

Construction d'un mur de 50 briques (5 rangées de 10 briques)

Étude détaillée de ce script

Définition de la classe "Briques"
class Briques(pygame.sprite.Sprite) :
    def __init__(self) :
        super().__init__()
        self.image = pygame.Surface((58,18))
        self.image.fill((rouge))
        self.rect = self.image.get_rect()

Via ce code il y a création d'une classe Briques qui hérite de la classe pygame.sprite.Sprite.
Comme la classe Briques hérite de la classe pygame.sprite.Sprite elle hérite aussi de ses attributs qui permettent de définir la surface (attribut image) et le positionnement de cette surface (attribut rect).

Commentons les instructions ci-dessus.
La première ligne super().__init__() appelle le constructeur de la classe parente.
self.image = pygame.Surface((58, 18)) : création d'un objet pygame.Surface de dimensions (58, 18) représentant l'apparence d'une brique.
self.image.fill('red') : remplir cette surface avec la couleur rouge.
self.rect = self.image.get_rect() : la méthode get_rect() renvoie un Rect dont la largeur et la hauteur correspondent à celles de la surface.

Création de 50 instances de Briques

On crée donc 50 objets 'briques' via deux boucles imbriquées.

# créer les sprites
mur = pygame.sprite.Group() # création d'un groupe de sprites
col = 0
lig = 0
"""
boucles imbriquées pour construire un mur de 
briques : 5 rangées de 10 briques
 """
for rangee in  range(1,6) :
    for  n_brique in range(1,11) :
       brique = Briques()
       brique.rect.x = col
       brique.rect.y = lig
       mur.add(brique) # ajout de ce sprite au groupe mur
       col +=60   # espacement entre deux briques
    col =0  # pour nouvelle rangée de briques
    lig += 20
# fin boucles imbriquées

mur = pygame.sprite.Group() : création d'un groupe de sprites nommé "mur" ; groupe qui est vide au départ.
Nous avons utilisé le constructeur

pygame.sprite.Group()

Une brique fait 58 par 18 alors que "col" augmente de 60 et "lig" de 20. Ainsi les briques ne sont touchent pas mais il y a un joint entre elles.
Attention le Rect est "brique.rect". Il faut appliquer les propriétés d'un Rect à "brique.rect".

Dessiner les 50 briques
	mur.draw(fenetre)

Cette instruction (dans la boucle de jeu) dessine les 50 sprites appartenant au groupe "mur".
Reconnaissez que c'est assez blufant ; une seule instruction pour dessiner 50 sprites ...

Le jeu casse briques - suite

je viens de vous présenter la première étape du jeu : créer le mur.

Le fichier "casse_briques_classes.py"

Vous retrouvez la définition de la classe Briques mais auss de trois autres classes.

Le script

# casse_briques_classes.py
import pygame
largeur, hauteur = 600, 600

class Briques(pygame.sprite.Sprite) :
    def __init__(self) :
        pygame.sprite.Sprite.__init__(self) 
        self.image = pygame.Surface((58,18))
        self.image.fill('red')
        self.rect = self.image.get_rect()

class Raquettes(pygame.sprite.Sprite):
    def __init__(self) :
        pygame.sprite.Sprite.__init__(self) 
        self.image = pygame.Surface((100,10))
        self.image.fill('white')
        self.rect = self.image.get_rect()
    
class Balles(pygame.sprite.Sprite):
    def __init__(self) :
        pygame.sprite.Sprite.__init__(self) 
        self.image = pygame.Surface((40,40))
        self.image.fill('black')
        self.rect = self.image.get_rect()
        self.deltax = 10
        self.deltay = 10
    def update(self):
    # gestion rebond de la balle
        if self.rect.left <= 0:
            self.deltax = - self.deltax
        if self.rect.right >= largeur:
            self.deltax = - self.deltax
        if self.rect.top <= 0:
            self.deltay = - self.deltay
        if self.rect.bottom >= hauteur:
            self.deltay = - self.deltay
       
         # déplacement automatique de la balle
        self.rect.x +=self.deltax
        self.rect.y +=self.deltay
    def dessiner(self):
        pygame.draw.circle(self.image, 'blue', (20, 20), 20)

class Outs(pygame.sprite.Sprite) :
    def __init__(self) :
        super().__init__()
        self.image = pygame.Surface((largeur,5))
        self.image.fill('red')
        self.rect = self.image.get_rect()
# fin définition des classes

pygame.init()
fenetre = pygame.display.set_mode((largeur, hauteur))
pygame.display.set_caption("pygame et POO")
frequence = pygame.time.Clock()

# créer quatre groupes de sprites (initialement vides)
mur = pygame.sprite.Group() #groupe regroupant les briques
raquettes =pygame.sprite.Group() #groupe regroupant les raquettes
outs = pygame.sprite.Group() #groupe regroupant les outs
tous = pygame.sprite.Group() #groupe regroupant tous les sprites

# la raquette
raquette = Raquettes()
raquette.rect.center = (300, hauteur-20)
tous.add(raquette) # sprite ajouté au groupe 'tous'
raquettes.add(raquette)

# la balle
balle = Balles()
balle.dessiner()
balle.rect.center =(300,300)
tous.add(balle) 	# sprite ajouté au groupe 'tous'

# la zone out
out = Outs()
out.rect.topleft = (0, hauteur-5)
tous.add(out)		# sprite ajouté au groupe 'tous'
outs.add(out)                                   

# construction du mur de briques
col = 0
lig = 0
# boucles imbriquées 
for rangee in  range(1,6) :
    for  n_brique in range(1,11) :
       brique = Briques()
       brique.rect.x = col
       brique.rect.y = lig
       mur.add(brique) # sprite ajouté au groupe 'mur'
       tous.add(brique) # sprite ajouté au groupe 'tous'
       col +=60   # espacement entre deux briques
    col =0  # pour nouvelle rangée de briques
    lig += 20
# fin boucles imbriquées

Je ne reviens pas sur la définition de la classe Briques.
Concernant la classe Raquettes, toujours la même démarche : création d'une surface, remplissage de cette surface puis création d'un objet rect à partir de la surface.
Même remarque pour les classes Raquettes, Balles & Outs.

Le code de la classe Balles

  ...
		self.deltax = 10
		self.deltay = 10
    def update(self):
    # gestion rebond de la balle
        if self.rect.left <= 0:
            self.deltax = - self.deltax
        if self.rect.right >= largeur:
            self.deltax = - self.deltax
        if self.rect.top <= 0:
            self.deltay = - self.deltay
        if self.rect.bottom >= hauteur:
            self.deltay = - self.deltay
       
     # déplacement automatique de la balle
        self.rect.x +=self.deltax
        self.rect.y +=self.deltay
    
	def dessiner(self):
        pygame.draw.circle(self.image, 'blue', (20, 20), 20)
La méthode update(self)

Si la balle touche un bord alors deltax / deltay change de signe.
À chaque itération l'abscisse et l'ordonnée varie de 10 (en plus ou en moins en fonction du signe de deltax/deltay).

La méthode dessiner(self)

Dessin d'un cercle de rayon 20 au centre de la surface, rond rempli de bleu.

Les groupes de sprites

Il faut créer des groupes de sprites via le constructeur pygame.sprite.Group().
La manipulation de groupes de sprites simplifie considérablement la programmation comme je l'ai déjà montré dans le paragraphe précédent avec l'instruction mur.draw(fenetre) qui dessinait à elle seule 50 sprites ...

Le fichier "casse_briques_jeu.py"

Ce script se résume pratiquement à la boucle de jeu.

# nom prog : casse_briques_jeu.py
from casse_briques_classes import *              

sorties = 0
encore = True
#-------------boucle de jeu-----------------
while encore :
    for event in pygame.event.get() :
        if event.type ==pygame. QUIT :
            encore = False
    # déplacement latéral par clavier de la raquette
    touches = pygame.key.get_pressed()
    if raquette.rect.left >=0 : 
        if touches[pygame.K_LEFT]:
            raquette.rect.x -= 12
    if raquette.rect.right <=largeur : 
        if touches[pygame.K_RIGHT]:
            raquette.rect.x += 12

    # gestion collisions		
    if pygame.sprite.spritecollide(balle,mur,True):
        print("une ou des briques détruites")
        balle.deltay *= -1
    if pygame.sprite.spritecollide(balle,raquettes,False):
        print("Beau retour")
        balle.deltay *= -1
    if pygame.sprite.spritecollide(balle,outs,False):
        print("Balle dehors !")
        balle.deltay *= -1
        sorties +=1
    if len(mur) ==0 :
        print("Tout  le mur détruit ! ")
        encore = False
    if sorties >=20 :
        print("Vous avez perdu")
        encore = False
          
    # effacer la fenêtre précédente
    fenetre.fill('black')
    # MAJ emplacement balle
    balle.update()
    # Dessiner tous les sprites
    tous.draw(fenetre)
    # Mettre à jour l'affichage
    pygame.display.flip()
    frequence.tick(10)
# ------------------------
pygame.time.wait(3000) 
pygame.quit()

from casse_briques_classes import * : importation du "sous-programme" dans le programme principal.

Gestion des collisions

La fonction pygame.sprite.spritecollide() permet de détecter les collisions entre un sprite et un groupe de sprites en renvoyant une liste des sprites en collision et optionnellement en les supprimant.
C'est pour cette raison que j'ai du créer un groupe de sprites "raquettes" et un groupe "outs" car le deuxième argument de la fonction doit impérativement être un groupe de sprites.

# gestion collisions		
    if pygame.sprite.spritecollide(balle,mur,True):
        print("une ou des briques détruites")
        balle.deltay *= -1
    if pygame.sprite.spritecollide(balle,raquettes,False):
        print("Beau retour")
        balle.deltay *= -1
    if pygame.sprite.spritecollide(balle,outs,False):
        print("Balle dehors !")
        balle.deltay *= -1
        sorties +=1

if pygame.sprite.spritecollide(balle,mur,True) : si la balle touche un ou des briques du mur celles-ci sont supprimées (car le troisième argument est à True).
if pygame.sprite.spritecollide(balle,raquettes,False): le troisième argument est False, donc la raquette "survit" au contact et il y a rebond de la balle.

Dessiner les sprites

Une seule instruction pour redessiner à chaque itération : les briques restante, la balle, la raquette, la zone 'out' :

	tous.draw(fenetre)

Bluffant !

Le rendu

L'objectif du jeu est de détruire le mur de briques dans le délai le plus court.
La balle peut toucher le bord inférieur de la fenêtre (zone 'out' remplie de rouge) 20 fois seulement.
Le temps est illimité pour détruire le mur.
Pour durcir ou ramollir le jeu il suffit de jouer sur la FPS (qui est règlée à 10).

Variante

On peut imaginer le jeu avec une durée toujours limitée à 1 minute ; le score étant égal au nombre de briques supprimées.
La sortie de la balle par le bas n'est plus sanctionnée.

Ci-dessous extraits de la variante "casse_briques_jeu2.py" :

# nom prog : casse_briques_jeu2.py
# durée du jeu limitée à 60 secondes
from casse_briques_classes import *
print("Attention le jeu va se lancer dans 5 secondes")
pygame.time.wait(5000)  # Pause de 5000 millisecondes
boucle_jeu =0
# ---- boucle de jeu------------
while boucle_jeu <: 600: 
...

	# gestion collisions		
    if pygame.sprite.spritecollide(balle,mur,True):
        balle.deltay *= -1
        print(len(mur))
    if pygame.sprite.collide_rect(balle,raquette):
        balle.deltay *= -1
    if pygame.sprite.collide_rect(balle,out):
        balle.deltay *= -1
	...
	
    boucle_jeu+= 1
    frequence.tick(10)
# ------------------------
score = 50- len(mur)
print(f"Votre score est : {score}")
pygame.time.wait(5000)  # Pause de 5000 millisecondes
pygame.quit()

Le jeu ne démarre pas tout de suite, le joueur a le temps de se préparer.
Comme la FPS est 10, la boucle de jeu doit être exécutée 600 fois (60 secondes * 10).

Attention pour la gestion des collisions entre balle et raquette d'une part et entre balle et 'out' d'autre part, j'ai utilisé une autre méthode : spritecollide_rect(sprite,sprite).
Donc grâce à cette méthode, les groupes de sprites "raquettes" et "outs" deviennent inutiles.

Amusez-vous !

Téléchargez ce fichier

Le jeu du labyrinthe

Le joueur doit déplacer le plus vite possible un personnage dans un labyrinthe. Un labyrinthe c'est comme un mur de briques mais avec des briques absentes.

Étape 1 : construction du labyrinthe

Le script

# labyrinthe_decor.py
# jeu de labyrinthe - construction du décor
import pygame, random

tuile = 30
largeur_laby = tuile * 25
hauteur_laby = tuile * 25
X = largeur_laby + 20
Y = hauteur_laby + 20

pygame.init()
class Briques(pygame.sprite.Sprite) :
    def __init__(self) :
        pygame.sprite.Sprite.__init__(self) 
        self.image = pygame.Surface((28,28))
        self.image.fill("red")
        self.rect = self.image.get_rect()

class Personnages(pygame.sprite.Sprite):
  def __init__(self):
    pygame.sprite.Sprite.__init__(self)
    self.image = pygame.image.load("images_sons/homme.png").convert_alpha()
    self.rect = self.image.get_rect()

fenetre = pygame.display.set_mode((X, Y))
pygame.display.set_caption('Labyrinthe décor')

# groupes de sprites
mur = pygame.sprite.Group() # pour regrouper les briques
tous = pygame.sprite.Group() # pour regrouper tous les sprites

# construction du labyrinthe à partir d'un fichier TXT
x = 0
y = 0
liste = ["lab1.txt", "lab2.txt", "lab3.txt"]
modele = random.choice(liste)
print(f"modele de  labyrinthe retenu est : {modele}")
with open(modele, "r") as fichier:
  for ligne in fichier:
    for car in ligne:
        if car == 'B':
           brique = Briques()
           brique.rect.x = x
           brique.rect.y = y
           mur.add(brique)
           tous.add(brique)
        else :
           pass
        x+=30
    x= 0
    y+=30

# création d'une instance de Personnage
personnage = Personnages()
personnage.topleft =(0,0)
tous.add(personnage)

frequence = pygame.time.Clock()
encore = True
# -------------boucle de jeu------------
while encore :
    for event in pygame.event.get() :
        if event.type ==pygame. QUIT :
            encore = False
               
    # effacer la fenêtre précédente
    fenetre.fill("white")
    # Dessiner tous les sprites
    tous.draw(fenetre)
    # Mettre à jour l'affichage
    pygame.display.flip()
    frequence.tick(8)
# ---------------------
pygame.quit()

Commentaires

Dimensions du labyrinthe
tuile =30
largeur_laby = tuile * 25
hauteur_laby = tuile * 25
Notion de 'tuile'

J'introduis ici la notion de 'tuile' ; une 'tuile' est une entité élémentaire de la fenêtre de jeu.
Ici une tuile est un simple carré de 30 par 30. Donc la fenêtre de jeu est comme un damier (ou une matrice) de L tuiles sur H tuiles. Ici L (nombre de tuiles en largeur) est 25 et H (nombre de tuiles en hauteur).
Cette façon de définir la fenêtre de jeu offre beaucoup de souplesse ; si vous voulez rédéfinir les dimensions de la fenêtre de jeu il suffit de modifier les valeurs de L & H donc ne modifier que deux instructions du script.

Aire d'arrivée
X = largeur_laby + 20
Y = hauteur_laby + 20

Le personnage sortant du labyrinthe arrive dans cette zone situé tout en bas et à droite du labyrinthe.

Utilisation de fichiers TXT

liste = ["lab1.txt", "lab2.txt", "lab3.txt"] : liste de trois fichiers txt
modele = random.choice(liste) : choix aléatoire d'un des trois fichiers.

Contenu d'un des fichiers TXT :

  BBBBBBBBBBBBBBBBBBBBBBB
                       BB
BBBBBBB BBBBB BBBB BBBBBB
B                       B
BBBBBBBBBB BBBBBBBBBBBBBB
B      BBB BB           B
BBBB B        BB BBB B BB
B  B BBBBBBBBBBB BBB B  B
B  B           B BBB B  B
B  BBBBB BBBBBBBBBBB BBBB
B                       B
B BBBBBBBBB BBBBBB BBBBBB
B             BB       BB
BBBBBB BBBBBBBBBBB BBBBBB
BB BBBB                 B
BB BBBB  BBBBBBBBBBBB  BB
BB BBB           BBBB  BB
BB BBBBBBBBBBB  BBBBB  BB
BB                 BB  BB
BBBBBB     BBBBBBB  BBBBB
BBBBBB  BBBBB       BBBBB
B         BBB BBBBB     B
BBBBBBBBB BBB BBBBBB    B
B                  BB	B
BBBBBBBBBBBBBBBBBBBBBBB  

25 lignes et 25 colonnes. Chaque caractère est 'B' ou 'espace'.

Création de deux groupes de sprites
# groupes de sprites
mur = pygame.sprite.Group()
tous = pygame.sprite.Group()
Création du labyrinthe

La lecture caractère par caractère du fichier TXT va permettre de construire le labyrinthe.

with open(modele, "r") as fichier:
  for ligne in fichier:
    for car in ligne:
        if car == 'B':
           brique = Briques()
           brique.rect.x = x
           brique.rect.y = y
           mur.add(brique)
           tous.add(brique)
        else :
           pass
        x+=30
    x= 0
    y+=30

Ouverture du fichier TXT.
Lecture caractère par caractère de ce fichier.
Si le caractère lu est 'B' on crée une instance de Briques et on la positionne sinon "pass".

Le rendu

Observez bien le personnage à l'entrée du labyrinthe.

Le programme du jeu "labyrinthe"

Pour obtenir le programme du jeu à partir du script précédent il faut tout d'abord rajouter deux instructions avant la boucle de jeu.

...
debut_jeu = pygame.time.get_ticks()
delta = tuile

debut_jeu = pygame.time.get_ticks() : une des composantes pour calculer le temps de parcours du labyrinthe.
delta =tuile : paramètre pour déplacer le bonhomme ; "tuile" contient 30.

Le code complet de la boucle de jeu

Le code est donc le suivant :

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

    # gestion collisions du personnage avec le mur	
    if pygame.sprite.spritecollide(personnage,mur,False):
        print("Dans le mur")
        if derniere_touche =='L' :
            personnage.rect.x +=delta
        if derniere_touche =='R' :
            personnage.rect.x -=delta
        if derniere_touche =='U' :
            personnage.rect.y +=delta
        if derniere_touche =='D' :
            personnage.rect.y -=delta

    # deplacement du personnage
    touches = pygame.key.get_pressed()
    if touches[pygame.K_LEFT]:
        personnage.rect.x -= delta
        derniere_touche = 'L'
    if touches[pygame.K_RIGHT]:
        personnage.rect.x += delta
        derniere_touche ='R'
    if touches[pygame.K_UP]:
        personnage.rect.y -= delta
        derniere_touche ='U'
    if touches[pygame.K_DOWN]:
        personnage.rect.y += delta
        derniere_touche ='D'

    # gestion sortie du labyrinthe
    if (personnage.rect.y >= largeur_utile) and (personnage.rect.y >= largeur_utile) : 
        fin_jeu = pygame.time.get_ticks()
        temps_jeu = (fin_jeu - debut_jeu) //1000
        print(f"Vous avez parcouru le labyrinthe en  {temps_jeu} secondes")
        pygame.time.wait(5000)
	
	fenetre.fill("white")
    tous.draw(fenetre)
    pygame.display.flip()
    frequence.tick(8)

Commentons certains blocs d'instructions

Déplacement du personnage
    touches = pygame.key.get_pressed()
    if touches[pygame.K_LEFT]:
        personnage.rect.x -= delta
        derniere_touche = 'L'
    ...
	if touches[pygame.K_UP]:
        personnage.rect.y -= delta
        derniere_touche ='U'
	...

Récupération dans la variable "derniere_touche" de 'L' ou 'R' ou 'U' ou 'D'. Le contenu de cette variable permet de gérer les collisions.

Gestion des collisions

Le personnage n'est pas un "perce-muraille" ; s'il entre en contact avec le mur il doit rebondir ; il ne peut pas détruire la brique.

	if pygame.sprite.spritecollide(personnage,mur,False):
        print("Dans le mur")
        if derniere_touche =='L' :
            personnage.rect.x +=delta
        if derniere_touche =='R' :
            personnage.rect.x -=delta
		...

Donc de façon générale cette gestion des collisions consiste à annuler le dernier déplacement du personnage.

Gestion arrivée à la dernière case
    if (personnage.rect.y >= largeur_laby) and (personnage.rect.y >= largeur_laby) : 
        fin_jeu = pygame.time.get_ticks()
        temps_jeu = (fin_jeu - debut_jeu) //1000
        print(f"Vous avez parcouru le labyrinthe en  {temps_jeu} secondes")
        pygame.time.wait(5000)
        encore = False

Si le personnage arrive dans l'aire d'arrivée alors calcul du temps de parcours et affichage de celui-ci.

Instructions toujours exécutées dans la boucle de jeu
	fenetre.fill("white")
    tous.draw(fenetre)
    pygame.display.flip()
    frequence.tick(8)

tous.draw(fenetre) : une seule instruction pour redessiner tous les sprites ...
La FPS est règlé à 8 mais ce paramètre a peu d'importance ici puisqu'il y a pas de sprites animés automatiquement.

Pour aller plus loin

Je vous invite à lire le chapitre suivant (et dernier) sur Pygame. Vous trouverez une version "enrichie" du jeu.
Enrichie en ce sens que des choses vertes (des salades) sont positionnées de façon aléatoire dans les couloirs. Le joueur doit non avaler toutes ces choses vertes et parcourir le labyrinthe le plus vite possible.