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 - vers des jeux vidéos plus complexes - drag & drop

Dans le chapitre précédent je vous ai montré l'intérêt de la POO en matière de jeux vidéos : pouvoir manipuler plusieurs dizaines de sprites avec un code succinct.

L'objectif de ce cinquième chapitre sur Pygame n'est pas de vous proposer des jeux vidéos plus complexes mais de vous donner des outils pour les réaliser. Donc il est composé d'une suite d'astuces pour programmer efficacement en Pygame. Ainsi vous verrez l'intérêt d'utiliser un fichier texte pour construire le décor d'un jeu.
En fin de chapitre je vous montre comment réaliser un "drag and drop" dans le cadre de Python-Pygame.

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

Charger, redimensionner voir convertir les images PNG

Charger et redimensionner les images

Découper la fenêtre de jeu en damier

case_taille = 50
case_nombre = 8
largeur  = case_taille * case_nombre
hauteur = case_taille * case_nombre

Pour de nombreux jeux il est pratique de découper la fenêtre de jeu en cases (souvent des carrés)
Ici la case est un carré de 50 de côté ; le nombre de cases est de 8 tant en largeur qu'en hauteur. Donc hauteur = 50 * 8 = 400 et largeur = 50 * 8 = 400 également.
Idéalement un sprite représenté par une image doit s'inscrire dans une case et une seule. Or souvent les PNG font d'origine plus de 200 de hauteur et de largeur ...

Autres nouveautés dans le code

cavalier_noir2 =pygame.transform.scale(pygame.image.load('cavalier_noir.png'),  \
(case_taille,case_taille))
cavalier_blanc2 =pygame.transform.scale(pygame.image.load('cavalier_blanc.png'), \
(case_taille,case_taille))

Dans chacune des deux commandes il y imbrication de pygame.image.load() dans pygame.transform.scale() donc il y a chargement de l'image réduction de sa taille.

Le rendu

pygame chargement d'image avec réduction

Les deux PNG apparaissent tout d'abord avec leur taille d'origine puis en réduction

Auparavant j'avais pris la précaution de convertir le fond blanc en fond transparent via l'application en ligne : https://www.remove.bg/fr

Convertir le fond blanc en fond transparent dans le script

La conversion du fond blanc en fond transparent des images PNG peut se réaliser dans le programme !
Donc plus besoin de recourir aux services de https://www.remove.bg/fr.

La nouveauté

surface3 = pygame.Surface((case_taille , case_taille))
surface3 =pygame.transform.scale(pygame.image.load('reine_noire.png'). \ 
 convert_alpha(), (case_taille,case_taille))
surface3.set_colorkey(blanc)

Attention la méthode set_colorkey(couleur) ne s'applique qu'à une surface d'où l'instruction surface3 = pygame.Surface((case_taille , case_taille)) .
Ensuite je charge dans la surface, l'image avec réduction.
Et enfin je demande la conversion du fond blanc en fond transparent pour la surface en question.

Le rendu

pygame - méthode set_colorkey()

Construire un damier (jeu de dames)

Un fichier texte va servir de "patron".

Le contenu du fichier texte nommé "damier.txt"

CFCFCFCFCF
FCFCFCFCFC
CFCFCFCFCF
FCFCFCFCFC
CFCFCFCFCF
FCFCFCFCFC
CFCFCFCFCF
FCFCFCFCFC
CFCFCFCFCF
FCFCFCFCFC

Dans ce fichier deux lettres majuscules uniquement. "F" comme foncé et "C" comme clair.
Ce fichier comprend dix lignes et chaque ligne comprend dix caractères.
En effet selon les règles internationales, le damier doit comprendre 100 cases et la case en bas à gauche doit être foncée.

Le script "damier_brut.py"

Ce programme dessine un damier de 10 par 10.

Étude approfondie de ce script

Le damier fait partie du décor du jeu de dames. Il suffit de créer des objets de type rect avec une couleur de fond différente (beige ou chocolate) en fonction du caractère lu dans le fichier "damier.txt".

Construction du damier

x=0
y =0
with open("damier.txt", "r") as fichier:
  for ligne in fichier:
    for car in ligne:
        case_damier =pygame.Rect(x,y, case_taille,case_taille)
        if car == 'C':
           pygame.draw.rect(fenetre, "beige", case_damier)
        if car =="F" : 
            pygame.draw.rect(fenetre, "chocolate", case_damier)
        x+=case_taille
    x= 0
    y+=case_taille

Pour lire un fichier texte caractère par caractère il faut deux boucles imbriquées : la boucle externe pour lire chaque ligne et la boucle interne pour lire chaque lettre de la ligne.

Si le caractère lu est "F" il faut remplir l'objet rect de la couleur 'chocolate' et si le caractère lu est "C" il faut remplir l'objet rect de la couleur 'beige'.
y et x sont incrémentés de "case_taille" c'est à dire de 50.
Il faut réinitialiser x à zéro en debut de chaque ligne.

Le rendu

pygame - réalisation d'un damier

Réaliser un labyrinthe

Là aussi il faut utiliser en guise de "patron" un fichier texte.

Le fichier "lab1.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  

Ce fichier comprend 25 lignes et chaque ligne comprend 25 caractères ("B" ou " "). Il y a deux autres fichiers de même nature : "lab2.txt " & "lab3.txt".

Le script

Analyse détaillé du programme

Toujours le principe de case ou "tuiles" ; ici 25 par 25 et une case fait 30 de côté donc les dimensions du labyrinthe proprement dit sont de 750 par 750.
On rajoute une marge de 20 à droite et en bas à ces dimensions utiles.

Création de deux classes : une à partir d'une surface (Briques) et l'autre à partir d'une image PNG (Personnages).t
L'image n'a pas besoin d'être réduite ; elle l'avait via Paint.

Concernant la création du labyrinthe, c'est le même principe que dans le chapitre précédent ; le fichier texte sert de "patron". Cependant le modèle est déterminé de façon aléatoire parmi 3 d'où l'importation nécessaire du module random.

import random 
...
liste = ["lab1.txt", "lab2.txt", "lab3.txt"]
modele = random.choice(liste)
...
with open(modele, "r") as fichier:
...

Le rendu

pygame : construction d'un labyrinthe

Le personnage est positionné en début de labyrinthe.
On ne peut encore le déplacer puisque les touches de déplacement ne sont pas encore programmées.

Le jeu de dames - suite

Il faut désormais compléter le décor : poser les pions sur les cases foncées (et seulement sur les cases foncées) ...

Le programme

Faites une copie de "damier_brut.py" , nommez cette copie "damier&pions.py" et insérer les blocs de code précisés ci-dessous.
Cette fois je crée des classes qui héritent bien sûr de la classe pygame.sprite.Sprite. Les pions doivent en effet être des sprites puisqu'ils vont être manipulés (déplacement et prise).
Tous ce code sera inséré avant la boucle de jeu.

# damier&pions.py"
...
class Noirs(pygame.sprite.Sprite):
    def __init__(self) :
        super().__init__()
        self.image = pygame.Surface((50,50))
        self.image.fill("chocolate")
        self.rect = self.image.get_rect()
    def dessiner(self):
        pygame.draw.circle(self.image,"black",(25, 25), 25)

class Blancs(pygame.sprite.Sprite):
    def __init__(self) :
        super().__init__()
        self.image = pygame.Surface((50,50))
        self.image.fill("chocolate")
        self.rect = self.image.get_rect()
    def dessiner(self):
        pygame.draw.circle(self.image,"white",(25, 25), 25)

Dans chaque surface de couleur "chocolate" je dessine un rond de couleur "black" ou "white".

Les groupes de sprites

Il faut créer deux groupes de sprites :

pions_noirs = pygame.sprite.Group()
pions_blancs = pygame.sprite.Group()

Définir les pions noirs

Ils sont placés en haut du damier.

Il faut deux boucles imbriquées.

Y = 0
for ligne in range(1,5) :
    if ligne in (1,3) :
        X = case_taille
    if ligne in (2,4) :
        X = 0
    for nombre in range(1,6) :
        pion_noir = Noirs()
        pion_noir.dessiner()
        pion_noir.rect.x = X
        pion_noir.rect.y = Y
        pions_noirs.add(pion_noir)
      
        X +=case_taille *2
    Y +=case_taille

Il y a 4 rangées de pions : for ligne in range(1,5)
Les pions disposés seulement sur les cases foncées.
Toujours 5 pions par rangée : for nombre in range(1,6)
Pour les première et troisième lignes la première case est claire : if ligne in (1,3) : X = case_taille
Pour les deuxième et quatrième lignes la première case est foncée : if ligne in (2,4) : X =0

Attention à la fonction range(a,b) qui permet de créer une séquence d'entiers. a est inclus mais b est exclu.
Par exemple range(1, 6) désigne la séquence d'entiers 1, 2, 3, 4, 5 (1 inclus mais 6 exclu)

Définir les pions blancs

Ils sont placés en bas du damier.
Il faut utiliser la même solution que pour les pions noirs (deux boucles imbriquées).

	# définir les pions blancs
	Y = case_taille *6
	for ligne in range(1,5) :
		...

Dessiner les pions

Toujours avant la boucle de jeu, rajoutez :

pions_noirs.draw(fenetre)
pions_blancs.draw(fenetre)

Le rendu

pygame : construction d'un labyrinthe

Le labyrinthe - suite

Revenons sur ce thème !
Le personnage doit se déplacer dans les couloirs du labyrinthe mais il n'a pas de pouvoirs surnaturels ; il ne doit pas traverser les murs et il ne peut pas casser les murs. Il faut donc gérer les collisions éventuelles entre le groupe "mur" et l'instance "personnage". Mais cette fois et à la différence du "casse-briques" il n'y a jamais de "kill" à effectuer.

Le code

Faites une copie de "labyrinthe_decor.py" et nommez cette copie "labyrinthe_jeu.py".
Rajoutez dans le nouveau script les blocs de code qui suivent (dans la boucle de jeu).

Déplacement du personnage

# deplacement du bonhomme 
    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'

Aucune difficulté. Remarque que je sauvegarde dans la variable derniere_touche le code du dernier déplacement : L pour à gauche (left), R pour à droite (right), U pour en haut (up) et D pour en bas(down).

Gestion des collisions

Si le personnage tente de pénétrer dans un mur, il doit rebondir !

 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

if pygame.sprite.spritecollide(personnage,mur,False) : pas de 'kill' d'instance de mur donc le troisième argument est False.
En cas de collision entre le personnage et le groupe "mur", le dernier déplacement du personnage doit être annulé.
Donc si par exemple, derniere_touche vaut L (déplacement à gauche) il faut annuler par un déplacement à droite : personnage.rect.x +=delta

Gestion de l'arrivée

Bloc dans la boucle de jeu :

# gestion arrivée à la dernière case 
    if (personnage.rect.y >= hauteur_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)
        encore = False

Mais n'oubliez pas avant la boucle de jeu :

debut_jeu = pygame.time.get_ticks()

Le rendu

pygame - jeu de labyrinthe

À chaque tentative le labyrinthe proposé est différent.
Vous pouvez aussi modifier les fichiers texte pour rendre le dédale plus complexe.
Vous pouvez jouer aussi sur le nombre de cases ; passer par exemple à 30 * 30. Pensez alors à réduire un peu le côté de chaque case.

Déplacer une balle avec la souris

Il peut être utile de déplacer un sprite non pas via les touches de direction mais via la souris.

Principes du déplacement de la souris

Dans un chapitre précédent et concernant la gestion de la souris nous avons vu les événements MOUSEBUTTONUP et MOUSEBUTTONDOWN.
Je rappelle que le premier est appelé dès qu’on appuie sur un bouton et le deuxième quand on relâche un bouton.
L'attribut button permet de préciser sur quel bouton l'appui (ou le relachement a été effectué. Un appui sur le bouton gauche de la souris retourne 1.
Dans ce paragraphe je vais utilisé un troisième événement : MOUSEMOTION qui se produit lorsqu'on déplace la souris (et uniquement lors de son déplacement).

Le script

Dans ce script je peux déplacer la balle et par ailleurs je vous indique deux solutions pour obtenir la position du pointeur de souris dans la fenêtre : event.pos() OU pygame.mouse.get()
Dans les deux cas l'info est affichée sous forme d'un tuple de deux valeurs.

Le rendu

pygame - déplacer un sprite avec la souris

"Drag and drop" ou "glisser-déposer"

Dans une chapitre précédent je vous ai montré comment créer à la volée des objets de type rect.
Voir le script "rect_a_la_volee.py" dans "pygame3_prog".
Cette fois nous allons voir comment déplacer ces objets un par un avec la souris.

Le code

Ajouts par rapport à "rect_a_la_volee.py"

Ajout avant la boucle de jeu

Dans la boucle de jeu

Dans la boucle de jeu il faut rajouter le code pour le "glisser-déposer".

		if event.type == MOUSEBUTTONDOWN and event.button == 1 :
              for num, boite in enumerate(boites) :
                  if boite.collidepoint(event.pos) :
                      boite_active = num
        if event.type == MOUSEMOTION :
            if boite_active != None :
                boites[boite_active].move_ip(event.rel)
        if event.type   == MOUSEBUTTONUP and event.button == 1 :
            boite_active = None

Étude de ce code compliqué ...

Ce code doit être inséré dans la boucle for event in pygame.event.get().
Notez que nous utilisons tous les évènements de la souris. Par ailleurs nous n'avons pas besoin de préfixer les constantes avec "pygame." car le programme comporte l'instruction from pygame.locals import *

Dans ce code il y a une boucle bizarre : for num, boite in enumerate(boites)
Cette structure permet de parcourir une structure itérable mais de récupérer non seulement l'élément de la liste mais aussi son index.

Dans cette boucle il y a un test :
if boite.collidepoint(event.pos) : teste si le pointeur de souris est dans un des rectangles.
Donc la variable boite_active récupère l'index de la boite survolée par la souris :boite_active = num
boites[boite_active].move_ip(event.rel) : déplacement du rectangle sélectionné par la souris
boite_active = None : sur relachement du bouton gauche de la souris on remet à None la variable ; il n'y a plus de boite sélectionnée ; vous pouvez en sélectionner une autre pour la déplacer à son tour.

Le rendu

Situation au départ

pygame - drag and drop

Les rectangles sont éparpillés suite à leur génération aléatoire.

Situation à l'arrivée

pygame - drag and drop

Grâce à des "glisser-déposer" successifs j'ai regroupé les rectangles en un grand carré.

Récupération des images

Images PNG utilisées dans les scripts