oujood.com

PyGame : Initiation à la programmation de jeux en Python 2ÉME PARTIE

Tutoriel, Dans cette seconde partie nous allons entamer la finalisation du jeu en ajoutant le contrôle du joueur à l'aide du clavier, dessinant les ennemies et finalisant le code du jeu

Rappel :

Nous rappelons qu'à la fin de la première partie de ce tutoriel nous avons obtenu le code suivant:

Exemple :     📋 Copier le code

# Importer le module pygame
import pygame

# Importer pygame.locals pour faciliter l'accès aux coordonnées clés
# Mise à jour pour se conformer aux normes flake8 et black
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Définir des constantes pour la largeur et la hauteur de l'écran
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 300

# Définir un objet joueur en étendant pygame.sprite.Sprite
# La surface dessinée sur l'écran est maintenant un attribut de 'joueur'
class Player(pygame.sprite.Sprite) :
    def __init__(self) :
        super(Player, self).__init__()
        self.surf = pygame.Surface((75, 25))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect()

# Initialiser pygame
pygame.init()

# Créer l'objet écran
# La taille est déterminée par les constantes SCREEN_WIDTH et SCREEN_HEIGHT
ecran = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

# Instanciation du joueur. Pour l'instant, c'est juste un rectangle.
joueur = Player()

# Variable pour maintenir la boucle principale en cours d'exécution
running = True

# Boucle principale
while running :
    # Boucle de recherche dans la file d'attente des événements
    for event in pygame.event.get() :
        # Vérifier la présence d'un événement KEYDOWN
        if event.type == KEYDOWN :
            # Était-ce la touche Escape ? Si c'est le cas, arrêter la boucle.
            if event.key == K_ESCAPE :
                running = False
        # Vérifier s'il y a un événement QUIT. Si c'est le cas, mettre running à false.
        elif event.type == QUIT :
            running = False
    
    # Remplir l'écran de noir
    ecran.fill((0, 0, 0))

    # Dessine le joueur sur l'écran
    ecran.blit(joueur.surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))

    # Mettre à jour l'affichage
    pygame.display.flip()

pygame.quit()

Entrée utilisateur

Jusqu'à présent dans la 1ére partie, nous avons appris à configurer pygame et à dessiner des objets sur l'écran. Maintenant, les choses sérieuses commencent ! Nous allons faire en sorte que le joueur puisse être contrôlé à l'aide du clavier.

Dans les cours sur pygame, nous avons vu que pygame.event.get() renvoie une liste d'événements dans la file d'attente des événements, que nous recherchons pour les types d'événements KEYDOWN. Eh bien, ce n'est pas la seule façon de lire les pressions de touches. pygame fournit également pygame.event.get_pressed(), qui renvoie un dictionnaire contenant tous les événements KEYDOWN actuels dans la file d'attente.

Plaçons cette fonction dans notre boucle de jeu, juste après la boucle de gestion des événements. Ceci renvoie un dictionnaire contenant les touches pressées au début de chaque image.

Voici le bout du code à insérer dans la boucle principale à partir de la ligne 54:

# Obtenir l'ensemble des touches pressées et vérifier l'entrée de l'utilisateur
pressed_keys = pygame.key.get_pressed()

Ensuite, nous écrivons une méthode dans Player pour accepter ce dictionnaire. Cela définira le comportement du sprite en fonction des touches pressées. En voici un exemple, insérer le code suivant avant
# Initialiser pygame
pygame.init() à partir de la ligne 29 :

# Déplace le sprite en fonction des touches pressées par l'utilisateur
def update(self, pressed_keys):
    if pressed_keys[K_UP]:
        self.rect.move_ip(0, -5)
    if pressed_keys[K_DOWN]:
        self.rect.move_ip(0, 5)
    if pressed_keys[K_LEFT]:
        self.rect.move_ip(-5, 0)
    if pressed_keys[K_RIGHT]:
        self.rect.move_ip(5, 0)

K_UP, K_DOWN, K_LEFT et K_RIGHT correspondent aux touches fléchées du clavier. Si l'entrée du dictionnaire pour cette touche est True, alors elle est enfoncée et nous déplaçons le .rect du joueur dans la bonne direction. Ici, nous utilisons .move_ip(), qui signifie déplacer sur place, pour déplacer le Rect actuel.

Nous pouvons ensuite appeler .update() à chaque image pour déplacer le sprite du joueur en fonction de la pression des touches. Ajouter l'appel à cette fonction juste après l'appel à la fonction .get_pressed() :

Ce qui donne le code complet comme suit :

Exemple :     📋 Copier le code

# Importation du module pygame
import pygame

# Importer pygame.locals pour un accès plus facile aux coordonnées des touches
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Définir des constantes pour la largeur et la hauteur de l'écran
LARGEUR_ECRAN = 800
HAUTEUR_ECRAN = 600

# Définir un objet joueur en étendant pygame.sprite.Sprite
class Joueur(pygame.sprite.Sprite):
    def __init__(self):
        super(Joueur, self).__init__()
        self.surf = pygame.Surface((75, 25))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect()

    # Déplacer le sprite en fonction des touches utilisées par l'utilisateur
    def update(self, touches_appuyees):
        if touches_appuyees[K_UP]:
            self.rect.move_ip(0, -5)
        if touches_appuyees[K_DOWN]:
            self.rect.move_ip(0, 5)
        if touches_appuyees[K_LEFT]:
            self.rect.move_ip(-5, 0)
        if touches_appuyees[K_RIGHT]:
            self.rect.move_ip(5, 0)

# Initialiser pygame
pygame.init()

# Créer l'objet écran
ecran = pygame.display.set_mode((LARGEUR_ECRAN, HAUTEUR_ECRAN))

# Instancier le joueur. Pour l'instant, il s'agit simplement d'un rectangle.
joueur = Joueur()

# Variable pour maintenir la boucle principale en cours d'exécution
en_marche = True
# Boucle principale
while en_marche:
    # Boucle à travers la file d'événements
    for evenement in pygame.event.get():
        # Vérifier l'événement KEYDOWN
        if evenement.type == KEYDOWN:
            # Si la touche Échap est pressée, quitter la boucle principale
            if evenement.key == K_ESCAPE:
                en_marche = False
        # Vérifier l'événement QUIT. S'il est présent, définir en_marche sur False.
        elif evenement.type == QUIT:
            en_marche = False

    # Obtenir toutes les touches actuellement enfoncées
    touches_appuyees = pygame.key.get_pressed()

    # Mettre à jour le sprite du joueur en fonction des touches appuyées par l'utilisateur
    joueur.update(touches_appuyees)

    # Remplir l'écran en noir
    ecran.fill((0, 0, 0))

    # Dessiner le joueur à l'écran
    ecran.blit(joueur.surf, joueur.rect)

    # Mettre à jour l'affichage
    pygame.display.flip()

# Quitter pygame correctement
pygame.quit()

Vous pouvez maintenant utiliser les touches fléchées pour déplacer le rectangle du joueur sur l'écran.

Nous remarquons deux petits problèmes :

  • 1. Le rectangle du joueur peut se déplacer très rapidement lorsqu'une touche est maintenue enfoncée. Nous y travaillerons plus tard.
  • Le rectangle du joueur peut sortir de l'écran. Nous allons y remédier tout de suite.

Pour que le joueur reste à l'écran, il faut ajouter une logique permettant de détecter si le rectangle se déplace hors de l'écran. Pour ce faire, nous vérifions si les coordonnées du rectangle se sont déplacées au-delà de la limite de l'écran. Si c'est le cas, nous demandons au programme de le ramener au bord de l'écran :

Voici le code :

Exemple :     📋 Copier le code

    # Déplacer le sprite en fonction des touches utilisées par l'utilisateur
    def update(self, touches_appuyees):
        if touches_appuyees[K_UP]:
            self.rect.move_ip(0, -5)
        if touches_appuyees[K_DOWN]:
            self.rect.move_ip(0, 5)
        if touches_appuyees[K_LEFT]:
            self.rect.move_ip(-5, 0)
        if touches_appuyees[K_RIGHT]:
            self.rect.move_ip(5, 0)
            
       # Garder le joueur à l’intérieur de l'écran
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.right > LARGEUR_ECRAN:
            self.rect.right = LARGEUR_ECRAN
        if self.rect.top <= 0:
            self.rect.top = 0
        if self.rect.bottom >= HAUTEUR_ECRAN:
            self.rect.bottom = HAUTEUR_ECRAN

Ce morceau de code est responsable de la gestion des limites de l'écran pour le déplacement du joueur afin de s'assurer qu'il reste entièrement visible dans la fenêtre de jeu. Voici une explication ligne par ligne :

# Garder le joueur à l’intérieur de l'écran
if self.rect.left < 0:
    self.rect.left = 0

Cette ligne vérifie si le côté gauche du joueur (la coordonnée x la plus à gauche de son rectangle) est en dehors de la zone de jeu (c'est-à-dire, si sa position est plus petite que 0, donc hors de l'écran à gauche).
Si c'est le cas, cela déplace le côté gauche du joueur vers la position x = 0, ce qui le ramène à l'intérieur de l'écran.

if self.rect.right > LARGEUR_ECRAN:
    self.rect.right = LARGEUR_ECRAN

Cette ligne vérifie si le côté droit du joueur (la coordonnée x la plus à droite de son rectangle) dépasse la largeur de l'écran (c'est-à-dire, si sa position est supérieure à la largeur de l'écran).
Si c'est le cas, cela ajuste le côté droit du joueur pour qu'il soit égal à la largeur de l'écran, le ramenant ainsi à l'intérieur de la zone de jeu.

if self.rect.top <= 0:
    self.rect.top = 0

Cette ligne vérifie si le haut du joueur (la coordonnée y la plus haute de son rectangle) est au-dessus de la zone de jeu (c'est-à-dire, si sa position est inférieure ou égale à 0, donc hors de l'écran en haut).
Si c'est le cas, cela ajuste la position du haut du joueur à y = 0, le ramenant à l'intérieur de la zone de jeu.

if self.rect.bottom >= HAUTEUR_ECRAN:
    self.rect.bottom = HAUTEUR_ECRAN

Cette ligne vérifie si le bas du joueur (la coordonnée y la plus basse de son rectangle) dépasse la hauteur de l'écran (c'est-à-dire, si sa position est supérieure ou égale à la hauteur de l'écran). Si c'est le cas, cela ajuste la position du bas du joueur à la hauteur de l'écran, le ramenant ainsi à l'intérieur de la zone de jeu.
En résumé, ces lignes de code garantissent que le joueur ne puisse pas sortir des limites de l'écran en ajustant sa position s'il dépasse les bords de la fenêtre de jeu.

Maintenant, ajoutons des ennemis !

Ajout d'ennemies

Que ferait un jeu sans ennemis ? Nous allons utiliser les techniques que nous avons déjà apprises et créer une classe d'ennemis de base, puis en créer un grand nombre pour que le joueur tente de les éviter. Tout d'abord, importez la bibliothèque random :

Exemple :     📋 Copier le code

# Importation de la bibliothèque random
import random

Puis nous créons une nouvelle classe de sprites appelée Enemy, en suivant le même modèle que celui utilisé pour Player :

Exemple :     📋 Copier le code

# Définir l'objet ennemi en étendant pygame.sprite.Sprite
# La surface que vous dessinez à l'écran est maintenant un attribut de 'ennemi'
class Ennemi(pygame.sprite.Sprite):
    def __init__(self):
        super(Ennemi, self).__init__()
        self.surf = pygame.Surface((20, 10))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect(
            center=(
                random.randint(LARGEUR_ECRAN + 20, LARGEUR_ECRAN + 100),
                random.randint(0, HAUTEUR_ECRAN),
            )
        )
        self.speed = random.randint(5, 20)

    # Déplacer le sprite en fonction de la speed
    # Supprimer le sprite lorsqu'il dépasse le bord gauche de l'écran
    def update(self):
        self.rect.move_ip(-self.speed, 0)
        if self.rect.right < 0:
            self.kill()

Il existe quatre grandes différences entre Enemy et Player :

  • 1. Sur les lignes :
    self.rect = self.surf.get_rect(
                center=(
                    random.randint(LARGEUR_ECRAN + 20, LARGEUR_ECRAN + 100),
                    random.randint(0, HAUTEUR_ECRAN),
                )
    
    nous mettons à jour rect pour qu'il soit un endroit aléatoire le long du bord droit de l'écran. Le centre du rectangle est juste en dehors de l'écran. Il est situé à une distance comprise entre 20 et 100 pixels du bord droit, et quelque part entre les bords supérieur et inférieur.
  • 2. À la ligne :

    self.vitesse = random.randint(5, 20)

    nous définissons .speed comme un nombre aléatoire compris entre 5 et 20. Ce nombre indique la vitesse à laquelle l'ennemi se déplace vers le joueur.
  • 3. Aux lignes :
        def update(self):
            self.rect.move_ip(-self.speed, 0)
    
    nous définissons .update(). Elle ne prend aucun argument puisque les ennemis se déplacent automatiquement. En revanche, .update() déplace l'ennemi vers la gauche de l'écran à la vitesse .speed définie lors de sa création.
  • Àux lignes :

    if self.rect.right < 0: self.kill()

    nous vérifions si l'ennemi s'est déplacé hors de l'écran. Pour s'assurer que l'ennemi est complètement hors de l'écran et qu'il ne disparaîtra pas simplement alors qu'il est encore visible, nous vérifions que le côté droit du .rect a dépassé le côté gauche de l'écran. Une fois que l'ennemi est hors de l'écran, vous appelez .kill() pour l'empêcher d'être traité ultérieurement.

Mais que fait donc .kill() ? Pour le savoir, vous devez connaître les groupes de sprites.

Les groupes de sprites

Une autre classe super utile fournie par pygame est le Sprite Group. C'est un objet qui contient un groupe d'objets Sprite. Alors pourquoi l'utiliser ? Ne pouvons-nous pas simplement suivre nos objets Sprite dans une liste à la place ? Eh bien, nous le pouvons, mais l'avantage d'utiliser un groupe réside dans les méthodes qu'il contient. Ces méthodes permettent de détecter si un ennemi est entré en collision avec le joueur, ce qui facilite grandement les mises à jour.

Voyons comment créer des groupes de sprites. Nous allons créer deux objets Group différents :

Le premier groupe contiendra tous les sprites du jeu.
Le second ne contiendra que les objets Enemy.

Voici à quoi cela correspond dans le code :

Exemple :     📋 Copier le code

 ...
 ...
 ...
 # Instancier le joueur. Pour l'instant, il s'agit simplement d'un rectangle.
joueur = Joueur()

# Créer des groupes pour contenir les sprites des ennemis et tous les sprites
# - 'ennemis' est utilisé pour la détection de collision et les mises à jour de position
# - 'all_sprites' est utilisé pour le rendu
ennemis = pygame.sprite.Group()
tous_sprites = pygame.sprite.Group()
tous_sprites.add(joueur)

# Variable pour maintenir la boucle principale en cours d'exécution
en_marche = True
...
...
...
 

Voici l'explication du code :
1. `ennemis = pygame.sprite.Group()` : Cette ligne crée un groupe de sprites nommé `ennemis`. Les groupes de sprites dans Pygame sont des collections qui contiennent et gèrent plusieurs sprites. Dans ce cas, nous créons un groupe spécifique pour les sprites ennemis.
2. `all_sprites = pygame.sprite.Group()` : Cette ligne crée un autre groupe de sprites nommé `all_sprites`. Ce groupe est destiné à contenir tous les sprites du jeu, y compris le joueur et les ennemis.
3. `all_sprites.add(joueur)` : Ici, nous ajoutons le sprite `joueur` (qui représente le joueur) au groupe `all_sprites`. Le sprite `joueur` fait ainsi partie de la collection gérée par `tous_sprites`.

Lorsque nous appelons .kill(), le Sprite est supprimé de tous les groupes auxquels il appartient. Les références au Sprite sont également supprimées, ce qui permet au gestionnaire de Python de récupérer la mémoire si nécessaire.
Maintenant que nous avons un groupe all_sprites, nous pouvons modifier la façon dont les objets sont dessinés. Au lieu d'appeler .blit() sur le seul joueur, nous pouvons itérer sur tout ce qui se trouve dans all_sprites :

Exemple :     📋 Copier le code

    # Remplir l'écran en noir
    ecran.fill((0, 0, 0))

    # Dessiner tous les sprites à l'écran
	for entity in all_sprites:
    ecran.blit(entity.surf, entity.rect)

    # Mettre à jour l'affichage
    pygame.display.flip()

Maintenant, tout ce qui est placé dans all_sprites sera dessiné à chaque frame, qu'il s'agisse d'un ennemi ou du joueur.
Mais il y a un problème... Nous n'avons pas d'ennemis ! Nous pourrions créer un tas d'ennemis au début du jeu, mais le jeu deviendrait rapidement ennuyeux lorsqu'ils quitteraient tous l'écran quelques secondes plus tard. Voyons plutôt comment assurer un approvisionnement régulier en ennemis au fur et à mesure que le jeu progresse.

Événements personnalisés

La conception exige que les ennemis apparaissent à intervalles réguliers. Cela signifie que nous devrons faire deux choses à certains intervalles :

  • 1. Créer un nouvel ennemi.
  • 2. Ajoutez-le à all_sprites et enemies.

Nous disposons déjà d'un code permettant de gérer les événements aléatoires. La boucle d'événements est conçue pour rechercher les événements aléatoires qui se produisent à chaque image et les traiter de manière appropriée. Heureusement, pygame ne nous limite pas à utiliser les types d'événements qu'il définit. Vous pouvez définir vos propres événements et les gérer comme vous le souhaitez.

Voyons comment créer un événement personnalisé qui sera généré toutes les quelques secondes. Nous pouvons créer un événement personnalisé en lui donnant un nom :

Exemple :     📋 Copier le code

.....

# Créer l'objet écran
ecran = pygame.display.set_mode((LARGEUR_ECRAN, HAUTEUR_ECRAN))

# Créer un événement personnalisé pour l'ajout d'un nouvel ennemi
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)

# Instancier le joueur. Pour l'instant, il s'agit simplement d'un rectangle.
joueur = Joueur()
....

pygame définit les événements en interne comme des entiers, vous devez donc définir un nouvel événement avec un entier unique. Comme le dernier événement réservé par pygame s'appelle USEREVENT, la définition de ADDENEMY = pygame.USEREVENT + 1 à la ligne 83 permet de s'assurer qu'il est unique.

Ensuite, nous devons ajouter ce nouvel événement à la file d'attente des événements à intervalles réguliers tout au long du jeu. C'est là que le module de temps entre en jeu.
On déclenche le nouvel événement ADDENEMY toutes les 250 millisecondes, soit quatre fois par seconde. Nous appelons .set_timer() en dehors de la boucle du jeu parce que nous n'avons besoin que d'un seul timer, mais il se déclenchera tout au long du jeu.

Exemple :     📋 Copier le code

# Boucle principale
while en_marche:
    # Boucle à travers la file d'événements
    for evenement in pygame.event.get():
        # Vérifier l'événement KEYDOWN
        if evenement.type == KEYDOWN:
            # Si la touche Échap est pressée, quitter la boucle principale
            if evenement.key == K_ESCAPE:
                en_marche = False
        # Vérifier l'événement QUIT. S'il est présent, définir en_marche sur False.
        elif evenement.type == QUIT:
            en_marche = False

        # Ajout d'un nouveau ennemi
        elif evenement.type == ADDENEMY:
            # Create the new enemy and add it to sprite groups
            new_enemy = Ennemi()
            ennemis.add(new_enemy)
            all_sprites.add(new_enemy)

    # Obtenir toutes les touches actuellement enfoncées
    touches_appuyees = pygame.key.get_pressed()

    # Mettre à jour le sprite du joueur en fonction des touches appuyées par l'utilisateur
    joueur.update(touches_appuyees)
    
    # Mise à jour de la position de ennemis
    ennemis.update()

Chaque fois que le gestionnaire d'événement voit le nouvel événement ADDENEMY, il crée un Ennemi et l'ajoute aux groupes ennemis et all_sprites. Puisque Ennemi est dans all_sprites, il sera dessiné à chaque frame. Nous devons également appeler ennemis.update(), qui met à jour tout ce qui se trouve dans le groupe ennemis, afin de s'assurer qu'ils se déplacent correctement :
Cependant, ce n'est pas la seule raison pour laquelle nous avons créé un groupe spécifique pour les ennemis. Et c'est ce que nous allons voir dans ce qui suit

La détection des collisions

Nous nous attendons à ce que le jeu s'arrête dès qu'un ennemi entre en collision avec le joueur. La vérification des collisions est une technique de base dans la programmation de jeux, et nécessite généralement des mathématiques non triviales pour déterminer si deux sprites vont se chevaucher.

C'est là qu'un framework comme pygame s'avère utile ! Écrire du code de détection de collision est fastidieux, mais pygame dispose de beaucoup de méthodes de détection de collision que nous pouvons utiliser.

Pour ce tutoriel, nous utiliserons une méthode appelée .spritecollideany(), qui peut être lue comme "sprite collide any". Cette méthode prend un sprite et un groupe comme paramètres. Elle examine chaque objet du groupe et vérifie si son .rect croise le .rect d'un sprite. Si c'est le cas, elle renvoie True. Sinon, il renvoie False. C'est parfait pour ce jeu, car nous devons vérifier si le joueur unique entre en collision avec l'un des groupes d'ennemis.

Voici le code :

Exemple :     📋 Copier le code

# Dessiner tous les sprites
    for entity in all_sprites:
        ecran.blit(entity.surf, entity.rect)

    # Vérifier si des ennemis ont collisionné avec le joueur
    if pygame.sprite.spritecollideany(joueur, ennemis):
    # Si c'est le cas, supprimer le joueur et arrêter la boucle
        joueur.kill()
        en_marche = False

Explication du code

  • 1. La boucle for entity in all_sprites: parcourt tous les sprites contenus dans le groupe all_sprites et dessine chaque sprite à l'emplacement spécifié par entity.rect sur l'écran à l'aide de ecran.blit(entity.surf, entity.rect). Cela suppose que chaque sprite a des attributs surf pour sa surface d'affichage et rect pour sa position/zone sur l'écran.
  • 2. Ensuite, la condition if pygame.sprite.spritecollideany(joueur, ennemis): vérifie s'il y a eu une collision entre le joueur (joueur) et l'un des sprites ennemies (ennemis). La méthode spritecollideany de Pygame est utilisée pour détecter toute collision entre les deux groupes de sprites.
  • Si une collision est détectée, cela entraîne l'exécution du code à l'intérieur du bloc if. À l'intérieur, joueur.kill() est appelé pour supprimer le sprite du joueur (joueur) du groupe all_sprites. Ensuite, la variable en_marche est définie sur False, ce qui peut être utilisé pour arrêter la boucle principale du jeu (en_marche = False).

Maintenant, habillons-le un peu, rendons-le plus jouable et lui ajoutons quelques fonctionnalités avancées pour qu'il se démarque.

Télécharger le code final de cette partie

Pour continuer voir la suite dans la 3ème partie


Voir aussi nos tutoriel :

list-style-image

Spécifie une image comme marqueur list-item

fonction strstr, strstr

Trouve la première occurrence dans une chaîne

fonction md5, md5

Calcule le md5 d'une chaîne