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 - des jeux vidéos en POO dont le casse briques

Créez un nouveau dossier à la racine de C: et nommez le "pygame4_prog".

Arrivés à ce stade nous savons creer des jeux vidéos mais ne comprenant que quelques sprites (une fusée et un astre OU un carré et deux ronds, etc.)
Les techniques que nous utilisons ne seront pas efficaces pour manipuler plusieurs dizaines de sprites ; le code deviendrait trop lourd.

Si le jeu vidéo que vous envisagez comprend plusieurs dizaines de sprites il faut alors créer différentes classes puis créer des instances de ces classes en guise de sprites. Il faut donc recourir à la POO (Programmation Orientée Objet).

La POO fait peur à de nombreux programmeurs débutants. Mais rassurez vous. Vous allez voir que dans le cadre de Pygame c'est facile de créer et utiliser des classes. Et le module pygame.sprite propose des fonctions très intéressantes.

Je vous conseille de rafraichir vos connaissances en POO en relisant le chapitre qui est en lien ci-dessous.
La POO en Python

Programme "mur_briques.py"

Vous connaissez le jeu "casse-briques" et son fameux mur de briques.
Je vais vous montrer que ce n'est pas très difficile de créer ce mur à condition de recourir à la POO.

Le code

# nom du prog : mur_briques.py
import pygame
import sys
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
# fin boucles imbriquées

encore = True
# début 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('black')
    # Dessiner le groupe de sprites
    mur.draw(fenetre)
    # Mettre à jour l'affichage
    pygame.display.flip()
    frequence.tick(8)
# fin boucle de jeu
pygame.quit()

Le rendu

pygame & POO

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

Étude détaillée du programme

Utilisation dans ce script de la POO.

Définition de la classe "Briques"

Le code :

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.
La première ligne super().__init__() appelle le constructeur de la classe parente. On aurait pu écrire : pygame.sprite.Sprite.__init__(self).
La deuxième ligne self.image = pygame.Surface((58, 18)) crée un objet Surface de dimensions (58, 18) représentant l'apparence d'une brique.
La troisième ligne self.image.fill('red') remplit cette surface avec la couleur rouge.
La quatrième ligne self.rect = self.image.get_rect() crée un objet de type rect à partir de cette surface.

Création de 60 briques

On crée donc 60 instances de la classe Briques via deux boucles imbriquées.

# créer les 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
# fin boucles imbriquées

mur est le nom donné à groupe de sprites. Il s'agit d'une structure itérable !

Une brique fait 58 par 18 alors que "col" augmente de 60 et "lig" de 20. Ainsi les briques sont bordurées de noir (couleur de fond de la fenêtre de jeu).

Dessiner les 60 briques

  mur.draw(fenetre)

C'est magique ; une seule instruction pour dessiner les 60 briques !

Nous venons donc de créer le décor du jeu de casse-briques.
En fin de chapitre nous compléterons ce script pour obtenir le jeu.

Un nouveau jeu : "la savane cruelle"

Par étapes successives nous allons réaliser un jeu qu'on pourrait intituler "la chasse".
Il n'a rien d'original : un lion doit dévorer le plus vite possible tout un troupeau de gazelles. Mais l'important n'est pas là ...
Dans ce jeu les sprites sont des images PNG.

Première étape :"savane_cruelle1.py"

Le code

Le rendu

pygame & POO

Problème : l'image PNG a un fond blanc (et non pas transparent).
Rassurez vous ce problème est simple à résoudre ... Patientez un peu pour avoir la solution

Étudions en détail certaines parties du code !

Définition de la classe

class Proies(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("gazelle.png").convert_alpha()
        self.rect = self.image.get_rect()
        self.deltax = 10
    def update(self):
        if self.rect.left <= 0:
            self.deltax = - self.deltax
        if self.rect.right >= largeur:
            self.deltax = - self.deltax
        self.rect.x +=self.deltax
# fin définition de la classe

Cette fois la classe Proies comprend non seulement des attributs mais aussi une méthode : update() De plus la surface est remplie par une image PNG (et non pas par une couleur de fond).

La méthode update() gère le déplacement des sprites et leur rebond lorsque le bord est atteint deltax change de signe.

Création de 16 proies

Auparavant on a créé un groupe de sprites nommé "troupeau" : troupeau = pygame.sprite.Group()
Code pour créer 16 instances de la classe Proies.

# création de 16 sprites
col = 50
lig = 40
for i in  range(1,5) :
    for j in range(1,5) :
       proie = Proies()
       proie.rect.x = col
       proie.rect.y = lig
       troupeau.add(proie)
       col+=50
    col = 50
    lig += 50
# fin boucles imbriquées

Il suffit donc d'imbriquer deux boucles FOR pour créer : 4 * 4 instances de la classe "Proies".

Code de la boucle de jeu

   # effacer fenetre précédente
    fenetre.fill('wheat')
    # déplacement troupeau
    troupeau.update()
    # affichage du troupeau de gazelles
    troupeau.draw(fenetre)
    # actualisation 
    pygame.display.flip()

fenetre.fill('wheat') : la fenêtre de jeu a pour fond un jaune blé.
troupeau.update() : application de la méthode update() au groupe de proies. Donc le groupe de sprites est déplacé à chaque itération de la boucle de jeu.

Étape 2 : "savane_cruelle2.py"

Dans cette seconde étape j'ajoute le prédateur (le lion).
De plus les sprites auront désormais un fond transparent. En effet je charge les PNG "lion3.png" et "gazelle3.png" qui sont des images à fond transparent.

Voyons les rajouts de code à effectuer par rapport à la version précédente.

Deux groupes de sprites désormais

# création de deux  groupes de sprites
troupeau = pygame.sprite.Group()
tous = pygame.sprite.Group()

Le groupe "troupeau" ne comprendra que les instances de Proies.
Le groupe "tous" comprendra les proies et le prédateur donc tous les sprites du jeu.
Attention dans le bloc qui crée les instances de Proies il faudra insérer l'instruction : tous.add(proie)

Classe Prédateurs

class Predateurs(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("lion3.png").convert_alpha()
        self.rect = self.image.get_rect()
# fin définition de la classe Prédateur
predateur = Predateurs()
predateur.rect.center = (550,550)
tous.add(predateur)

Je crée la classe "Predateurs" puis une instance de cette classe nommmée "predateur" ; j'ajoute ce sprite au groupe "tous" et je positionne le sprite avec la méthode "center".

Les sprites créés sont des objets rect donc on peut appliquer toutes les méthodes d'un objet rect : center, topleft, topright, bottomleft, bottomright, etc.

Déplacement du prédateur

Il doit se déplacer dans les quatre directions sans sortir de la fenêtre de jeu.
On retrouve un code que vous connaissez déjà (bloc dans la boucle de jeu).

# deplacement du predateur
    touches = pygame.key.get_pressed()
    if predateur.rect.left >=0 : 
        if touches[pygame.K_LEFT]:
            predateur.rect.x -= 10
    if predateur.rect.right <= largeur :
        if touches[pygame.K_RIGHT]:
            predateur.rect.x += 10
    if predateur.rect.top >= 0 :
        if touches[pygame.K_UP]:
            predateur.rect.y -= 10
    if predateur.rect.bottom <= hauteur :
        if touches[pygame.K_DOWN]:
            predateur.rect.y += 10

Le déplacement est "bordé" : autorise le déplacement seulement si le bord n'est pas atteint.
Remarque : on utilise dans les instructions des propriétés de l'objet rect : left, right, top, bottom, x, y ...

Autres modification dans la boucle de jeu

	# déplacement troupeau
    troupeau.update()	
    # affichage des sprites (troupeau & predateur)
    tous.draw(fenetre)

Le prédateur se déplace via le clavier (donc par le joueur) ; le troupeau se déplace conformément à la méthode update().
La méthode update() s'applique donc au groupe "troupeau".
La méthode draw() s'applique au groupe "tous" donc à tous les sprites (gazelles et lion).

Le rendu

pygame & POO

Notez que les images ont désormais un fond transparent.

Étape 3 : "savane_cruelle3.py"

Le déplacement des proies est trop prévisible et est seulement horizontal.
Il faut gérer les collisions : si le prédateur chevauche une proie celle-ci doit disparaitre.
Voyons les modifications par rapport à la version précédente.

Classe Proies()

Il faut modifier le code de la méthode update().

...
 self.deltax = 10
 self.deltay = random.randint(5,15)
 def update(self):
    # gestion rebond du troupeau
        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 des proies
        self.rect.x +=self.deltax
        self.rect.y +=self.deltay

Rajout d'un déplacement vertical des gazelles avec un delta variable : self.deltay = random.randint(5,15)
N'oubliez pas d'importer le module random !

Gestion des collisions

# gestion collision		
    if pygame.sprite.spritecollide(predateur,troupeau,True):
        print("une gazelle dévorée")
        
    if len(troupeau) ==0 :
        print("Gagné ; toutes les gazelles dévorées ! ")
        encore = False

C'est ici qu'on apprécie Pygame et plus précisément le module pygame.sprite
En effet ce module propose une fonction très puissante : spritecollide(instance,groupe,booléen)

L'instruction if pygame.sprite.spritecollide(predateur,troupeau,True) signifie que si le prédateur chevauche un élément du troupeau (donc une gazelle), ce dernier est supprimé (car le troisième argument est à True).
if len(troupeau) ==0 : si le groupe "troupeau" est vide.

Calculer et afficher la durée du massacre

...
debut_jeu = pygame.time.get_ticks()
encore = True
# début boucle de jeu
while encore :
...
# fin boucle de jeu
fin_jeu = pygame.time.get_ticks()
duree = (fin_jeu - debut_jeu) //1000
time.sleep(5) # pause pour lire le score
print(f" temps pour tuer toutes les gazelles : {duree} secondes")
print("Tâchez de faire mieux la prochaine fois !")
pygame.quit()

On calcule le temps écoulé juste avant l'entrée dans la boucle de jeu. Puis le temps passé au sortir de cette boucle. Et on fait la différence dans duree.

Notez la pause de 5 s avant que le programme se termine.
Plutôt que time.sleep() j'aurais pu utiliser pygame.wait().

Un programme mais plusieurs fichiers

Comme vous devez le constater "savane_cruelle3.py" c'est déjà un nombre conséquent de lignes de code , plus qu'un écran de PC puisse en afficher. Pour faciliter la maintenance logicielle il est préférable d'éclater un lourd programme en plusieurs scripts.
Ici je vais créer deux fichiers d'extension .py ! Il faut bien sûr que ces deux fichiers soient dans le même dossier.

Le fichier "savane_constantes.py"

Dans ce fichier on retrouve les constantes et les définitions de classe.

Le fichier "savane_principal.py"

Dans ce fichier on retrouve les traitements mais aussi une instruction qui fait le lien avec "savane_constantes.py".

Notez l'instruction
from savane_constantes import * . Ce qui signifie qu'il faut charger le fichier "savane_constantes.py".

Exécutez le programme

À partir de l'explorateur windows il suffit de double cliquer sur le fichier "savane_principal.py"

Observez le répertoire c:/pygame4_prog.
Il a été rajouté automatiquement un sous-dossier nommé "_pycache_" qui contient un fichier unique nommmé "savane_constantes.cpython39.pyc"

Le jeu casse briques

En début de chapitre je vous ai montré comment créer le mur ("mur_briques.py").

Le programme découpé en deux fichiers.

Le fichier "casse_briques_classes.py"

# casse_briques_classes.py
import pygame
import sys
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((80,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
mur = pygame.sprite.Group()
tous = pygame.sprite.Group()
raquettes =pygame.sprite.Group()
outs = pygame.sprite.Group()

# la raquette
raquette = Raquettes()
raquette.rect.center = (300, hauteur-20)
tous.add(raquette)
raquettes.add(raquette)

# la balle
balle = Balles()
balle.dessiner()
balle.rect.center =(300,300)
tous.add(balle)

# la zone out
out = Outs()
out.rect.topleft = (0, hauteur-5)
tous.add(out)
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)
       tous.add(brique)
       col +=60   # espacement entre deux briques
    col =0  # pour nouvelle rangée de briques
    lig += 20
# fin boucles imbriquées

Dans ce fichier je définis les classes, les groupes de sprites, je construis le mur de briques.

Observez attentivement la classe Balles qui est "costaud" !

La méthode update() gère le déplacement de la balle avec gestion du rebond.
La méthode dessiner() dessine un rond bleu dans l'objet rect (rempli de noir)/

Le fichier "casse_briques_jeu.py"

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

# nom prog : casse_briques_jeu.py
from casse_briques_classes import *              

sorties = 0
encore = True
# début 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(18)
# fin boucle de jeu
pygame.quit()

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

Gestion des collisions

À chaque fois deltay doit changer de signe ! : balle.deltay *= -1

Le rendu

module pygame.sprite

L'objectif du jeu est de détruire le mur de briques dans le délai le plus court.
Vous avez le droit de laisser sortir la balle de la fenêtre 20 fois mais pas plus sinon fin de partie !
Si vous trouvez que la balle va trop vite il suffit de réduire la FPS : frequence.tick(?)

Améliorer le jeu ?

En fonction de vos connaissances vous êtes capable d'améliorer le jeu.

En effet dans une version distribuable du jeu on ne peut imaginer des affichages dans le terminal windows ... Les consignes de jeu, le score doivent apparaitre dans la fenêtre de jeu. Cette codification ne pose pas de problème particulier : affichage des consignes avant démarrage effectif du jeu.

Récupération des images du chapitre 29

Les images de ce chapitre dans un zip