===== Attaque de zombies avec Pygame ====== {{ .:undead.png?direct&400 |}} Des mort vivants attaquent. Il faut les abattre. Difficile de faire plus simple... La figure ci-dessus schématise l'espace de jeu : * les morts-vivants, en jaune, apparaissent aléatoirement en haut et avancent vers le bas en ligne droite, * le joueur, en rouge en bas, se déplace horizontalement, commandé par le clavier, * le joueur tir des balles de fusil, carrés blancs, qui se déplacent vers le haut et percutent éventuellement des morts-vivants. Chacun de ses éléments est un //sprite//, c'est à dire un élément graphique mobile dans le jeu. Dans Pygame on définira une **classe** pour chacun d'eux, c'est à dire un modèle définissant leurs propriétés. Une fois que les propriétés d'une balle ou d'un mort-vivant sont définies, on peut les créer, les dupliquer... On fait le choix de définir un fichier par classe. ===== La classe Undead ===== Nous ne créons pas la classe ''Undead'' définissant un mort-vivant à partir de rien : nous créons une classe qui **hérite** de ''pygame.sprite.Sprite'', une classe de Pygame qui définit déjà tout un tas de comportement. C'est bien sûr l'intérêt d'un module comme Pygame : nous n'avons pas à tout faire ! Je détaille ci-dessous ce que nous avons à faire. # Fichier undead.py import pygame class Undead(pygame.sprite.Sprite): SIZE = 40 # taille du sprite en pixels COLOR = (245, 242, 66) # couleur du sprite en RGB - du jaune VELOCITY = 10 # nombre de pixels parcourus à chaque image def __init__(self, x): """ x: position x de création du mort-vivant Cette fonction crée un mort-vivant en haut de l'écran. """ # on commence par demander toutes les initialisations # requises par l'objet de base défini dans pygame.sprite.Sprite : super().__init__() # un sprite doit avoir un attribut image. # Nous pourrions charger un fichier image, mais ici, nous # nous contentons de créer un rectangle self.image = pygame.Surface((self.SIZE, self.SIZE)) # nous remplissons ce rectangle dans la couleur choisie self.image.fill(self.COLOR) # le sprite a un attribut rect qui est un objet définissant # le cadre délimitant le sprite self.rect = self.image.get_rect() # on règle le centre rectangle à la position choisie self.rect.centerx= x # la position y = 0 est en haut, on laisse une marge self.rect.y = 10 def update_position(self): """ Tenant compte du mouvement du mort-vivant, calcule la nouvelle position du mort-vivant """ self.rect.y += self.VELOCITY Plus d'aide sur [[https://www.pygame.org/docs/ref/rect.html|rect]]. ===== Classe Bullet ===== Dans un fichier ''bullet.py'' on définit une classe ''Bullet''. Quasiment la même chose avec ces différences : * l'aspect graphique un peu différent : c'est un carré plus petit et blanc, * la vitesse est plus élevée, * il faut préciser ''x'' et ''y'' à la création, * la balle va vers le haut et non vers le bas, ==== Balle perdue ==== Si le ''self.rect.y'' devient ''<0'', cela signifie que la balle est sorti de l'écran par le haut. C'est une balle perdue. On peut la détruire. On utilise la méthode ''self.kill()'' qui se charge de supprimer l'objet. Cela se passe dans la méthode ''update_position()''. ===== Classe player ===== Dans un fichier ''player.py'' on définit la classe ''Player''. Le fichier suivant est complet. # fichier player.py import pygame from bullet import Bullet class Player(pygame.sprite.Sprite): COLOR = (255, 0, 0) VELOCITY = 5 SIZE = 50 def __init__(self, x0, y0, xmin, xmax): """ x0, y0: position initiale xmin, xmax: valeurs à ne pas dépasser """ super().__init__() self.image = pygame.Surface((self.SIZE, self.SIZE)) self.image.fill(self.COLOR) self.rect = self.image.get_rect() self.rect.centerx = x0 self.rect.centery = y0 self.xmin = xmin self.xmax = xmax self.vx = 0 def move_left(self): """ amorce un mouvement vers la gauche """ self.vx = -self.VELOCITY def move_right(self): """ amorce un mouvement vers la droite """ self.vx = self.VELOCITY def fire(self): """ crée une balle à la position courante du joueur """ return Bullet(self.rect.centerx, self.rect.centery) def update_position(self): """ Calcule la nouvelle position tenant compte de la vitesse courante et sans dépasser les limites """ self.rect.centerx += self.vx if self.rect.centerx > self.xmax: # il ne faut pas dépasser à droite self.rect.centerx = self.xmax elif self.rect.centerx < self.xmin: # il ne faut pas dépasser à gauche self.rect.centerx = self.xmin # à chaque réactualisation, la vitesse est amortie # pour créer un effet de ralentissement self.vx *= 0.8 # quand la vitesse devient assez faible, on la met à 0 if abs(self.vx) < 1: self.vx = 0 ===== Le programme principal ===== On crée un fichier ''main.py'', programme principal. # fichier main.py import pygame from random import randint # import des modules des éléments de jeu from player import Player from undead import Undead # gestion du rythme de rafraîchissement écran, # notamment pour que le jeu s'écoule au même rythme # quelque soit la puissance de la machine. clock = pygame.time.Clock() # gestion du temps # réglage des paramètres de jeu # c'est plus propre de le faire en définissant des constantes LARGEUR = 750 HAUTEUR = 750 # création surface de jeu screen = pygame.display.set_mode((LARGEUR, HAUTEUR)) # titre de la fenêtre pygame.display.set_caption("L'attaque des morts-vivants") # Il sera parfois utile de grouper les sprites. # Par exemple, il peut être pratique de demander quelque chose # à l'ensemble des morts-vivants. De plus, certaines fonctions # de Pygame n'existent que sur des groupes, comme les fonctions # permettant de détecter une collision de sprites. # un même sprite peut être dans plusieurs groupes all_sprites = pygame.sprite.Group() # groupe contenant tous les sprites undeads_sprites = pygame.sprite.Group() # groupe pour les morts-vivants bullets_sprites = pygame.sprite.Group() # groupe pour les balles # création du sprite joueur player = Player(50, HAUTEUR - Player.SIZE - 10, 0, LARGEUR) # ajout du sprite au groupe de tous les sprites all_sprites.add(player) # on enferme le jeu dans une boucle infinie. # la sortie de la boucle est gérée par la valeur de running running = True # on décide d'envoyer un nouveau mort-vivant toutes les 150 images FRAMES_BETWEEN_UNDEAD = 150 # pour cela on initialise un décompteur. # Quand il tombe à 0, un mort-vivant apparaît frames_count_down = FRAMES_BETWEEN_UNDEAD # boucle principale. Elle se répète indéfiniment pour # rafraîchir l'affichage while running: # limitation du taux de rafraîchissement à 60 images / seconde clock.tick(60) # Avant de dessiner la prochaine image, on repeint le cadre en noir # ce qui efface tout ce qu'il y a eu avant screen.fill((0,0,0)) frames_count_down -= 1 if frames_count_down == 0: # apparition d'un mort-vivant # calcul de la position aléatoire x = randint(100, LARGEUR-100) # création du mort-vivant new_undead = Undead(x) # ajout du mort-vivants aux groupes de sprites undeads_sprites.add(new_undead) all_sprites.add(new_undead) # réinitialisation du décompteur frames_count_down = FRAMES_BETWEEN_UNDEAD # actualisation de l'affichage de tous les sprites for sprite in all_sprites: sprite.update_position() all_sprites.draw(screen) # lecture des événements # les événement sont notamment les demandes de l'utilisateur # souris, clavier, demande de fermeture de la fenêtre... for event in pygame.event.get(): if event.type == pygame.QUIT: # fermeture fenêtre running = False # récupère la liste des états des touches keys = pygame.key.get_pressed() if keys[pygame.K_LEFT] and not keys[pygame.K_RIGHT]: # la touche flèche gauche est appuyée mais pas la flèche droite player.move_left() if keys[pygame.K_RIGHT] and not keys[pygame.K_LEFT]: # la touche flèche droite est appuyée player.move_right() if keys[KEY_SPACE]: # la touche espace est appuyée # création d'une balle newBullet = player.fire() # ajout du sprite dans les groupes all_sprites.add(newBullet) bullets_sprites.add(newBullet) # parcours des morts-vivants pour savoir si l'un d'eux a reçu une balle for undead in undeads_sprites: # On utilise la fonction spritecollide # le dernier argument, True, signifie que la balle touchée est détruite hits = pygame.sprite.spritecollide(undead, bullets_sprites, True) # hits contient la liste des balles # il nous suffit de savoir si hits est vide ou non if len(hits) > 0: undead.kill() # mise à jour de l'affichage pygame.display.flip() # quand la boucle while se termine, on peut quitter le jeu pygame.quit() ===== Encore beaucoup de travail... ===== Si vous avez su créer la classe ''Bullet'', le jeu devrait fonctionner. Mais il reste encore beaucoup à faire ! * que se passe-t-il si un mort-vivant n'est pas abattu et atteint la ligne du bas ? * comptage et affichage des points ? * les morts-vivants pourraient arriver par vague. * les morts-vivants pourraient avoir une barre de vie et ne mas mourir d'une seule balle. * certains morts-vivants pourraient avoir plus ou moins de vie, être plus ou moins gros. * remplacer les sprites géométriques par des images ? * permettre au joueur de changer d'armes. * limiter les munitions du joueur, limiter le débit, contraindre le joueur à recharger. * les morts-vivants ne marchent pas en ligne droite. * les morts-vivants jettent des projectiles sur le joueur. * etc. Vous êtes libres d'ajouter vos propres idées ! ===== Exemple de panneau de score ===== On souhaite ajouter un panneau qui affiche un score et une barre de vie pour le joueur. On aura besoin d'ajouter un peu de texte. Il faut ajouter : # dans main, après l'import de pygame pygame.init() On peut ajouter un module pour le score. Il consistera en un simple rectangle avec un texte pour le score et une barre pour la vie. # fichier score.py import pygame class Score(pygame.sprite.Sprite): COLOR = (100, 100, 100) # couleur du fond. Un gris LIFE_COLOR = (100, 255, 100) # couleur de la vie. Un vert LIFE_HEIGHT = 10 # hauteur de la barre de vie WIDTH=200 # largeur du panneau HEIGHT=50 # hauteur du panneau def __init__(self): super().__init__() self.image = pygame.Surface((self.WIDTH, self.HEIGHT)) self.rect = self.image.get_rect() self.rect.x = 0 self.rect.y = 0 self.score = 0 self.life = 100 # on doit initialiser une objet pour le texte self.font = pygame.font.SysFont('timesnewroman', 30) # le panneau ne change pas beaucoup # on se contente de le rafraîchir quand nécessaire self.refresh() def up_score(self, amount:int): """ augmente le score de la quantité amount réactualise l'affichage """ self.score += amount self.refresh() def down_life(self, amount:int): """ diminue le score de la quantité amount ne peut descendre en dessous de 0 réactualise l'affichage """ self.life -= amount if self.life < 0: self.life = 0 self.refresh() def refresh(self): """ reconstruit le contenu du panneau """ self.image.fill(self.COLOR) # remarquez le formatage du score textsurface = self.font.render(f'Score : {self.score:04d}', False, (0, 0, 0)) # blit permet de placer les pixels du texte dans self.image self.image.blit(textsurface,(0,0)) # construction de la barre de vie: un simple rectangle # aligné en bas du panneau largeur_barre = int(self.WIDTH / 100 * self.life) barre = pygame.Surface((largeur_barre, self.LIFE_HEIGHT)) barre.fill(self.LIFE_COLOR) rect_barre = barre.get_rect() rect_barre.bottom = self.rect.bottom # là encore, blit permet de placer les pixels de la barre dans self.image self.image.blit(barre, rect_barre) def draw(self, screen): # Les autres sprites du jeu sont placés dans des groupes de sprites # les groupes ont une fonction draw qui fait la même chose que celle ci. # comme notre score sera à part, on le dote du fonction draw # qui reçoit l'écran de jeu et y place le contenu de self.image screen.blit(self.image, self.rect) À chaque nouvelle image, l'affichage est effacé. Il faut donc redessiner le panneau de score à chaque fois. On complète donc `main.py` : # actualisation de l'affichage de tous les sprites for sprite in all_sprites: sprite.update_position() all_sprites.draw(screen) score.draw(screen) Et on souhaite qu'à chaque mort-vivant abattu on gagne par exemple 10 pts. On complète ''main.py''. for undead in undeads_sprites: # On utilise la fonction sprtecollide # le dernier argument, True, signifie que la balle touchée est détruite hits = pygame.sprite.spritecollide(undead, bullets_sprites, True) # hits contient la liste des balles # il nous suffit de savoir si hits est vide ou non if len(hits) > 0: undead.kill() score.up_score(10) On voudrait enfin que chaque fois qu'un mort vivant dépasse la ligne du bas de l'écran, le joueur perde un peu de vie. On ajoute une méthode dans ''undead.py'' : def accross(self, y): """ y: ordonnée renvoie True si le undead dépasse l'ordonnée y """ return self.rect.top > y Et on complète ''main.py'' : for undead in undeads_sprites: # On utilise la fonction sprtecollide # le dernier argument, True, signifie que la balle touchée est détruite hits = pygame.sprite.spritecollide(undead, bullets_sprites, True) # hits contient la liste des balles # il nous suffit de savoir si hits est vide ou non if len(hits) > 0: undead.kill() score.up_score(10) elif undead.accross(HAUTEUR): undead.kill() score.down_life(10)