nsi:tds:pygame:zombies
Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentesRévision précédenteProchaine 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.1 | nsi:tds:pygame:zombies [2023/03/18 18:17] (Version actuelle) – goupillwiki | ||
|---|---|---|---|
| Ligne 1: | Ligne 1: | ||
| + | ===== Attaque de zombies avec Pygame ====== | ||
| + | {{ .: | ||
| + | |||
| + | Des mort vivants attaquent. Il faut les abattre. Difficile de faire plus simple... | ||
| + | |||
| + | La figure ci-dessus schématise l' | ||
| + | * les morts-vivants, | ||
| + | * le joueur, en rouge en bas, se déplace horizontalement, | ||
| + | * 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 '' | ||
| + | |||
| + | 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: position x de création du mort-vivant | ||
| + | Cette fonction crée un mort-vivant en haut de l' | ||
| + | """ | ||
| + | # on commence par demander toutes les initialisations | ||
| + | # requises par l' | ||
| + | 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, | ||
| + | |||
| + | # 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, | ||
| + | la nouvelle position du mort-vivant | ||
| + | """ | ||
| + | self.rect.y += self.VELOCITY | ||
| + | </ | ||
| + | |||
| + | Plus d'aide sur [[https:// | ||
| + | | ||
| + | ===== Classe Bullet ===== | ||
| + | |||
| + | Dans un fichier '' | ||
| + | |||
| + | Quasiment la même chose avec ces différences : | ||
| + | * l' | ||
| + | * la vitesse est plus élevée, | ||
| + | * il faut préciser '' | ||
| + | * la balle va vers le haut et non vers le bas, | ||
| + | |||
| + | ==== Balle perdue ==== | ||
| + | |||
| + | Si le '' | ||
| + | |||
| + | Cela se passe dans la méthode '' | ||
| + | |||
| + | ===== Classe player ===== | ||
| + | |||
| + | Dans un fichier '' | ||
| + | |||
| + | 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: position initiale | ||
| + | xmin, xmax: valeurs à ne pas dépasser | ||
| + | """ | ||
| + | super().__init__() | ||
| + | self.image = pygame.Surface((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, | ||
| + | |||
| + | 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, | ||
| + | # 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 '' | ||
| + | |||
| + | <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' | ||
| + | # 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, | ||
| + | |||
| + | # titre de la fenêtre | ||
| + | pygame.display.set_caption(" | ||
| + | |||
| + | |||
| + | # Il sera parfois utile de grouper les sprites. | ||
| + | # Par exemple, il peut être pratique de demander quelque chose | ||
| + | # à l' | ||
| + | # de Pygame n' | ||
| + | # 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' | ||
| + | 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' | ||
| + | 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, | ||
| + | |||
| + | frames_count_down -= 1 | ||
| + | if frames_count_down == 0: | ||
| + | # apparition d'un mort-vivant | ||
| + | # calcul de la position aléatoire | ||
| + | x = randint(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' | ||
| + | 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' | ||
| + | # souris, clavier, demande de fermeture de la fenêtre... | ||
| + | for event in pygame.event.get(): | ||
| + | if event.type == pygame.QUIT: | ||
| + | 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, | ||
| + | # 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' | ||
| + | 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 '' | ||
| + | |||
| + | * 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' | ||
| + | * 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' | ||
| + | |||
| + | ===== 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' | ||
| + | |||
| + | <code python> | ||
| + | # dans main, après l' | ||
| + | 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. | ||
| + | |||
| + | <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 | ||
| + | HEIGHT=50 | ||
| + | |||
| + | def __init__(self): | ||
| + | super().__init__() | ||
| + | self.image = pygame.Surface((self.WIDTH, | ||
| + | |||
| + | 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(' | ||
| + | # le panneau ne change pas beaucoup | ||
| + | # on se contente de le rafraîchir quand nécessaire | ||
| + | self.refresh() | ||
| + | |||
| + | |||
| + | def up_score(self, | ||
| + | """ | ||
| + | augmente le score de la quantité amount | ||
| + | réactualise l' | ||
| + | """ | ||
| + | self.score += amount | ||
| + | self.refresh() | ||
| + | |||
| + | def down_life(self, | ||
| + | """ | ||
| + | diminue le score de la quantité amount | ||
| + | ne peut descendre en dessous de 0 | ||
| + | réactualise l' | ||
| + | """ | ||
| + | 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' | ||
| + | # blit permet de placer les pixels du texte dans self.image | ||
| + | self.image.blit(textsurface, | ||
| + | | ||
| + | # 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, | ||
| + | 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, | ||
| + | |||
| + | 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' | ||
| + | screen.blit(self.image, | ||
| + | </ | ||
| + | |||
| + | À chaque nouvelle image, l' | ||
| + | |||
| + | <code python> | ||
| + | # actualisation de l' | ||
| + | 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 '' | ||
| + | |||
| + | <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, | ||
| + | # 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' | ||
| + | |||
| + | <code python> | ||
| + | def accross(self, | ||
| + | """ | ||
| + | y: ordonnée | ||
| + | renvoie True si le undead dépasse l' | ||
| + | """ | ||
| + | return self.rect.top > y | ||
| + | </ | ||
| + | |||
| + | Et on complète '' | ||
| + | |||
| + | <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, | ||
| + | # 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) | ||
| + | </ | ||
