Cette page fait partie du cours de polytech PeiP1 et 2 Bio

7. Mise en pratique : Tennis ou jeu de Pong

7.1. Introduction

Pong est l'un des premiers jeux vidéo d'arcade commercialisé en 1972. Il s'inspire du tennis de table. Devant l'engouement généré par le jeu, Pong est porté sur une console de salon à partir de 1975.

Dans la même veine, il a existé beaucoup plus tard l'ancêtre de la Nintendo switch

Le package PyGame permet de réaliser avec Python des jeux en quelques heures. Nous allons donc créer un jeu de pong rudimentaire en utilisant des classes pour gérer la balle et la raquette

Voici ce à quoi ressemblera l'interface du jeu que nous allons créer :

Pong

7.2. Objets et classes

La Programmation (Orientée) Objet (POO) est un paradigme de programmation dont le but est de simplifier le codage des concepts liés à un domaine.

Elle se fonde sur plusieurs principes dont les principaux sont :

  • la classe qui permet de créer des structures de données :
    • l'encapsulation des données
    • constructeur et destructeur
    • attributs et méthodes
  • l'héritage qui permet la réutilisabilité
  • la généricité qui permet la généralisation

On remarquera qu'en Python la paradigme Objet est plutôt contraignant notamment avec l'utilisation du mot-clé self.

Par exemple, dans un jeu de quête dans un univers en deux ou trois dimensions, on trouvera :

En ce qui concerne les personnages et les monstres, il ont tous une caractéritique commune : le nombre de points de vie par exemple.

Les personnages auront des capacités comme la force, la dextérité qui autorisent les guerriers à porter des armes et des armures lourdes alors que les magiciens qui jetteront des sorts n'auront pas la force de porter des armes lourdes.

En modélisation objet cela permet de créer des classes qui sont des structures de données dotées de méthodes (sous-programmes) qui peuvent être héritées par d'autres classes.

Hierarchie d'objets

Pour utiliser une classe il faut en créer un représentant qui s'appelle une instance de la classe.

7.3. Pygame

Nous allons mettre en oeuvre le concept objet en définissant une classe Balle et une classe Raquette pour le jeu de Pong.

7.3.1. Ouverture d'une fenêtre graphique

Voici le code à saisir pour créer une fenêtre graphique dans laquelle nous dessinerons :

pygame_initialisation.py

  1. import pygame
  2. from pygame.locals import *
  3.  
  4. HAUTEUR_FENETRE = 480
  5. LARGEUR_FENETRE = 640
  6.  
  7. fenetre = pygame.display.set_mode( (LARGEUR_FENETRE, HAUTEUR_FENETRE) )
  8. pygame.display.set_caption("Titre de la fenêtre")
  9. pygame.font.init()
  10. fonte = pygame.font.Font(None, 30)
  11.  
  12.  
  13.  

La fenêtre s'affiche mais disparait aussitôt. il faut créer une boucle pour la garder ouverte et rafraichir l'affichage dans la fenêtre :

pygame_fenetre_boucle.py

  1. import pygame
  2. from pygame.locals import *
  3.  
  4. HAUTEUR_FENETRE = 480
  5. LARGEUR_FENETRE = 640
  6.  
  7. fenetre = pygame.display.set_mode( (LARGEUR_FENETRE, HAUTEUR_FENETRE) )
  8. pygame.display.set_caption("Titre de la fenêtre")
  9. pygame.font.init()
  10. fonte = pygame.font.Font(None, 30)
  11.  
  12. while True:
  13.   pygame.display.update()
  14.  
  15.  
  16.  

L'écran reste noir et rien ne s'affiche. Dans la boucle while il faut réaliser une série d'actions : afficher des images, du texte, etc.

pygame_fenetre_balle.py

  1. import pygame
  2. from pygame.locals import *
  3.  
  4. HAUTEUR_FENETRE = 480
  5. LARGEUR_FENETRE = 640
  6.  
  7. fenetre = pygame.display.set_mode( (LARGEUR_FENETRE, HAUTEUR_FENETRE) )
  8. pygame.display.set_caption("Titre de la fenêtre")
  9. pygame.font.init()
  10. fonte = pygame.font.Font(None, 30)
  11.  
  12. x = LARGEUR_FENETRE // 2
  13. y = HAUTEUR_FENETRE // 2
  14. rayon = 10
  15. while True:
  16.   # dessine la balle
  17.   pygame.draw.circle(fenetre, (255,255,255), (x, y), rayon)
  18.   pygame.display.update()
  19.  
  20.   # attente du clic souris sur x pour fermer la fenêtre
  21.   # ou Alt+F4 sous Linux
  22.   for event in pygame.event.get():
  23.     if event.type == pygame.QUIT:
  24.       pygame.quit()
  25.       exit()
  26.  
  27.  

Enfin il est nécessaire de déplacer les éléments dans la fenêtre :

pygame_fenetre_balle_deplace_evenement.py

  1. import pygame
  2. from pygame.locals import *
  3.  
  4. HAUTEUR_FENETRE = 480
  5. LARGEUR_FENETRE = 640
  6.  
  7. fenetre = pygame.display.set_mode( (LARGEUR_FENETRE, HAUTEUR_FENETRE) )
  8. pygame.display.set_caption("Titre de la fenêtre")
  9. pygame.font.init()
  10. fonte = pygame.font.Font(None, 30)
  11.  
  12. # données de la balle
  13. x = LARGEUR_FENETRE // 2
  14. y = HAUTEUR_FENETRE // 2
  15. rayon = 10
  16. # vecteur de déplacement
  17. vx = 1
  18. vy = 1
  19. while True:
  20.   # temporise
  21.   pygame.time.delay(15)
  22.  
  23.   # dessine le fond de l'écran en noir
  24.   fenetre.fill( (0,0,0) )
  25.  
  26.   # dessine la balle
  27.   pygame.draw.circle(fenetre, (255,255,255), (x, y), rayon)
  28.  
  29.   # nouvelle position
  30.   x = x + vx
  31.   y = y + vy
  32.  
  33.   if x >= LARGEUR_FENETRE or x <= 0:
  34.     vx = -vx
  35.  
  36.   if y >= HAUTEUR_FENETRE or y <= 0:
  37.     vy = -vy 
  38.  
  39.   pygame.display.update()
  40.  
  41.  
  42.  

Exercice 7.1

Améliorer la gestion du rebond car la balle sort de l'écran.

7.3.2. Gestion des événements

Afin de gérer les évènements (appui sur une touche, clic souris, etc), il faut énumérer chacun des événements reçus et vérifier celui qui nous intéresse.

Sur l'exemple suivant on test l'appui sur la touche 'A', un clic souris ou le fait de quitter la fenêtre en cliquant sur l'icone correspondante en haut de la fenêtre.

pygame_fenetre_balle_deplace.py

  1. import pygame
  2. import sys
  3. from pygame.locals import *
  4.  
  5. HAUTEUR_FENETRE = 480
  6. LARGEUR_FENETRE = 640
  7.  
  8. fenetre = pygame.display.set_mode( (LARGEUR_FENETRE, HAUTEUR_FENETRE) )
  9. pygame.display.set_caption("Titre de la fenêtre")
  10. pygame.font.init()
  11. fonte = pygame.font.Font(None, 30)
  12.  
  13. # données de la balle
  14. x = LARGEUR_FENETRE // 2
  15. y = HAUTEUR_FENETRE // 2
  16. rayon = 10
  17. # vecteur de déplacement
  18. vx = 1
  19. vy = 1
  20. while True:
  21.   # temporise
  22.   pygame.time.delay(15)
  23.  
  24.   # dessine le fond de l'écran en noir
  25.   fenetre.fill( (0,0,0) )
  26.  
  27.   # dessine la balle
  28.   pygame.draw.circle(fenetre, (255,255,255), (x, y), rayon)
  29.  
  30.   # nouvelle position
  31.   x = x + vx
  32.   y = y + vy
  33.  
  34.   if x >= LARGEUR_FENETRE or x <= 0:
  35.     vx = -vx
  36.  
  37.   if y >= HAUTEUR_FENETRE or y <= 0:
  38.     vy = -vy 
  39.  
  40.   for event in pygame.event.get():
  41.     if event.type == KEYDOWN:
  42.       if event.key == pygame.K_a:
  43.         vx = -vx
  44.     if event.type == QUIT:
  45.       pygame.quit()
  46.       sys.exit()
  47.     if event.type == MOUSEBUTTONDOWN:
  48.       vy = -vy
  49.    
  50.   pygame.display.update()
  51.  
  52.  
  53.  

7.4. Le jeu de tennis (à la Pong)

Pour la création du jeu de tennis, nous allons organiser notre code en plusieurs fichiers :

7.4.1. Les packages utilisés

On commence par introduire les packages (bibliothèques) qui seront utilisées :

  1. # paquet pygame pour gestion des graphismes et des sons
  2. import pygame
  3. # constantes de pygame pour le clavier
  4. from pygame.locals import *
  5. # paquet system pour quitter le jeu
  6. import sys
  7. # paquet random pour générer une position aléatoire de la balle au début
  8. import random
  9.  
  10.  

7.4.2. Les constantes du jeu

On définit la hauteur et la largeur de la fenêtre ainsi que l'abscisse du mur qui sera situé à droite.

On définit également un jeu de couleurs qui utilisent des nuances proches de celles du tournoi du grand chelem de Roland Garros.

  1. # dimensions de la fenêtre
  2. LARGEUR_FENETRE = 640
  3. HAUTEUR_FENETRE = 480
  4.  
  5. # abscisse pour le mur
  6. COORD_X_MUR = LARGEUR_FENETRE-20
  7.  
  8. # couleurs
  9. BLANC_ROLAND_GARROS = 0
  10. NOIR = 1
  11. ORANGE_ROLAND_GARROS = 2
  12. VERT_ROLAND_GARROS = 3
  13.  
  14. couleurs = [ (217,223,226), (0,0,0), (0xc8, 0x62, 0x2f), (0x22, 0x3f, 0x34) ]
  15.  

7.4.3. La classe Balle

La balle se déplace dans l'aire de jeu et rebondit sur les bords haut, bas et droit de la fenêtre. Par contre sur la gauche elle doit rebondir sur la raquette pour que le jeu continue.

On crée une classe Balle que l'on instanciera par la suite. La classe est une structure de donnée et on pourra créer autant de balles que nécessaire par la suite mais dans le jeu qui nous intéresse il n'y a qu'une seule balle.

Pour la balle, on doit définir les méthodes suivantes :

On notera l'utilisation du mot-clé self qui permet de faire référence aux variables et méthodes de la classe.

balle.py

  1. import random
  2. import pygame
  3. from constantes import *
  4.  
  5. """
  6.   classe Balle
  7.  
  8.   une balle est définie par les attributs (champs) suivants :
  9.   - sa position x, y dans la fenêtre en pixels
  10.   - son rayon en pixel
  11.   - son vecteur de déplacement (dx, dy) en pixels
  12.   - une référence à la fenêtre du jeu pour pouvoir dessiner la balle
  13.  
  14. """
  15. class Balle(object):
  16.  
  17.   """
  18.     initialisation de la balle
  19.   """
  20.   def __init__(self, fenetre):
  21.     self.x = 100
  22.     self.y = random.randint(20, HAUTEUR_FENETRE-20)
  23.     self.rayon = 10
  24.     self.dx = 1
  25.     self.dy = 1
  26.     self.fenetre = fenetre
  27.  
  28.   """
  29.     dessine la balle sous forme d'un cercle dans la fenêtre graphique
  30.     avec une couleur blanche
  31.   """
  32.   def dessine(self):
  33.     pygame.draw.circle(self.fenetre, couleurs[BLANC_ROLAND_GARROS], \
  34.       (self.x, self.y), self.rayon)
  35.  
  36.   """
  37.     déplace la balle en gérant les rebonds
  38.   """
  39.   def deplace(self):
  40.     self.x += self.dx
  41.     self.y += self.dy
  42.     # rebond sur le mur à droite
  43.     # TODO
  44.     # rebond sur les bords haut et bas
  45.     # TODO
  46.      
  47.   """
  48.     rebond sur l'axe des x
  49.   """
  50.   def rebond_x(self):
  51.     self.dx = -self.dx
  52.  
  53.   """
  54.     rebond sur l'axe des y
  55.   """
  56.   def rebond_y(self):
  57.     self.dy = -self.dy
  58.        
  59.   """
  60.     indique si la balle est en dehors de l'aire de jeu
  61.   """
  62.   def perdue(self):
  63.     return self.x < 0
  64.      
  65.  
  66.  

7.4.4. La classe Raquette

La raquette se déplace verticalement sur le bord gauche. On créera une instance de la raquette.

Pour la raquette, on doit définir les méthodes suivantes :

raquette.py

  1. import random
  2. import pygame
  3. from constantes import *
  4.  
  5. """
  6.   classe raquette
  7.  
  8.   une raquette est définie par les attributs (champs) suivants :
  9.   - sa position x, y dans la fenêtre en pixels, sachant que x est fixé
  10.   - sa largeur et sa hauteur en pixels
  11.   - son vecteur de déplacement en dy en pixels
  12.   - une référence à la fenêtre du jeu pour pouvoir dessiner la balle
  13. """
  14. class Raquette(object):
  15.  
  16.   """
  17.     initialisation de la raquette
  18.   """
  19.   def __init__(self, fenetre):
  20.     self.hauteur = 80
  21.     self.largeur = 20
  22.     self.x = 20
  23.     self.y = (HAUTEUR_FENETRE - self.hauteur) // 2
  24.     self.dy = 2
  25.     self.fenetre = fenetre
  26.    
  27.   """
  28.     dessine la raquette dans la fenêtre graphique 
  29.     en blanc sous forme d'un rectangle
  30.   """
  31.   def dessine(self):
  32.     pygame.draw.rect(self.fenetre, couleurs[BLANC_ROLAND_GARROS], \
  33.       (self.x, self.y, self.largeur, self.hauteur), 0)
  34.  
  35.   """
  36.     déplace la raquette suivant le vecteur de déplacement en inversant la direction
  37.     si on arrive en haut ou en bas de la fenêtre
  38.   """
  39.   def deplace(self):
  40.     self.y += self.dy
  41.     # si la raquette est en haut on change la direction du déplacement
  42.     # TODO
  43.     # si la raquette est en bas on change la direction du déplacement
  44.     # TODO
  45.  
  46.   """
  47.     modifie le vecteur de déplacement pour que la raquette aille vers le haut
  48.   """
  49.   def vers_le_haut(self):
  50.     self.dy = -1
  51.  
  52.   """
  53.     modifie le vecteur de déplacement pour que la raquette aille vers le bas
  54.   """
  55.   def vers_le_bas(self):
  56.     self.dy = +1
  57.  
  58.   """
  59.     vérifie s'il y a contact entre la raquette et la balle
  60.   """
  61.   def contact(self, balle):
  62.     # TODO
  63.    
  64.  
  65.  

7.4.5. Le jeu

On crée la fenêtre de jeu, on utilise la fonte courante et on charge les sons qui seront utilisé pour le jeu :

Le jeu se compose de trois parties :

tennis.py

  1. ###########################################################
  2. # librairies
  3. ###########################################################
  4. import pygame
  5. from pygame.locals import *
  6. import sys
  7. import random
  8. from constantes import *
  9. import balle
  10. import raquette
  11.  
  12. ###########################################################
  13. # fonctions utiles
  14. ###########################################################
  15.  
  16. """
  17.   afficher un texte au centre de l'écran
  18.  
  19.   si la coordonnée y est égale à -1, on place le texte
  20.   au milieu de la fenetred'affichage
  21.  
  22.   on peut également spécifier la couleur qui est l'orange
  23.   par défaut
  24.  
  25. """
  26. def affiche_texte_centre(texte, y=-1, couleur=None):
  27.   if couleur == None:
  28.     couleur = couleurs[ ORANGE_ROLAND_GARROS ]
  29.   rendu = fonte.render(texte, True, couleur)
  30.   rectangle = rendu.get_rect()
  31.   if y == -1:
  32.     rectangle.center = ((LARGEUR_FENETRE) / 2 , (HAUTEUR_FENETRE) / 2)
  33.   else:
  34.     rectangle.center = ((LARGEUR_FENETRE) / 2 , y)
  35.   fenetre.blit(rendu, rectangle)
  36.  
  37.  
  38. """
  39.   afficher un texte au centre de l'écran en précisant
  40.   les coordonnées x et y
  41.  
  42.   on peut également spécifier la couleur qui est l'orange
  43.   par défaut
  44.  
  45. """
  46. def affiche_texte(texte, x, y, couleur=None):
  47.   if couleur == None:
  48.     couleur = couleurs[ ORANGE_ROLAND_GARROS ]
  49.   rendu = fonte.render(texte, True, couleur)
  50.   rectangle = rendu.get_rect()
  51.   rectangle.center = (x, y)
  52.   fenetre.blit(rendu, rectangle)
  53.  
  54. """
  55.   quitter le programme
  56.  
  57.   on arrête la musique d'ambiance et on libère les resources
  58.   utilisées par pygame
  59.  
  60. """
  61. def quitter():
  62.   pygame.mixer.music.stop()
  63.   pygame.quit()
  64.   sys.exit()
  65.  
  66. """
  67.   attend qu'on tape une touche au clavier
  68.  
  69.   on parcourt les événements (clavier, souris) mais en ne
  70.   traitant que l'appui sur une touche (KEYDOWN)
  71. """
  72. def touche_clavier():
  73.   for event in pygame.event.get():
  74.     if event.type == KEYDOWN:
  75.       # Ctrl-C pour quitter le jeu
  76.       if event.key == pygame.K_c and pygame.key.get_mods() & pygame.KMOD_CTRL:
  77.         quitter()
  78.       # retourner la touche pressée 
  79.       return event.key
  80.     # sinon, ne rien retourner (valeur nulle)
  81.     return None    
  82.        
  83. """
  84.   on attend que l'utilisateur tape une touche du clavier avant
  85.   d'aller plus loin. A chaque itération on redessine la fenêtre
  86. """      
  87. def attente():
  88.   while touche_clavier() == None:
  89.     pygame.display.update()
  90.      
  91.  
  92.  
  93. ###########################################################
  94. # initialisation du jeu
  95. ###########################################################
  96. pygame.init()
  97. fenetre = pygame.display.set_mode((LARGEUR_FENETRE, HAUTEUR_FENETRE))
  98. pygame.display.set_caption("Tennis")
  99. pygame.font.init()
  100. fonte = pygame.font.Font(None, 30) 
  101. crash_sound = pygame.mixer.Sound("glass_break.wav")
  102. pygame.mixer.music.load("music.mp3")
  103. pygame.mixer.music.play()
  104.  
  105. ###########################################################
  106. # définition des variables du jeu
  107. ###########################################################
  108.  
  109. balle = balle.Balle(fenetre)
  110.  
  111. raquette = raquette.Raquette(fenetre)
  112.  
  113. # nombre de rebonds sur la raquette
  114. score = 0
  115.  
  116. # variable booléenne à vrai si on est en pause
  117. pause = False
  118.  
  119.  
  120. ###########################################################
  121. # écran d'accueil
  122. ###########################################################
  123. fenetre.fill(couleurs[NOIR])
  124. affiche_texte_centre("Appuyez sur une touche pour commencer", 100)
  125. affiche_texte_centre("Flèche haut pour faire monter la raquette", 140)
  126. affiche_texte_centre("Flèche bas pour faire descendre la raquette", 170)
  127. affiche_texte_centre("p pour pause", 200)
  128. affiche_texte_centre("Ctrl-C pour quitter", 230)
  129. attente()
  130.      
  131. ###########################################################
  132. # écran de jeu : boucle principale
  133. ###########################################################
  134. while True:
  135.   # temporisation : arrêt de l'exécution du programme pendant 5 ms
  136.   pygame.time.delay(5)
  137.  
  138.   # -------------------------------
  139.   # dessine les différents éléments
  140.   # - fond d'écran
  141.   # - mur de droite
  142.   # - balle
  143.   # - raquette
  144.   # - score
  145.   # -------------------------------
  146.  
  147.   # dessine le fond en orange
  148.   fenetre.fill( couleurs[ ORANGE_ROLAND_GARROS ] )
  149.   # dessine le mur de droite
  150.   pygame.draw.rect(fenetre, couleurs[ VERT_ROLAND_GARROS ], (COORD_X_MUR, 0, 20, HAUTEUR_FENETRE), 0)
  151.  
  152.   balle.dessine()
  153.  
  154.   raquette.dessine()
  155.  
  156.   # affichage du score
  157.   texte = "Score = {}".format(score)
  158.   affiche_texte_centre(texte, 20, couleurs[VERT_ROLAND_GARROS])
  159.  
  160.   pygame.display.update()
  161.  
  162.   # -------------------------------
  163.   # gestion des événements clavier
  164.   # - flèche vers le haut
  165.   # - flèche vers le bas
  166.   # - 'p' pour pause
  167.   # -------------------------------
  168.  
  169.   clavier = touche_clavier()
  170.   if clavier == K_UP:
  171.     raquette.vers_le_haut()
  172.   if clavier == K_DOWN:
  173.     raquette.vers_le_bas() 
  174.   if clavier == K_p:
  175.     pause = not pause
  176.  
  177.   # si on a appuyé sur 'p' et qu'on met le jeu en pause
  178.   # il ne faut pas aller plus loin c'est à dire qu'il ne
  179.   # faut pas déplacer ni la raquette, ni la balle
  180.   if pause:
  181.     continue
  182.  
  183.   # ------------------------------
  184.   # déplacement de la raquette et de la balle
  185.   # vérification de fin de jeu
  186.   # ------------------------------
  187.    
  188.   raquette.deplace()
  189.    
  190.   balle.deplace()
  191.  
  192.   # si balle en dehors de l'aire de jeu, on quitte la boucle
  193.   # de jeu
  194.   # TODO
  195.  
  196.   # si la balle touche la raquette, on réalise le rebond
  197.   # TODO   
  198.  
  199. ################################
  200. # écran de fin de partie
  201. ################################
  202.  
  203. fenetre.fill( couleurs[NOIR] )
  204. texte = "Votre score est de {} points".format(score)
  205. affiche_texte_centre(texte)
  206. affiche_texte_centre("Appuyez sur une touche pour terminer", 200)
  207. pygame.mixer.Sound.play(crash_sound)
  208. pygame.mixer.music.stop()
  209. attente()
  210.  
  211. quitter()
  212.  
  213.  
  214.  

7.5. Multi Pong

Exercice 7.2

A titre d'exercice vous pouvez modifier le jeu de manière à lancer plusieurs balles au début du jeu. Il suffit de créer une liste de balles et de travailler avec cette liste plutôt qu'avec une seule balle.

Exercice 7.3

On peut également faire en sorte qu'à mesure que le temps passe :

  • les balles se déplacent plus vite
  • la hauteur de la raquette diminue