R p te une chaîne
Dans ce tutoriel nous allons compléter le jeu vidéo créé dans les parties 1 et 2, par l'ajout des images de sprites pour améliorer l'apparence, l'ajout des effets sonores et régler la vitesse du jeu.
À la fin de la seconde partie de ce tutoriel nous avons obtenu un jeu, mais soyons honnêtes... Il est plutôt laid. Le joueur et les ennemis ne sont que des blocs blancs sur un fond noir. C'était le top du top à la naissance de Pong, mais ce n'est plus du tout le cas aujourd'hui.
Donc remplaçons tous ces rectangles blancs ennuyeux et moches par des images plus cool qui donneront au jeu l'impression d'être un vrai jeu.
Plus tôt dans nos cours et tutoriels, nous avons appris que les images peuvent être chargées dans une Surface à l'aide du module image.
Dans ce tutoriel, nous avons créé un petit jet avec un joueur et des missiles pour les ennemis. Nous vous invitons à utiliser ces images, à dessiner les vôtres ou à télécharger des images gratuites pour le jeu. Vous pouvez cliquer sur le lien ci-dessous pour télécharger les illustrations utilisées dans ce tutoriel :
Avant d'utiliser des images pour représenter les sprites du joueur et de l'ennemi, nous devons modifier leurs constructeurs. Le code ci-dessous remplace le code utilisé précédemment :
Exemple : Copier le code
# Importer pygame.locals pour faciliter l'accès aux coordonnées clés # Mise à jour pour se conformer aux standards flake8 et black # from pygame.locals import * from pygame.locals import ( RLEACCEL, 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 l'objet Player en étendant pygame.sprite.Sprite # Au lieu d'une surface, utilisez une image pour un meilleur sprite class Joueur(pygame.sprite.Sprite): def __init__(self): super(Joueur, self).__init__() self.surf = pygame.image.load("chemin-vers-le-dossier-dans-votre-pc/avion.png").convert() self.surf.set_colorkey((255, 255, 255), RLEACCEL) self.rect = self.surf.get_rect()
Décortiquons un peu le code:
L'appel à la méthode pygame.image.load() charge une image depuis le disque. Nous lui passons un chemin vers le fichier. Il renvoie une Surface.
Et l'appel à la méthode .convert() optimise la Surface, rendant les futurs appels à .blit() plus rapides.
L'utilisation de .set_colorkey() pour indiquer la couleur que pygame rendra transparente. Dans ce cas, nous choisissons le blanc, car c'est la couleur de fond de l'image du jet. La constante RLEACCEL est un paramètre optionnel qui aide pygame à effectuer un rendu plus rapide sur les écrans non accélérés. Elle est ajoutée à la déclaration d'importation de pygame.locals.
Rien d'autre n'a besoin d'être modifié. L'image est toujours une Surface, sauf que maintenant elle a une image peinte dessus. Vous continuez à l'utiliser de la même manière.
Le code suivat montre des changements similaires pour l'ennemi :
Exemple : Copier le code
# Définir l'objet ennemi en étendant pygame.sprite.Sprite # Au lieu d'une surface, utilisez une image pour un meilleur sprite class Ennemi(pygame.sprite.Sprite): def __init__(self): super(Ennemi, self).__init__() self.surf = pygame.image.load("chemin-vers-le-dossier-dans-votre-pc/missile.png").convert() self.surf.set_colorkey((255, 255, 255), RLEACCEL) # La position de départ est générée aléatoirement, tout comme la vitesse self.rect = self.surf.get_rect( center=( random.randint(LARGEUR_ECRAN + 20, LARGEUR_ECRAN + 100), random.randint(0, HAUTEUR_ECRAN), ) ) self.vitesse = random.randint(1, 5)
L'exécution du programme devrait montrer qu'il s'agit du même jeu que celui que nous avions auparavant, à ceci près que nous avons ajouté de jolis skins graphiques avec des images. Mais pourquoi se contenter de rendre seulement les sprites du joueur et de l'ennemi agréables à regarder ? Ajoutons aussi quelques nuages qui passent pour donner l'impression d'un jet volant dans le ciel.
Pour les nuages d'arrière-plan, vous utilisez les mêmes principes que pour le joueur et l'ennemi :
Création de la classe Nuage.
Ajoutons-y l'image d'un nuage.
Créons une méthode .update() qui déplace le nuage vers le côté gauche de l'écran.
Créons aussi un événement et un gestionnaire personnalisés pour créer de nouveaux objets nuage à un intervalle de temps donné.
Ajoutons les objets nuages nouvellement créés à un nouveau groupe appelé clouds (nuages).
Mettons à jour et dessinons les nuages dans la boucle du jeu.
Voici à quoi ressemble un nuage :
Exemple : Copier le code
# Définir l'objet nuage en étendant pygame.sprite.Sprite # Utiliser une image pour un sprite plus beau class Cloud(pygame.sprite.Sprite) : def __init__(self) : super(Cloud, self).__init__() self.surf = pygame.image.load("chemin-vers-le-dossier-dans-votre-pc/nuage.png").convert() self.surf.set_colorkey((0, 0, 0), RLEACCEL) # La position de départ est générée aléatoirement self.rect = self.surf.get_rect( center=( random.randint(LARGEUR_ECRAN + 20, LARGEUR_ECRAN + 100), random.randint(0, HAUTEUR_ECRAN), ) ) # Déplacer le nuage en fonction d'une vitesse constante # Retire le nuage lorsqu'il passe le bord gauche de l'écran def update(self) : self.rect.move_ip(-2, 0) if self.rect.right < 0 : self.kill()
Tout cela devrait nous sembler très familier. C'est à peu près la même chose que pour la classe Ennemi.
Pour faire apparaître les nuages à certains intervalles, nous allons utiliser un code de création d'événement similaire à celui que nous avons utilisé pour créer de nouveaux ennemis. Plaçons-le juste en dessous de l'événement de création d'ennemi :
Exemple : Copier le code
# Créer des événements personnalisés pour ajouter un nouvel ennemi et un nouvel nuage ADDENEMY = pygame.USEREVENT + 1 pygame.time.set_timer(ADDENEMY, 250) ADDCLOUD = pygame.USEREVENT + 2 pygame.time.set_timer(ADDCLOUD, 500)
Cela signifie qu'il faut attendre 500 millisecondes, soit une demi seconde, avant de créer le nuage suivant.
Ensuite, créons un nouveau groupe pour contenir chaque nuage nouvellement créé :
Exemple : Copier le code
# Créer des groupes pour contenir les sprites ennemis, les sprites de nuages et tous les sprites # - ennemis est utilisé pour la détection des collisions et les mises à jour de position # - nuages est utilisé pour les mises à jour de position # - all_sprites est utilisé pour le rendu ennemis = pygame.sprite.Group() all_sprites = pygame.sprite.Group() nuages = pygame.sprite.Group() all_sprites = pygame.sprite.Group() all_sprites.add( joueur)
Ensuite, ajoutez un gestionnaire pour le nouvel événement ADDCLOUD dans le gestionnaire d'événements :
Exemple : Copier le code
# Boucle principale while running : # Regarde chaque événement dans la file d'attente pour event dans pygame.event.get() : # L'utilisateur a-t-il appuyé sur une touche ? if event.type == KEYDOWN : # Était-ce la touche Escape ? Si c'est le cas, arrêtez la boucle. if event.key == K_ESCAPE : running = False # L'utilisateur a-t-il cliqué sur le bouton de fermeture de la fenêtre ? Si c'est le cas, arrêtez la boucle. elif event.type == QUIT : running = False # Ajouter un nouvel ennemi ? elif event.type == ADDENEMY : # Créer le nouvel ennemi et l'ajouter aux groupes de sprites new_enemy = Enemy() enemies.add(new_enemy) all_sprites.add(new_enemy) # Ajouter un nouveau nuage ? elif event.type == ADDCLOUD : # Créer le nouveau nuage et l'ajouter aux groupes de sprites nouveau_nuage = Cloud() nuages.add(nouveau_nuage) all_sprites.add( nouveau_nuage)
Enfin, assurez-vous que les nuages sont mis à jour à chaque image :
Exemple : Copier le code
# Mettre à jour la position des ennemis et des nuages ennemis.update() nuages.update() # Remplir l'écran de bleu ciel screen.fill((135, 206, 250))
On met à jour le screen.fill() original pour remplir l'écran d'une agréable couleur bleu ciel. N'hésitez pas à changer cette couleur pour quelque chose d'autre. Peut-être voulez-vous un monde extraterrestre avec un ciel violet, un terrain vague toxique en vert néon, ou la surface de Mars en rouge !
Notez que chaque nouveau nuage et chaque nouvel ennemi sont ajoutés à all_sprites ainsi qu'aux nuages et aux ennemis. Cela s'explique par le fait que chaque groupe est utilisé dans un but différent :
Le rendu est effectué à l'aide de all_sprites.
Les mises à jour de position sont effectuées à l'aide des nuages et des ennemis.
La détection des collisions se fait à l'aide des ennemis.
Nous créons plusieurs groupes afin de pouvoir modifier la façon dont les sprites se déplacent ou se comportent sans affecter le mouvement ou le comportement des autres sprites.
En testant le jeu, vous avez peut-être remarqué que les ennemis se déplacent un peu vite. Si ce n'est pas le cas, ce n'est pas grave, car les résultats varieront d'une machine à l'autre.
La raison en est que la boucle du jeu traite les images aussi vite que le processeur et l'environnement le permettent. Comme tous les sprites se déplacent une fois par image, ils peuvent se déplacer des centaines de fois par seconde. Le nombre d'images traitées par seconde s'appelle le taux de rafraîchissement, et un bon réglage fait la différence entre un jeu jouable et un jeu oubliable.
Normalement, vous voulez une fréquence d'images aussi élevée que possible, mais pour ce jeu, vous devez la ralentir un peu pour que le jeu soit jouable. Heureusement, le module time contient une horloge qui est conçue exactement à cette fin.
L'utilisation de Clock pour établir une fréquence d'images jouable ne nécessite que deux lignes de code. La première crée une nouvelle horloge avant que la boucle du jeu ne commence :
Exemple : Copier le code
# Régler l'horloge pour un framerate adéquat clock = pygame.time.Clock()
La seconde appelle .tick() pour informer pygame que le programme a atteint la fin de l'image :
Exemple : Copier le code
# Afficher le tout à l'écran pygame.display.flip() # S'assurer que le programme maintient un taux de 30 images par seconde clock.tick(30)
L'argument transmis à .tick() détermine la fréquence d'images souhaitée. Pour ce faire, .tick() calcule le nombre de millisecondes que chaque image doit prendre, en fonction de la fréquence d'images souhaitée. Il compare ensuite ce nombre au nombre de millisecondes qui se sont écoulées depuis le dernier appel à .tick(). Si le temps écoulé est insuffisant, .tick() retarde le traitement pour s'assurer qu'il ne dépasse jamais la fréquence d'images spécifiée.
Une fréquence d'images plus faible laisse plus de temps à chaque image pour les calculs, tandis qu'une fréquence d'images plus élevée permet un jeu plus fluide (et éventuellement plus rapide) :
N’hésitez pas à changer ce chiffre pour voir ce qui vous convient le mieux !
Jusqu'à présent, nous nous sommes concentrés sur le gameplay et les aspects visuels de notre jeu. Maintenant, explorons comment ajouter du son à notre jeu. pygame fournit le module de mixage 'mixer' pour gérer toutes les activités liées au son. Nous utiliserons les classes et les méthodes de ce module pour fournir de la musique de fond et des effets sonores pour diverses actions.
Le nom mixer fait référence au fait que le module mélange différents sons en un tout cohérent. Le sous-module Musique (Music) nous permet de diffuser des fichiers sonores individuels dans une variété de formats, y compris MP3, Ogg et Mod. Nous pouvons également utiliser Sound pour enregistrer et jouer un effet sonore unique, au format Ogg ou WAV non compressé. La lecture se fait en arrière-plan, donc lorsque nous jouons un son, la méthode sera renvoyée dès que le son aura été joué.
Note : La documentation de pygame indique que la prise en charge des MP3 est limitée et que les formats non pris en charge peuvent entraîner un plantage du système. Les sons référencés dans cet article ont été testés, et nous vous recommandons de tester tous les sons avant de publier votre jeu.
Comme pour la plupart des choses dans Pygame, l'utilisation du mixeur commence par une étape d'initialisation. Heureusement, cette étape est déjà réalisée par pygame.init(). Nous n'avons besoin d'appeler pygame.mixer.init() que si nous voulons changer les paramètres par défaut :
Exemple : Copier le code
# Configuration pour les sons. Les valeurs par défaut sont bonnes. pygame.mixer.init() # Initialiser pygame pygame.init() # Configurer l'horloge pour un framerate décent clock = pygame.time.Clock()
pygame.mixer.init() accepte un certain nombre d'arguments, mais les valeurs par défaut fonctionnent bien dans la plupart des cas. Notez que si vous voulez changer les valeurs par défaut, vous devez appeler pygame.mixer.init() avant d'appeler pygame.init(). Sinon, les valeurs par défaut resteront inchangées.
Une fois le système initialisé, nous pouvons configurer les sons et la musique d'ambiance :
Voici le code à placer just avant la boucle principale du jeu :
Exemple : Copier le code
# Chargement et lecture de la musique de fond # Source du son : http://ccmixter.org/files/Apoxode/59262 # Licence : https://creativecommons.org/licenses/by/3.0/ pygame.mixer.music.load("Apoxode_-_Electric_1.mp3") pygame.mixer.music.play(loops=-1) # Chargement de tous les fichiers son # Sources sonores : Jon Fincher move_up_sound = pygame.mixer.Sound("Rising_putter.ogg") move_down_sound = pygame.mixer.Sound("Falling_putter.ogg") collision_sound = pygame.mixer.Sound("Collision.ogg")
Les lignes :
pygame.mixer.music.load("Apoxode_-_Electric_1.mp3") pygame.mixer.music.play(loops=-1)
chargent un clip sonore d'arrière-plan et commencent à le jouer. Nous pouvons demander au clip sonore de tourner en boucle et de ne jamais se terminer en définissant le paramètre nommé loops=-1.
Les lignes:
move_up_sound = pygame.mixer.Sound("Rising_putter.ogg") move_down_sound = pygame.mixer.Sound("Falling_putter.ogg") collision_sound = pygame.mixer.Sound("Collision.ogg")
Chargent trois sons que nous utiliserons pour divers effets sonores. Les deux premiers sont des sons ascendants et descendants, qui sont joués lorsque le joueur se déplace vers le haut ou vers le bas. Le dernier est le son utilisé lorsqu'il y a une collision. Nous pouvons également ajouter d'autres sons, comme un son pour chaque fois qu'un ennemi est créé, ou un son final pour la fin du jeu.
Comment utiliser les effets sonores ? Nous voulons que chaque son soit joué lorsqu'un certain événement se produit. Par exemple, lorsque le navire se déplace vers le haut, nous voulons jouer le son move_up_sound. Par conséquent, vous ajoutez un appel à .play() chaque fois que nous gérons cet événement. Dans la conception, cela signifie ajouter les appels suivants à .update() pour Player :
Exemple : Copier le code
# Définir l'objet Player en étendant pygame.sprite.Sprite # Au lieu d'une surface, utilisez une image pour un meilleur sprite class Player(pygame.sprite.Sprite) : def __init__(self) : super(Player, self).__init__() self.surf = pygame.image.load("jet.png").convert() self.surf.set_colorkey((255, 255, 255), RLEACCEL) self.rect = self.surf.get_rect() # Déplacer le sprite en fonction des touches pressées def update(self, pressed_keys) : if pressed_keys[K_UP] : self.rect.move_ip(0, -5) move_up_sound.play() if pressed_keys[K_DOWN] : self.rect.move_ip(0, -5) move_up_sound.play() self.rect.move_ip(0, 5) move_down_sound.play()
Pour une collision entre le joueur et un ennemi, nous jouons le son correspondant à la détection des collisions :
Exemple : Copier le code
# 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() # Arrête les sons de mouvement et joue le son de collision move_up_sound.stop() move_down_sound.stop() collision_sound.play() # Arrêter la boucle running = False
Ici, nous arrêtons d'abord tous les autres effets sonores, car lors d'une collision, le joueur ne se déplace plus. Ensuite, nous jouons le son de la collision et continuons l'exécution à partir de là.
Enfin, lorsque le jeu est terminé, tous les sons doivent s'arrêter. Ceci est vrai que le jeu se termine à cause d'une collision ou que l'utilisateur quitte le jeu manuellement. Pour ce faire, ajoutons les lignes suivantes à la fin du programme, après la boucle :
Exemple : Copier le code
# Tout! Arrêter et quitter le mixeur. pygame.mixer.music.stop() pygame.mixer.quit()
Techniquement, ces dernières lignes ne sont pas nécessaires, puisque le programme se termine juste après. Cependant, si nous décidons plus tard d'ajouter un écran d'introduction ou un écran de sortie à notre jeu, il se peut qu'il y ait plus de code qui s'exécute après la fin du jeu.
Conclusion
Tout au long de ce tutoriel, nous avons appris comment la programmation de jeux avec pygame diffère de la programmation procédurale standard. Nous avons également appris à :
Pour ce faire, nous avons utilisé un sous-ensemble de modules pygame, notamment les modules display, mixer and music, time, image, event et key. Nous avons également utilisé plusieurs classes pygame, notamment Rect, Surface, Sound et Sprite. Mais cela ne fait qu'effleurer la surface de ce que pygame peut faire ! Consultez la documentation officielle de pygame pour une liste complète des modules et classes disponibles.
Vous pouvez trouver tous les fichiers de code, de graphiques et de sons pour cet article en cliquant sur le lien ci-dessous :
Exemple de code : Télécharger le code source du projet PyGame utilisé dans ce tutoriel.