Programmation orientée objet

Pygame

Pygame est un module de Python permettant de manipuler des images et des sons dans le but de réaliser un jeu

Par exemple tester celui-ci

    
import pygame.examples.aliens
pygame.examples.aliens.main()

A travers des exemples nous allons découvrir les classes de Pygame

  1. Animation 1

    Le premier programme crée une fenêtre graphique de 400 x 200 ayant un fonds noir

    Cependant on ne peut pas fermer la fenêtre

    
    #importation de librairies
    import pygame
    
    #-----------------Constantes-----------------------------------------
    LARGEUR    = 400
    HAUTEUR    = 200
    NOIR       = (0,0,0)
    
    #Initialisation de pygame
    pygame.init()
    
    #Création de la fenetre graphique
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation0")
    fenetre.fill(NOIR)
        
    

    Explications

    • En premier on importe pygame avec import pygame
    • Ensuite on initialise toutes les classes de Pygame
    • Ensuite on crée la fenêtre graphique

    Exercice

    1. Commentez pygame.init() et observez que le programme fonctionne
    2. Observez qu'on ne peut pas fermer la fenêtre et que Pygame est toujours actif

      Ajouter à la fin quit(). Que se passe-t-il ?

  2. Animation 2

    On veut quitter proprement Pygame et pouvoir fermer la fenêtre.On considère le fait de vouloir fermer la fenêtre comme un évènement à capturer

        
    #importation de librairies
    import pygame
    
    #-----------------Constantes-----------------------------------------
    LARGEUR    = 400
    HAUTEUR    = 200
    NOIR       = (0,0,0)
    
    #Initialisation de pygame
    pygame.init()
    
    #Création de la fenetre graphique
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation0")
    fenetre.fill(NOIR)
    
    #Boucle infinie
    while True:
         
        #capture et gestion des évènements
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
            
            

    Exercice

    1. Essayer en fermant la fenêtre en cliquant sur la croix
    2. Essayer en fermant la fenêtre avec ALT + F4 (Windows et Linux) ou Cmd + W (mac)
  3. Animation 3

    On veut dessiner un carré rouge dans la fenêtre

    
    # Ce programme dessine un carré rouge dans une
    # fenêtre ayant un fonds noir
    
    #importation de librairies
    import pygame
    
    #-----------------Constantes-----------------------------------------
    LARGEUR    = 400
    HAUTEUR    = 200
    COTE_CARRE = 50
    NOIR       = (0,0,0)
    RED        = (255,0,0)
    
    #Initialisation de pygame
    pygame.init()
    
    #Initialisation de la fenetre graphique
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation1")
    fenetre.fill(NOIR)
    
    #Creation du coin supérieur gauche qui repère le
    #carré
    csg_x = LARGEUR//2
    csg_y = HAUTEUR//2
    
    #Dessiner le carré et mettre à jour l'affichage
    pygame.draw.rect(fenetre, RED, (csg_x, csg_y, COTE_CARRE, COTE_CARRE))
    pygame.display.update()
    #pygame.display.flip()
    
    #boucle infinie
    while True:
    
        #Gestion des évènements
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
            
            

    S'assurer d'avoir bien compris le repérage et comment est défini un rectangle

    Exercice

    Modifier le programme pour avoir 4 carrés rouges 50x50 aux quatre coins de la fenêtre 400x200

  4. Animation 4

    On veut créer l'illusion du déplacement du carré rouge de la droite vers la gauche

    Comme le carré est défini par son coin supérieur gauche on va déplacer le coin supérieur gauche régulièrement suivant un "métronome"

    Il faut donc déplacer le dessin du carré et la mise à jour de la fenêtre dans la boucle infinie

    
    #  Ce programme dessine un carré rouge qui
    #  se déplace de la gauche vers la droite dans une
    #  fenêtre ayant un fond noir
    
    #importation des librairies
    import pygame
    
    #--------------------Constantes-------------------------------------
    LARGEUR    = 400
    HAUTEUR    = 200
    COTE_CARRE = 50
    DELTA_X      = 5
    DELTA_Y      = 5
    NOIR       = (0,0,0)
    ROUGE        = (255,0,0)
    
    #Initialisation de la fenetre graphique
    pygame.init()
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation2")
    
    #Création du coin supérieur gauche du carré
    
    csg_x = LARGEUR//2
    csg_y = HAUTEUR//2
    
    #Vecteur translation
    vecteur = [DELTA_X,0]
    
    #Gestion du rafraichissement de la fenetre
    frequence = pygame.time.Clock()
    
    #boucle infinie
    while True:
    
    #réception des  évènements
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
    	        quit()
    
    #mettre a jour les coordonnees du csg
        csg_x = csg_x + vecteur[0]
        csg_y = csg_y + vecteur[1]
    
    #peindre la fenêtre en noir
        fenetre.fill(NOIR)
    
    #dessiner le carré
        pygame.draw.rect(fenetre, ROUGE, (csg_x, csg_y, COTE_CARRE, COTE_CARRE))
    
    # Frequence de rafraichisement de la fenêtre
        frequence.tick(10)
    
    #mettre a jour la fenêtre
        pygame.display.update()
        #pygame.display.flip()
            
            

    Exercice

    1. Changer les valeurs de la fréquence de rafraîchissement et les valeurs du vecteur de translation
    2. Sortir l'instruction frequence.tick(10) de la boucle infinie Que se passe-t-il ?
    3. Sortir l'instruction fenetre.fill(NOIR) de la boucle infinie Que se passe-t-il ?
    4. Laisser l'instruction fenetre.fill(NOIR) à l'extérieur de la boucle et donner au vecteur de translation la valeur (100,0) Que se passe-t-il ?
    5. Modifier le programme pour que le carré réapparraisse du côté opposé au côté de la fenêtre où il a disparu
  5. Animation 5

    On veut insérer une image dans la fenêtre graphique

    Télécharger l'image ici

    
    # On charge une image png 99x96
    #L'image est repérée par son CSG
    import pygame
    
    #------------------------Constantes-----------------------------------
    LARGEUR = 800
    HAUTEUR = 400
    NOIR = (0,0,0)
    
    #Initialisation de pygame
    pygame.init()
    
    #Création de la fenetre graphique
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation5")
    
    #chargement de l'image en mémoire vive
    image_Balle  = pygame.image.load("SoccerBall.png")
    
    #peindre la fenêtre en noir
    fenetre.fill(NOIR)
    
    #intégration de l'image à la fenêtre
    fenetre.blit(image_Balle,(0,0))
    
    #mettre a jour la fenetre
    pygame.display.update()
    
    #boucle infinie
    while True:
    
    #receptionnaire evenements
        for event in pygame.event.get():
    	    if event.type == pygame.QUIT:
    	        quit()
            
            

    La méthode pygame.image.load("SoccerBall.png") permet de charger l'image en mémoire vive (il faut faire attention à la place relative de l'image par rapport au programme Python)

    L'instruction fenetre.blit(image_Balle,(0,0)) intégre l'image à la fenêtre

    Le coin supérieur gauche de l'image est placé en (0,0) de la fenêtre graphique

    Exercice

    Sachant que l'image est de 99x96, placer l'image dans le coin inférieur droit de la fenêtre

  6. Animation 6

    On veut créer l'illusion que la balle rebondit dans la fenêtre

    
    # On charge une image png 99x96
    # L'image est repérée par son CSG
    import pygame
    
    LARGEUR = 800
    HAUTEUR = 500
    NOIR = (0,0,0)
    LARGEUR_IMAGE = 99
    HAUTEUR_IMAGE = 96
    
    #csg de l'image
    csg = [(LARGEUR-LARGEUR_IMAGE)//2 ,(HAUTEUR-HAUTEUR_IMAGE)//2 ]
    
    #vecteur déplacement de l'image
    vecteur = [1,0]
    
    def touche_bords_lateraux()->bool:
        """
        renvoie Vrai si l'image touche les bords latéraux de 
        la fenêtre graphique
        """
        return csg[0] > LARGEUR - LARGEUR_IMAGE or csg[0]< 0
    
    def touche_bords_transversaux()->bool:
        """
        renvoie Vrai si l'image touche les bords transversaux de 
        la fenêtre graphique
        """
        pass 
    
    def mettre_a_jour_csg():
        """
        csg = topleft = coin supérieur gauche
        """
        if touche_bords_lateraux():
            vecteur[0] *= -1
        elif touche_bords_transversaux():
            pass
        csg[0]  += vecteur[0]
        csg[1]  += vecteur[1]
    
    #Initialisation de pygame
    pygame.init()
    
    #Initialisation de la fenetre graphique
    fenetre = pygame.display.set_mode((LARGEUR,HAUTEUR))
    pygame.display.set_caption("Animation5")
    fonds = pygame.Surface((LARGEUR,HAUTEUR))
    fonds.fill(NOIR)
    fenetre.blit(fonds,(0,0))
    
    
    #Gestion du rafraichissement de la fenetre
    frequence = pygame.time.Clock()
    
    #chargement de l'image en mémoire vive
    image_balle  = pygame.image.load("SoccerBall.png").convert()
    
    #récupérer le rectangle entourant l'image
    rect_balle = image_balle.get_rect(topleft = csg)
    
    #intégration de l'image à la fenêtre à la position du  csg
    fenetre.blit(image_balle,rect_balle) 
       
    #boucle infinie
    while True:
    
        #receptionnaire evenements
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
        
        #effacer l'image à son ancienne position (csg de rect_balle, deuxième
        #argument,en intégrant fonds dans la fenêtre juste sur l'aire de 
        #rect_balle troisième argument)
        #fenetre.blit(fonds,rect_balle,rect_balle)
        
        #mise à jour du csg de l'image       
        mettre_a_jour_csg()
        
        #mettre à jour le rectangle entourant l'image
        rect_balle.update(csg[0],csg[1],LARGEUR_IMAGE,HAUTEUR_IMAGE)
        
        #intégration de l'image à la fenêtre à la position du nouveau csg
        fenetre.blit(image_balle,rect_balle)
        
        #mettre a jour la fenetre
        pygame.display.update(rect_balle)
       
        # Frequence de rafraichisement de la fenêtre
        frequence.tick(60)
            
            

    Exercice

    1. Augmenter la vitesse par exemple en prenant vecteur = [5,0]. Qu'observez vous ?
    2. Pour remédier à ce problème décommentez l'instruction servant à effacer l'ancienne "image".

      Que signifie fenetre.blit(fonds,rect_balle,rect_balle) ?

    3. Définir la fonction touche_bords_transversaux(), puis compléter la fonction mise_au_point_csg() et enfin changer la valeur du Vecteur par [-2,2] par exemple

Héritage en programmation objet

En programmation on évite d'écrire plusieurs fois la même chose, on dit que l'on factorise le code

Dans le jeu Asteroides, le vaisseau spatial, les asteroides et les missiles sont à peu près les mêmes "objets"

Autant donc définir une classe "commune" que l'on pourrait appeler Animation , car après tout ces objets sont avant tout des images "intégrées" à la fenêtre graphique à un endroit qui évolue avec le temps

Ensuite les classes Vaisseau, Asteroide, Missile seront des classes dérivées ou enfants de la classe Animation

On dit aussi que les classes Vaisseau, Asteroide, Missile héritent de la classe Animation

        
class Animation:
    def __init__(self, image, position:tuple, vitesse:tuple):
        #image est de type Surface
        self.image = image
        #rayon du cercle circonscrit à l'image
        self.rayon = self.image.get_width()/2
        #position du csg de l'image dans le repère de la fenêtre
        self.position = Vector2(position)
        #rectangle (de type Rect) entourant l'image
        self.rectangle = pygame.Rect(self.position.x,self.position.y,self.rayon,self.rayon)
        #centre de l'image
        self.centre = self.position + Vector2(self.rayon)
        #vitesse de déplacement de l'Animation
        self.vitesse = Vector2(vitesse)

    def deplacer(self):
        self.rectangle.move_ip(self.vitesse.x,self.vitesse.y)
        #mise à jour de la position
        self.position = self.rectangle.topleft

    def dessiner(self,fenetre,aire=None):
        fenetre.blit(self.image, self.position,aire)

    def entrer_en_collision_avec(self, other):
        distance = self.centre.distance_to(other.centre)
        return distance < self.rayon + other.rayon
    
    

Explications

Une classe Date

Définir une classe Date avec trois attributs:

Lorsqu'un attribut concerne tous les objets d'une même classe, on parle d'attribut de classe

Ainsi pour la lisibilité des objets on va introduire un attribut de classe à la classe Date, sous la forme d'une liste nom_mois, permettant l'affichage d'une date sous la forme "15 septembre 2021" plutôt que 15/09/21


class Date:
    nom_mois = ['#', 'janvier','février',...]
    #à compléter
    def __init__(self,j,m,a):
        pass

Cet attribut de classe peut être utilisé sous la forme Date.nom_mois

Avec les méthodes suivantes :

On peut vouloir comparer des dates. Python propose une méthode particulière à définir pour cela __lt__(self,other) qui renvoie Vrai si l'objet en cours de construction self est strictement plus petit que la date other

Compléter le code suivant et tester le


def __lt__(self,other):
    return self.annee < other.annee or\
    (self.annee == other.annee and self.mois < other.mois) or\
    #... à compléter

Il existe aussi des méthodes qui ne s'appliquent pas à un objet en particulier mais servent de fonction d'aide pour tous les objets définis par la classe

On peut soit définir cette fonction à l'extérieur de la classe et l'appeler à l'intérieur de la classe où l'insérer dans la classe sans self dans les paramètres

Compléter le code suivant


def max(date1,date2):
    if date1.annee > date2.annee :
        return date1
    elif date2.annee > date1.annee :
        return date2
    elif date1.mois > date1.mois :
        # à compléter

Si on appelle cette méthode de classe à l'extérieur de la classe on procède ainsi par exemple


date1 = Date(12,09,2021)
date2 = Date(12,03,2022)
print(Date.max(date1,date2))

Gestion des erreurs

A quel moment contrôle-t-on la validité des données?

La classe Date telle que l'on a écrit précedemment suppose que l'on a "filtré" les dates avant de les insérer dans le constructeur

Si on a oublié de le faire on peut se retrouver avec des dates comme 32/15/2021 !

Pire les valeurs ne sont peut être pas entières!

Nous sommes habitués aux messages d'erreur dans la console

S'il y a plusieurs erreurs dans un programme, une erreur de syntaxe ou une erreur de calcul, le programme s'arrête à la première erreur rencontrée

Dans le langage Python un certain nombre d'erreurs est déjà répertorié comme par exemple une division par zéro

Dans notre cas nous devons définir ce qui est invalide et sera considéré comme une erreur qui arrêtera le programme dès son interception

Dans un premier temps on va définir une méthode de classe jour_est_valide(j) où on teste si le jour est de type entier et est compris entre 1 et 31, si ce n'est pas le cas on lève une erreur qui est un objet de type ValueError avec un message approprié "le jour "+ str(j) + " est invalide" passé en paramètre du constructeur de l'objet ValueError


class Date2:
    #attribut de classe
    nom_mois = ["#",'janvier','fevrier','mars','avril','mai',
    'juin','juillet','août','septembre','octobre','novembre','décembre']
    
    #méthodes de classe
    def jour_est_valide(j):
        return isinstance(j,int) and (1 <=  j <= 31)                

Ensuite dans le constructeur on écrit

def __init__(self,j,m,a):
        if Date2.jour_est_valide(j):
            self.jour = j
        else:
            raise ValueError("le jour "+ str(j) + " est invalide")

Compléter le code pour tester le mois et l'année puis ensuite réaliser les tests suivants


date1 = Date2((2,1),1,2022)
date2 = Date2("Lundi",1,2022)
date3 = Date2("Lundi",15,2022)

On observe que le programme s'arrête dès la première erreur car le jour est de type tuple au lieu de int, les deux dernières instructions ne sont pas exécutées

Pour que le programme soit exécuté dans son intégralité malgré les erreurs rencontrées on encadre les sources d'erreurs possibles par le bloc try ...except dans le constructeur


class Date3:
    #attribut de classe
    nom_mois = ["#",'janvier','fevrier','mars','avril','mai',
    'juin','juillet','août','septembre','octobre','novembre','décembre']
    
    #méthode de classe
    def jour_est_valide(j):
        return isinstance(j,int) and (1 <=  j <= 31)
        
    def __init__(self,j,m,a):
        try:
            if Date3.jour_est_valide(j):
                self.jour =  j
            else:
                raise ValueError
            
        except:
            print("le jour "+ str(j) + " est invalide")
    

Compléter le constructeur et réaliser les mêmes tests que précédemment et observer que cette fois ci le programme ne s'arrête pas et délivre tous les messages d'erreur d'un coup

Trier des fichiers

Un système d'exploitation propose de trier les fichiers contenus dans un répertoire par différentes entrées possibles(en simplifiant):

Définir une classe Fichier, les attributs sont nom de type str, date de type Date (voir plus haut) et taille de type int

On commence par définir une méthode __lt__(self,other) qui retourne vrai si un fichier est strictement plus petit qu'un autre suivant la date d'ajout

On rappelle qu'en Première on a implémenté le tri par insertion pour des entiers ou des chaînes de caractères


def insertion(T,i):
    """
    La liste T contient des entiers
    ou des chaines de caractères
    est triée de l'indice 0
    jusqu'à l'indice i - 1 
    i >= 1
    On insère T[i] à sa place et à la fin 
    La liste T est triée de l'indice 0
    jusqu'à l'indice i 
    """    
    
    temp = T[i]
    j = i
    while j > 0 and T[j-1] > temp:
        T[j] = T[j-1]
        j -= 1
    T[j] = temp

def tri_insertion(T):
    """
    tri par insertion la liste T 
    contenant des nombres ou des chaines de caractères
    """
    for i in range(1,len(T)):
        insertion(T,i)

nous allons l'adapter pour des fichiers avec le comparateur défini ci-dessus


def insertion(T,i):
    """
    La liste T contient des fichiers
    comparables suivant leur date d'ajout
    est triée de l'indice 0
    jusqu'à l'indice i - 1 
    i >= 1
    On insère T[i] à sa place et à la fin 
    La liste T est triée de l'indice 0
    jusqu'à l'indice i 
    """    
    
    temp = T[i]
    j = i
    #on peut utiliser > à cause de __lt__()
    while j > 0 and T[j-1] > temp:
        T[j] = T[j-1]
        j -= 1
    T[j] = temp

def tri_insertion(T):
    """
    tri par insertion la liste T 
    contenant des nombres ou des chaines de caractères
    """
    for i in range(1,len(T)):
        insertion(T,i)

On n'a résolu que partiellement le problème car on ne peut pas utiliser la fonction tri_insertion(T) pour trier les fichiers suivant leur taille ou leur nom

Pour plus de généralité on va donc créer un objet Comparateur ayant un attribut nom, valant soit 'taille', soit 'nom' et soit 'date'

Cet objet n'a qu'une seule méthode plus_petit(self,x,y) qui en fonction du nom du comparateur compare les fichiers x et y avec l'opérateur <

Ce qui suppose que pour un attribut d'un autre type que int,float ou str la méthode __lt__() ait été implémentée pour donner un sens à l'opérateur <


class Comparateur:
    
    def __init__(self,nom):
        self.nom = nom
    
    def plus_petit(self,x,y):
        if self.nom== 'taille':
            return x.taille < y.taille
        if self.nom == 'date':
            return x.date < y.date
        if self.nom == 'nom':
            return x.nom < y.nom

Maintenant on peut trier les fichiers suivant n'importe quel attribut du fichier en ajoutant comme paramètre à insertion(T,i) et tri_insertion(T) un comparateur comp


def insertion(T,i,comp):
    """
    La liste T est triée de l'indice 0
    jusqu'à l'indice i - 1 
    i >= 1
    On insère T[i] à sa place et à la fin 
    La liste T est triée de l'indice 0
    jusqu'à l'indice i 
    """    
    
    temp = T[i]
    j = i
    while j > 0 and comp.plus_petit(temp,T[j-1]):
        T[j] = T[j-1]
        j -= 1
    T[j] = temp

def tri_insertion(T,comp):
    """
    tri par insertion la liste T 
    contenant des nombres ou des chaines de caractères
    """
    for i in range(1,len(T)):
        insertion(T,i,comp)

A l'exécution on peut avoir par exemple


date1 = Date(15,1,2022)
date2 = Date(17,1,2022)
date3 = Date(15,2,2022)

fic1 = Fichier('programme1',date1,1000)
fic2 = Fichier('texte1',date2,300)
fic3 = Fichier('image',date3,100)
   
c1 = Comparateur('nom')
c2 = Comparateur('date')
c3 = Comparateur('taille')
    
liste_fichiers = [fic1,fic2,fic3]
#tri suivant le nom   
tri_insertion(liste_fichiers,c1)
#tri suivant la date  
tri_insertion(liste_fichiers,c2)
#tri suivant la taille   
tri_insertion(liste_fichiers,c3)