Outils pour utilisateurs

Outils du site


nsi:tds:pygame:zombies

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
nsi:tds:pygame:zombies [2023/01/17 14:00] – supprimée - modification externe (Unknown date) 127.0.0.1nsi:tds:pygame:zombies [2023/03/18 18:17] (Version actuelle) goupillwiki
Ligne 1: Ligne 1:
 +===== 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.
 +
 +<code python linenums>
 +# 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
 +</code>
 +
 +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.
 +
 +<code python linenums>
 +# 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
 +</code>
 +
 +===== Le programme principal =====
 +
 +On crée un fichier ''main.py'', programme principal.
 +
 +<code python linenums>
 +# 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()
 +</code>
 +
 +===== 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 :
 +
 +<code python>
 +# dans main, après l'import de pygame
 +pygame.init()
 +</code>
 +
 +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.
 +
 +<code python>
 +# 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)
 +</code>
 +
 +À chaque nouvelle image, l'affichage est effacé. Il faut donc redessiner le panneau de score à chaque fois. On complète donc `main.py` :
 +
 +<code python>
 +    # actualisation de l'affichage de tous les sprites
 +    for sprite in all_sprites:
 +        sprite.update_position()
 +    all_sprites.draw(screen)
 +    score.draw(screen)
 +</code>
 +
 +Et on souhaite qu'à chaque mort-vivant abattu on gagne par exemple 10 pts. On complète ''main.py''.
 +
 +<code python>
 +    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)
 +</code>
 +
 +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'' :
 +
 +<code python>
 +    def accross(self, y):
 +        """
 +        y: ordonnée
 +        renvoie True si le undead dépasse l'ordonnée y
 +        """
 +        return self.rect.top > y
 +</code>
 +
 +Et on complète ''main.py'' :
 +
 +<code python>
 +    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)
 +</code>