pygame
pygame est une combinaison de Python avec la SDL (Simple Directmedia Library), une bibliothèque libre multi-plateformes permettant la gestion du multimédia dans la programmation.
- L’affichage vidéo 2D
- La gestion de l’audio
- La gestion de périphériques de commandes (clavier, souris…)
La SDL est écrite en C (un langage reconnu pour sa rapidité), mais utilisable par Python (connu pour sa facilité de programmation) !
Installation
Sur une installation de Python
Depuis une fenêtre de commande Windows, un terminal Linux, ou depuis pyzo
Taper la commande :
pip install pygame
Autre méthode :
- Télécharger le Precompiled Wheel Package de pygame (fichier
.whl
adapté à la version de Python) sur le site de Christophe Gohlke. - Suivre la procédure d’installation de bibliothèque.
Sur une installation WinPython
- Télécharger le Precompiled Wheel Package de pygame (fichier
.whl
adapté à la version de Python) sur le site de Christophe Gohlke. - Lancer le WinPython Control Panel (depuis le dossier de WinPython).
- Depuis le menu, faire Packages/Add Packages ou bien cliquer directement sur
- Sélectionner le fichier
.whl
téléchargé :
- Cliquer ensuite sur
- Tester si l’installation a bien réussi en tapant dans la console (de spyder par exemple) :
>>> import pygame
S’il n’y a pas de message d’erreur, c’est que tout est Ok !
Premiers pas
Suivre le tutoriel sur OpenClassroom …
Ou bien ce site https://realpython.com/pygame-a-primer/
Structure de base
import pygame from pygame.locals import * pygame.init() horloge = pygame.time.Clock() ################################################################################ # Chargement du fond fond = pygame.image.load("image_de_fond.png") # Ouverture de la fenêtre Pygame et collage du fond fenetre_size = fond.get_rect().size fenetre = pygame.display.set_mode(fenetre_size) fenetre_rect = pygame.Rect((0, 0), fenetre_size) fond = fond.convert() fenetre.blit(fond, (0,0)) # Rafraîchissement de l'écran pygame.display.flip() # Constantes ################################################################################ fps = 30 # Taux de rafraichissement (fps = frame per second) ################################################################################ # Boucle infinie ... continuer = True while continuer: # Interception des événements ############################ for event in pygame.event.get(): # tous les événements en attente (file) if event.type == QUIT: continuer = False # Calculs ################################################ # ............... # Re-collage des éléments fenetre.blit(fond, (0,0)) # Dessin ################################################ # .............. # Rafraichissement ###################################### pygame.display.flip() horloge.tick(fps) pygame.quit()
Utilisation
Fermer correctement la fenêtre pygame
Après la boucle principale, tout à la fin du script, ajouter :
pygame.quit()
Cadencer un mouvement
Afin que l’exécution d’un programme pygame soit correctement cadencée, par exemple qu’un mouvement se produise à une vitesse régulière, il faut utiliser une horloge (Clock()
) :
horloge = pygame.time.Clock()
Puis à chaque itération de la boucle principale, fixer le taux d’images par secondes (Frame Rate ou FPS = Frames Per Second) :
horloge.tick(30) # 30 images par seconde
Gérer les collisions
Excellent tutoriel sur OpenClassrooms
Dessiner des figures
Dans la zone # Dessin ####################
:
Intercepter des événements
Dans la zone # Interception des événements ####################
:
Avec la souris
if event.type == pygame.MOUSEBUTTONDOWN: print(f"Bouton {pygame.mouse.get_pressed()} appuyé à la position {pygame.mouse.get_pos()}") elif event.type == pygame.MOUSEBUTTONUP: print(f"Bouton {pygame.mouse.get_pressed()} relâché à la position {pygame.mouse.get_pos()}")
Avec le clavier
if event.type == pygame.KEYDOWN: print("User pressed a key.") elif event.type == pygame.KEYUP: print("User let go of a key.") keys = pygame.key.get_pressed() # états de toutes les touches appuyées if keys[pygame.K_LEFT]: print("Touche gauche appuyée") elif keys[pygame.K_RIGHT]: print("Touche droite appuyée")
Afficher des images
Une image bitmap qui diot être animée à l’écran est appelée sprite.
Voici une classe permettant de manipuler aisément un sprite (affichage, changement d’échelle, rotation, ….). Pour cela, on définit quelques caractéristiques, à partir des dimensions de l’image originale (celle du fichier) :
Dimensions de l’image originale et ancre
Attributs :
- Dimension (privé) :
_size: tuple[int, int]
en pixels - Point d’ancrage (privé) :
_ancre: tuple[int, int]
en pixels - Direction de l' »avant » du sprite (privé) :
_avant: float
en degrés
Ces caractéristiques ne sont jamais modifiées.
Position dans la fenêtre
Attributs :
- position (privé) :
_pos: tuple[int, int]
en pixels
Méthodes (publiques) :
- Obtention de la position :
get_pos() -> (int, int)
en pixels - Modification de la position :
set_pos(x:int, y:int)
en pixels - Déplacement :
move(dx:int, dy:int)
en pixels
Orientation
L’orientation du sprite est l’angle entre la direction horizontale \(\vec x\) et son « avant ».
Elle définit également la direction du mouvement (à faire évoluer…).
Attributs :
- angle de l’image (privé) :
_ang: float
en degrés
Méthodes (publiques) :
- Obtention de l’orientation :
get_ori() -> float
en degrés - Modification de l’orientation :
set_ori(a:float)
avec a en degrés - Rotation :
rotate(da:int)
en degrés - Orientation vers un point :
viser(cible:(x:int,y:int))
en pixels
Calcul de la position d’affichage
Le positionnement de l’image du sprite sur la fenêtre est donné par les coordonnées de son coin haut gauche (point \(A\)), par rapport à l’origine de la fenêtre (point \(O\)).
Or on souhaite placer le sprite par les coordonnées de son point d’ancrage (point \(C\)).
La relation suivante permet d’obtenir les équations reliant les coordonnées du point \(A\) en fonction de celles du point \(C\) :
\(\overrightarrow{OC}=\overrightarrow{OA}+\overrightarrow{AB}+\overrightarrow{BC}\)
Les coordonnées du vecteur \(\overrightarrow{OA}\) sont les valeurs \(X\) et \(Y\) recherchées.
Les coordonnées du vecteur \(\overrightarrow{OC}\) sont les valeurs \(x\) et \(y\) données.
Pour le vecteur \(\overrightarrow{AB}\), il faut considérer 4 cas de figure :
- si \(0<\alpha<\frac{\pi}{2}\), \(\overrightarrow{AB}=\begin{pmatrix}0 \\ w\sin\alpha \\\end{pmatrix}\)
- si \(-\frac{\pi}{2}<\alpha<0\), \(\overrightarrow{AB}=\begin{pmatrix} -h\sin\alpha \\0 \\\end{pmatrix}\)
- si \(-\pi<\alpha<-\frac{\pi}{2}\), \(\overrightarrow{AB}=\begin{pmatrix} -w\cos\alpha-h\sin\alpha \\ -h\cos\alpha \\\end{pmatrix}\)
- si \(\frac{\pi}{2}<\alpha<\pi\), \(\overrightarrow{AB}=\begin{pmatrix} -w\cos\alpha \\ -h\cos\alpha+w\sin\alpha \\\end{pmatrix}\)
Quant au vecteur \(\overrightarrow{BC}\), il faut au préalable exprimer la position du point d’ancrage \(C\) relativement au point \(B\) dans le repère lié à l’image \(R_i\left(B,\vec x_i, \vec y_i\right)\) :
\(\overrightarrow{BC}=\begin{pmatrix} x_a=x_{a0}\frac{w}{w_0} \\ y_a=y_{a0}\frac{h}{h_0} \\\end{pmatrix}\)
On a :
\(\begin{cases}
\vec x_i=\cos\alpha\;\vec x – \sin\alpha\;\vec y \\
\vec y_i=\sin\alpha\;\vec x + \cos\alpha\;\vec y\\
\end{cases}\)
Par conséquent :
\(\overrightarrow{BC}=x_a\vec x_i+y_a\vec y_i=\left[x_a\cos\alpha+y_a\sin\alpha \right]\vec x+\left[-x_a\sin\alpha+y_a \cos\alpha\right]\vec y\)
L’ensemble de ces expressions permet d’obtenir les expression des coordonnées \(X\) et \(Y\) de \(A\), exprimées en langage Python.
Programme complet
Voici un petit programme permettant de déplacer la licorne :
- translation : flèches du clavier
- rotation : touches haut et bas de page du clavier
- zoom : touches + et – du clavier
- mouvement de suivi : curseur de la souris
Fichiers : demo_pygame.zip (version avec scrolling : demo_pygame_v.zip)
import pygame from pygame.locals import * from math import sin, cos, pi, atan2, sqrt pygame.init() horloge = pygame.time.Clock() ################################################################################ # Chargement du fond fond = pygame.image.load("rainbow.png") # Ouverture de la fenêtre Pygame et collage du fond fenetre_size = fond.get_rect().size fenetre = pygame.display.set_mode(fenetre_size) fenetre_rect = pygame.Rect((0, 0), fenetre_size) fond = fond.convert_alpha() fenetre.fill((255,255,255)) fenetre.blit(fond, (0,0)) # Rafraîchissement de l'écran pygame.display.flip() rbutton_pressed = False last_pos = (0,0) # Constantes ################################################################################ fps = 30 # Taux de rafraichissement (fps = frame per second) # Classes ################################################################################ class Sprite: def __init__(self, img_file, fenetre, ancre = [None, None], size = [100, -1], avant = 0): """ ancre: tuple[int, int] None = milieu size: tuple[int, int] valeur négative = automatique (respect des proportions) avant: float angle en degrés désignant l'avant du sprite """ # image originale self._surface0 = pygame.image.load(img_file).convert_alpha() # image courante self._surface = self._surface0 # fenêtre dans laquelle afficher l'image self._fen = fenetre # Dimensions originales self._size0 = [self._surface0.get_width(), self._surface0.get_height()] self._ancre = ancre # Dimensions, Position et orientation self._size = size self._pos = [0,0] # position dans la fenêtre self._ang = 0 # angle absolu (en degrés) self._avant = avant # Paramètres du mouvement self._speed = 0 # vitesse en pixels/seconde # Position du sprite self._POS = self._pos[:] self._change = False # drapeau pour marquer si la position a été calculée # pour ne pas refaire les calculs si rien n'a changé self.set_size(*self._size) def set_pos(self, x, y): self._pos = [x, y] self._change = True # drapeau "la position n'a pas été recalculée" def move(self, dx, dy): self._pos[0] += dx self._pos[1] += dy self._change = True # drapeau "la position n'a pas été recalculée" def set_size(self, w, h): if h < 0 and w > 0: h = (w*self._size0[1])//self._size0[0] elif w < 0 and h > 0: w = (h*self._size0[0])//self._size0[1] self._size = [w, h] self._change = True # drapeau "la position n'a pas été recalculée" def scale(self, f): self.set_size(f*self._size[0], f*self._size[1]) def set_angle(self, a): self._ang = a self._change = True # drapeau "la position n'a pas été recalculée" def rotate(self, da): self.set_angle(self._ang + da) def viser(self, cible): dx = cible[0]-self._pos[0] dy = cible[1]-self._pos[1] self.set_angle(atan2(-dy, dx)*180/pi - self._avant) def set_speed(self, s): self._speed = s self._change = True # drapeau "la position n'a pas été recalculée" def update_pos(self, dt): a = (self._ang + self._avant)*pi/180 c = cos(a) s = sin(a) dx = self._speed * c * dt dy = -self._speed * s * dt self.move(dx, dy) def get_distance(self, pt): return sqrt((pt[0]-self._pos[0])**2+(pt[1]-self._pos[1])**2) def get_centre(self): return self._surface0.get_width()//2, self._surface0.get_height()//2 def update(self): self._surface = pygame.transform.scale(self._surface0, self._size) self._surface = pygame.transform.rotate(self._surface, self._ang) a = ((self._ang + 180)%360 - 180) # angle entre -180° et +180° a *= pi/180 # angle en radians s = sin(a) c = cos(a) w, h = self._size # Vecteur AB if 0<=a<pi/2: xab = 0 yab = w*s elif -pi/2<=a<0: xab = -h*s yab = 0 elif -pi<=a<-pi/2: xab = -w*c-h*s yab = -h*c elif pi/2<=a<pi: xab = -w*c yab = -h*c+w*s # Vecteur BC xa0 = self._ancre[0] if xa0 is None: # milieu xa0 = self._size0[0]/2 ya0 = self._ancre[1] if ya0 is None: # milieu ya0 = self._size0[1]/2 xa = (xa0*w)/self._size0[0] ya = (ya0*h)/self._size0[1] xbc = xa*c+ya*s ybc = -xa*s+ya*c X = self._pos[0]-xbc-xab Y = self._pos[1]-ybc-yab self._POS = [X, Y] self._change = False # drapeau "la position a été recalculée" def draw(self): if self._change: # drapeau "la position n'a pas été recalculée" self.update() self._fen.blit(self._surface, self._POS) # Variables globales ################################################################################ licorne = Sprite("unicorn.png", fenetre, avant = 180) pas_mvt = 5 pas_ang = 5 pas_scale = 1.1 coef_speed = 2 ################################################################################ # Boucle infinie ... continuer = True while continuer: for event in pygame.event.get(): # Attente des événements if event.type == QUIT: continuer = False elif event.type == pygame.MOUSEBUTTONDOWN: #print(f"Bouton {pygame.mouse.get_pressed()} appuyé à la position {pygame.mouse.get_pos()}") pass elif event.type == pygame.MOUSEBUTTONUP: #print(f"Bouton {pygame.mouse.get_pressed()} relâché à la position {pygame.mouse.get_pos()}") pass if event.type == pygame.MOUSEMOTION: #print(f"Curseur à la position {pygame.mouse.get_pos()}") pass # Actions du clavier keys = pygame.key.get_pressed() # états de toutes les touches appuyées if keys[pygame.K_LEFT]: licorne.move(-pas_mvt, 0) if keys[pygame.K_RIGHT]: licorne.move(pas_mvt, 0) if keys[pygame.K_UP]: licorne.move(0, -pas_mvt) if keys[pygame.K_DOWN]: licorne.move(0, pas_mvt) if keys[pygame.K_KP_PLUS]: licorne.scale(pas_scale) if keys[pygame.K_KP_MINUS]: licorne.scale(1/pas_scale) if keys[pygame.K_PAGEUP]: licorne.rotate(pas_ang) if keys[pygame.K_PAGEDOWN]: licorne.rotate(-pas_ang) # Calculs if pygame.mouse.get_focused(): # si le curseur de la souris est dans la fenêtre mouse_position = pygame.mouse.get_pos() licorne.viser(mouse_position) licorne.set_speed(licorne.get_distance(mouse_position)/coef_speed) licorne.update_pos(1/fps) # 1/fps = intervalle de temps entre deux calculs # Re-collage des éléments fenetre.fill((255,255,255)) fenetre.blit(fond, (0,0)) licorne.draw() # Rafraichissement pygame.display.flip() horloge.tick(fps) pygame.quit()