22 - Pygame
Ouverture hors programme. Pygame n’est pas au programme de NSI, mais c’est la bibliothèque la plus utilisée pour les projets de jeux. Tout ce qu’elle demande est au programme : variables, conditions, boucles, listes, fonctions. Les programmes de cette page s’exécutent dans Thonny (ou tout autre éditeur installé sur l’ordinateur), pas dans le navigateur.
Installer Pygame
Pygame ne fait pas partie de Python : il faut l’installer une fois sur l’ordinateur.
- Dans Thonny : menu Outils, puis Gérer les paquets, chercher
pygameet cliquer sur Installer. - Dans un terminal :
pip install pygame(oupython3 -m pip install pygame).
Pour vérifier l’installation, on exécute import pygame : un message « Hello from the pygame community » s’affiche, sans erreur.
La boucle de jeu
Un jeu vidéo affiche des images successives, comme un dessin animé : 60 images par seconde dans nos exemples. Entre deux images, le programme fait toujours les mêmes quatre étapes, dans le même ordre. C’est la boucle de jeu :
- Lire les événements : touches enfoncées, clics, fermeture de la fenêtre.
- Mettre à jour l’état du jeu : positions, vitesses, score, collisions. Rien n’est dessiné à cette étape.
- Dessiner l’image complète, en partant du fond, puis l’afficher.
- Attendre pour ne pas dépasser 60 images par seconde.
Voici le programme minimal : une fenêtre bleu nuit qui se ferme quand on clique sur la croix.
import pygame
pygame.init() # démarrer Pygame
ecran = pygame.display.set_mode((600, 400)) # fenêtre de 600 x 400 pixels
pygame.display.set_caption("Ma première fenêtre")
horloge = pygame.time.Clock()
en_cours = True
while en_cours:
# 1. Événements
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT: # clic sur la croix
en_cours = False
# 2. Mise à jour : rien pour l'instant
# 3. Dessin
ecran.fill((20, 30, 60)) # tout repeindre en bleu nuit
pygame.display.flip() # afficher l'image
# 4. Attente
horloge.tick(60) # 60 images par seconde au plus
pygame.quit()
pygame.display.set_mode((600, 400))crée la fenêtre et renvoie la surface sur laquelle on dessine, rangée dansecran. Attention aux doubles parenthèses : la taille est un couple.- La boucle
while en_courstourne jusqu’à ce qu’on ferme la fenêtre ; c’est nous qui l’écrivons, contrairement à Tkinter. - On dessine dans une image cachée ;
pygame.display.flip()l’affiche d’un coup à l’écran. Sans cette ligne, la fenêtre reste noire. horloge.tick(60)met le programme en pause juste ce qu’il faut pour ne pas dépasser 60 tours de boucle par seconde. Sans elle, le jeu irait aussi vite que le permet l’ordinateur, donc à une vitesse différente sur chaque machine.
Question. Que se passe-t-il si l’on supprime la boucle for evenement in pygame.event.get() ?
Réponse
La fenêtre ne répond plus : le système d’exploitation constate que le programme ne lit pas ses événements et l’indique (« Ne répond pas », curseur qui tourne). La croix ne ferme plus la fenêtre, puisque l’événement QUIT n’est jamais lu. Il faut appeler pygame.event.get() à chaque tour, même si l’on n’utilise aucun événement.
Le repère, les couleurs et les formes
Comme dans Tkinter, l’origine (0, 0) est le coin supérieur gauche de la fenêtre, l’axe des x va vers la droite et l’axe des y vers le bas. Dans une fenêtre de 600 × 400, les pixels ont des abscisses de 0 à 599 et des ordonnées de 0 à 399 : le pixel du coin inférieur droit a pour coordonnées (599, 399).
Une couleur est un triplet (rouge, vert, bleu), chaque composante allant de 0 à 255 : (255, 0, 0) est le rouge, (0, 0, 0) le noir, (255, 255, 255) le blanc, (255, 255, 0) le jaune. C’est le codage RVB (rouge, vert, bleu) qu’utilise aussi le CSS avec rgb(255, 0, 0). On range les couleurs utilisées dans des constantes, écrites en majuscules.
import pygame
CIEL = (135, 206, 235)
HERBE = (60, 160, 70)
JAUNE = (255, 210, 0)
BRUN = (120, 70, 30)
VERT = (30, 120, 50)
BLANC = (255, 255, 255)
pygame.init()
ecran = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Formes")
horloge = pygame.time.Clock()
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
ecran.fill(CIEL)
pygame.draw.rect(ecran, HERBE, (0, 300, 600, 100)) # rectangle : x, y, largeur, hauteur
pygame.draw.circle(ecran, JAUNE, (500, 80), 40) # cercle : centre, rayon
pygame.draw.rect(ecran, BRUN, (140, 200, 30, 100)) # tronc
pygame.draw.circle(ecran, VERT, (155, 180), 60) # feuillage
pygame.draw.ellipse(ecran, BLANC, (250, 60, 120, 50)) # nuage : rectangle englobant
pygame.draw.line(ecran, BRUN, (300, 300), (450, 250), 5) # ligne : départ, arrivée, épaisseur
pygame.draw.polygon(ecran, JAUNE, [(400, 300), (440, 260), (480, 300)]) # triangle
pygame.display.flip()
horloge.tick(60)
pygame.quit()

Un paysage fait de sept formes.
| Fonction | Paramètres après la surface et la couleur |
|---|---|
pygame.draw.rect | (x, y, largeur, hauteur) ; ajouter une épaisseur pour un contour seul : pygame.draw.rect(ecran, c, r, 3) |
pygame.draw.circle | (cx, cy) le centre, puis le rayon |
pygame.draw.ellipse | le rectangle qui contient l’ellipse |
pygame.draw.line | départ, arrivée, épaisseur |
pygame.draw.polygon | liste des sommets |
L’ordre compte : chaque forme est peinte par-dessus les précédentes. Le feuillage est dessiné après le tronc, donc il le recouvre en partie.
Question. Que verrait-on si l’on dessinait l’herbe en dernier ?
Réponse
La même image, sauf ce qui se trouve dans la bande y de 300 à 400 : l’herbe recouvrirait la base du trait brun et du triangle, qui touchent la ligne y = 300. Les formes au-dessus de cette ligne ne changent pas, puisqu’elles ne sont pas sous l’herbe.
Animer : modifier une position à chaque image
Pour qu’un objet bouge, on range sa position dans des variables, et on la modifie un peu à chaque tour de boucle, à l’étape de mise à jour. La vitesse est le nombre de pixels parcourus à chaque image.
import pygame
LARGEUR, HAUTEUR = 600, 400
RAYON = 20
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Rebonds")
horloge = pygame.time.Clock()
x, y = 100, 100 # position du centre de la balle
vx, vy = 5, 3 # vitesse, en pixels par image
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
# Mise à jour de la position
x = x + vx
y = y + vy
# Rebonds : on inverse la vitesse quand la balle touche un bord
if x - RAYON < 0 or x + RAYON > LARGEUR:
vx = -vx
if y - RAYON < 0 or y + RAYON > HAUTEUR:
vy = -vy
ecran.fill((20, 30, 60))
pygame.draw.circle(ecran, (255, 140, 0), (x, y), RAYON)
pygame.display.flip()
horloge.tick(60)
pygame.quit()

La balle à la 50e image.
Le tableau suit la balle pendant les premières images.
| Image | x | y | vx | vy |
|---|---|---|---|---|
| départ | 100 | 100 | 5 | 3 |
| 1 | 105 | 103 | 5 | 3 |
| 2 | 110 | 106 | 5 | 3 |
| 3 | 115 | 109 | 5 | 3 |
À 60 images par seconde, la balle parcourt 5 × 60 = 300 pixels par seconde horizontalement : elle traverse la fenêtre en deux secondes.
Question. On supprime la ligne ecran.fill((20, 30, 60)). Qu’observe-t-on ?
Réponse
Une traînée orange : chaque image dessine la balle à sa nouvelle position sans effacer les précédentes, qui restent à l’écran. C’est pourquoi chaque image commence par repeindre tout le fond.
Le clavier : deux façons de lire les touches
Pygame offre deux façons de savoir ce que fait le joueur au clavier, qui servent à des choses différentes.
L’événement KEYDOWN | pygame.key.get_pressed() | |
|---|---|---|
| Question posée | « une touche vient d’être enfoncée ? » | « quelles touches sont enfoncées en ce moment ? » |
| Réaction | une seule fois par appui | à chaque image, tant que la touche est maintenue |
| Où l’écrire | dans la boucle des événements | à l’étape de mise à jour |
| Usage | sauter, tirer, mettre en pause, valider un menu | se déplacer en continu |
Le programme suivant utilise les deux : les flèches déplacent le carré en continu, et la barre d’espace change sa couleur, une fois par appui.
import pygame
LARGEUR, HAUTEUR = 600, 400
COTE = 40
COULEURS = [(250, 200, 40), (230, 60, 60), (60, 200, 120)]
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Flèches et espace")
horloge = pygame.time.Clock()
x, y = 280, 180
vitesse = 4
numero_couleur = 0
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.KEYDOWN: # une touche vient d'être enfoncée
if evenement.key == pygame.K_SPACE:
numero_couleur = (numero_couleur + 1) % len(COULEURS)
touches = pygame.key.get_pressed() # état actuel du clavier
if touches[pygame.K_LEFT]:
x = x - vitesse
if touches[pygame.K_RIGHT]:
x = x + vitesse
if touches[pygame.K_UP]:
y = y - vitesse
if touches[pygame.K_DOWN]:
y = y + vitesse
x = max(0, min(x, LARGEUR - COTE)) # rester dans la fenêtre
y = max(0, min(y, HAUTEUR - COTE))
ecran.fill((30, 30, 60))
pygame.draw.rect(ecran, COULEURS[numero_couleur], (x, y, COTE, COTE))
pygame.display.flip()
horloge.tick(60)
pygame.quit()

Après un appui sur espace, puis une demi-seconde avec les flèches droite et haut enfoncées.
- La flèche du haut diminue
y, à cause du repère tourné vers le bas. max(0, min(x, LARGEUR - COTE))maintientxentre 0 etLARGEUR - COTE: sixdevient négatif,maxle ramène à 0 ; s’il dépasseLARGEUR - COTE,minle ramène à cette valeur.- On ne lit
evenement.keyqu’après avoir vérifié queevenement.type == pygame.KEYDOWN: un clic ou un déplacement de la souris n’a pas d’attributkey. - Les touches s’appellent
pygame.K_LEFT,pygame.K_SPACE,pygame.K_RETURN(Entrée),pygame.K_ESCAPE,pygame.K_a…pygame.K_z,pygame.K_0…pygame.K_9.
Question. On déplace le carré en testant pygame.K_RIGHT dans la boucle des événements KEYDOWN, avec x = x + vitesse. Que se passe-t-il quand on maintient la flèche droite ?
Réponse
Le carré avance de 4 pixels une seule fois : l’événement KEYDOWN ne se produit qu’au moment où la touche est enfoncée. Pour un déplacement continu, il faut pygame.key.get_pressed().
La souris
Un clic produit un événement MOUSEBUTTONDOWN, dont l’attribut pos donne la position (x, y) du clic ; l’attribut button vaut 1 pour le bouton gauche, 3 pour le droit. À tout moment, pygame.mouse.get_pos() donne la position actuelle du pointeur.
import random
import pygame
LARGEUR, HAUTEUR = 600, 400
RAYON = 25
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Attraper la cible")
horloge = pygame.time.Clock()
police = pygame.font.SysFont("Arial", 24)
def nouvelle_position():
"""Renvoie un centre au hasard, la cible restant entière dans la fenêtre."""
return (random.randint(RAYON, LARGEUR - RAYON),
random.randint(RAYON + 40, HAUTEUR - RAYON))
centre = nouvelle_position()
score = 0
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.MOUSEBUTTONDOWN:
dx = evenement.pos[0] - centre[0]
dy = evenement.pos[1] - centre[1]
if dx * dx + dy * dy <= RAYON * RAYON: # clic dans le disque
score = score + 1
centre = nouvelle_position()
ecran.fill((255, 255, 255))
pygame.draw.circle(ecran, (220, 40, 40), centre, RAYON)
pygame.draw.circle(ecran, (255, 255, 255), centre, RAYON // 2)
pygame.draw.circle(ecran, (220, 40, 40), centre, RAYON // 4)
texte = police.render(f"Score : {score}", True, (0, 0, 0))
ecran.blit(texte, (10, 10))
pygame.display.flip()
horloge.tick(60)
pygame.quit()

Après quatre cibles atteintes.
Le clic est dans le disque quand sa distance au centre est au plus égale au rayon. D’après le théorème de Pythagore, cette distance vaut la racine carrée de dx² + dy² ; comparer les carrés des deux longueurs évite de calculer une racine.
Les rectangles Rect et les collisions
Presque tout, dans un jeu, se ramène à des rectangles : le joueur, les ennemis, les murs, les boutons. Pygame fournit un type pygame.Rect, qui range la position et la taille d’un rectangle, et calcule tout seul ses bords et son centre.
joueur = pygame.Rect(100, 300, 50, 40) # x, y, largeur, hauteur
| Attribut | Signification | Pour pygame.Rect(100, 300, 50, 40) |
|---|---|---|
x, y (ou left, top) | coin supérieur gauche | 100, 300 |
width, height | largeur, hauteur | 50, 40 |
right, bottom | bord droit, bord bas | 150, 340 |
centerx, centery, center | centre | 125, 320, (125, 320) |
On peut modifier n’importe lequel de ces attributs, et le rectangle se déplace en conséquence : joueur.x = joueur.x + 5 le décale de 5 pixels vers la droite, joueur.center = (300, 200) le centre au point (300, 200), joueur.bottom = 400 le pose sur le bas de la fenêtre.
Deux méthodes détectent les collisions :
r1.colliderect(r2)renvoieTruesi les rectanglesr1etr2se chevauchent ;r.collidepoint(pos)renvoieTruesi le pointposest dans le rectangler: c’est le test d’un clic sur un bouton.
Question. Les rectangles a = pygame.Rect(0, 0, 50, 50) et b = pygame.Rect(40, 30, 50, 50) se chevauchent-ils ? Et a et c = pygame.Rect(50, 0, 50, 50) ?
Réponse
a va de x = 0 à x = 50 et de y = 0 à y = 50 ; b commence en (40, 30), donc à l’intérieur de a : ils se chevauchent, a.colliderect(b) vaut True.
c commence exactement en x = 50, là où a s’arrête : les deux rectangles se touchent sans se chevaucher, et a.colliderect(c) vaut False. Le bord droit a.right = 50 n’appartient pas au rectangle a, qui couvre les colonnes de pixels 0 à 49.
Écrire du texte
Pygame ne sait pas écrire directement sur l’écran : il fabrique une image du texte avec une police, puis on colle cette image sur l’écran avec blit.
police = pygame.font.SysFont("Arial", 32) # créée une seule fois, avant la boucle
image = police.render("Score : 12", True, (255, 255, 255)) # texte, lissage, couleur
ecran.blit(image, (10, 10)) # coller l'image, coin supérieur gauche en (10, 10)
pygame.font.SysFont("Arial", 32)cherche la police parmi celles de l’ordinateur ;pygame.font.Font(None, 32)utilise la police fournie avec Pygame, présente partout.renderrenvoie une surface ; le deuxième argument,True, lisse les bords des lettres.- Pour centrer un texte, on demande le rectangle de l’image, placé autour d’un centre :
ecran.blit(image, image.get_rect(center=(300, 200))).
On crée la police une seule fois, avant la boucle : la créer à chaque image ralentit nettement le jeu.
Un bouton cliquable
Un bouton, dans Pygame, n’est qu’un rectangle dessiné, un texte centré dessus, et un test collidepoint au moment du clic. Une fonction évite de répéter ce code pour chaque bouton ; la couleur change quand la souris survole le bouton.
import pygame
pygame.init()
ecran = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Choix de la difficulté")
horloge = pygame.time.Clock()
police = pygame.font.SysFont("Arial", 28)
boutons = {
"Facile": pygame.Rect(200, 90, 200, 60),
"Moyen": pygame.Rect(200, 170, 200, 60),
"Difficile": pygame.Rect(200, 250, 200, 60),
}
choix = "aucun"
def dessiner_bouton(texte, rect):
"""Dessine un bouton, plus clair quand la souris le survole."""
if rect.collidepoint(pygame.mouse.get_pos()):
couleur = (90, 140, 230)
else:
couleur = (50, 90, 170)
pygame.draw.rect(ecran, couleur, rect, border_radius=12)
image = police.render(texte, True, (255, 255, 255))
ecran.blit(image, image.get_rect(center=rect.center))
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.MOUSEBUTTONDOWN and evenement.button == 1:
for texte in boutons:
if boutons[texte].collidepoint(evenement.pos):
choix = texte
ecran.fill((25, 25, 40))
for texte in boutons:
dessiner_bouton(texte, boutons[texte])
info = police.render(f"Difficulté choisie : {choix}", True, (230, 230, 230))
ecran.blit(info, info.get_rect(center=(300, 360)))
pygame.display.flip()
horloge.tick(60)
pygame.quit()

Après un clic sur « Moyen », la souris étant restée dessus.
Les boutons sont rangés dans un dictionnaire : la clé est le texte du bouton, la valeur son rectangle. Une boucle sur les clés dessine tous les boutons, et une autre teste lequel a été cliqué : ajouter un bouton ne demande qu’une ligne.
Des images
Où placer les fichiers ?
Les images et les sons sont rangés dans des dossiers à côté du programme, par exemple :
mon_jeu/
├── main.py
├── images/
│ ├── fond.png
│ └── vaisseau.png
└── sons/
└── tir.wav
Écrire pygame.image.load("images/vaisseau.png") fonctionne quand le programme est lancé depuis le dossier mon_jeu, mais provoque FileNotFoundError s’il est lancé depuis un autre dossier : un chemin relatif part du dossier courant, qui n’est pas forcément celui du programme. Un chemin absolu comme C:/Users/Lea/Bureau/mon_jeu/images/vaisseau.png ne fonctionne que sur l’ordinateur de Léa.
La solution sûre construit le chemin à partir de l’emplacement du programme, donné par la variable spéciale __file__ :
import os
DOSSIER = os.path.dirname(os.path.abspath(__file__)) # dossier du programme
IMAGES = os.path.join(DOSSIER, "images")
vaisseau = pygame.image.load(os.path.join(IMAGES, "vaisseau.png"))
os.path.join assemble les morceaux avec le bon séparateur (/ ou \, selon le système) : le jeu fonctionne sur tous les ordinateurs.
Charger, transformer et afficher
import os
import pygame
DOSSIER = os.path.dirname(os.path.abspath(__file__))
pygame.init()
ecran = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Images")
horloge = pygame.time.Clock()
# Chargement : une seule fois, avant la boucle
vaisseau = pygame.image.load(os.path.join(DOSSIER, "images", "vaisseau.png")).convert_alpha()
vaisseau = pygame.transform.scale(vaisseau, (80, 80)) # redimensionner
petit = pygame.transform.scale(vaisseau, (40, 40))
tourne = pygame.transform.rotate(vaisseau, 45) # tourner de 45 degrés
miroir = pygame.transform.flip(vaisseau, False, True) # retourner verticalement
position = vaisseau.get_rect(center=(300, 300)) # rectangle de l'image
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
ecran.fill((10, 10, 30))
ecran.blit(vaisseau, position) # coller l'image à la place de son rectangle
ecran.blit(petit, (60, 60))
ecran.blit(tourne, (240, 40))
ecran.blit(miroir, (460, 60))
pygame.display.flip()
horloge.tick(60)
pygame.quit()

Une image chargée une fois, puis redimensionnée, tournée et retournée.
pygame.image.loadlit un fichier PNG ou JPEG et renvoie une surface. On l’appelle avant la boucle : lire un fichier sur le disque 60 fois par seconde ralentit énormément le jeu..convert_alpha()convertit l’image au format de l’écran en gardant la transparence du PNG : l’affichage est beaucoup plus rapide. Elle s’appelle aprèsset_mode.image.get_rect(center=...)renvoie unRectde la taille de l’image, placé où l’on veut : on déplace ce rectangle, on teste ses collisions, et on colle l’image à sa place avecblit.pygame.transform.flip(image, True, False)retourne horizontalement : c’est ainsi qu’un personnage regarde à gauche ou à droite avec une seule image.
Des sons
Le module pygame.mixer joue des sons courts (bruitages) et une musique de fond.
son_tir = pygame.mixer.Sound(os.path.join(DOSSIER, "sons", "tir.wav")) # avant la boucle
son_tir.play() # au moment du tir
pygame.mixer.music.load(os.path.join(DOSSIER, "sons", "musique.ogg"))
pygame.mixer.music.play(-1) # -1 : jouer en boucle
pygame.mixer.music.stop()
Les formats les plus sûrs sont WAV et OGG. Un son se charge une seule fois, avant la boucle, comme une image.
Le temps
pygame.time.get_ticks() renvoie le nombre de millisecondes écoulées depuis pygame.init(). En gardant l’instant d’un événement dans une variable, on mesure des durées :
debut = pygame.time.get_ticks() # au début de la partie
...
ecoule = (pygame.time.get_ticks() - debut) // 1000 # secondes écoulées
restant = 30 - ecoule # compte à rebours de 30 secondes
Le même principe déclenche une action à intervalle régulier, par exemple faire apparaître un ennemi toutes les secondes :
dernier = pygame.time.get_ticks()
...
maintenant = pygame.time.get_ticks()
if maintenant - dernier >= 1000: # une seconde depuis la dernière apparition
ennemis.append(nouvel_ennemi())
dernier = maintenant
On n’utilise jamais time.sleep dans un jeu : pendant l’attente, la fenêtre ne répond plus et rien ne bouge.
Plusieurs objets : une liste
Un jeu contient souvent beaucoup d’objets du même genre : ennemis, projectiles, pièces à ramasser. On les range dans une liste, et chaque étape de la boucle les traite tous avec une boucle for.
fruits = [] # liste de Rect
# Mise à jour : faire tomber chaque fruit
for fruit in fruits:
fruit.y = fruit.y + 4
# Garder seulement les fruits encore dans la fenêtre et pas attrapés
restants = []
for fruit in fruits:
if fruit.colliderect(panier):
score = score + 1
elif fruit.top < HAUTEUR:
restants.append(fruit)
fruits = restants
# Dessin
for fruit in fruits:
pygame.draw.ellipse(ecran, ROUGE, fruit)
Question. Pourquoi construire une nouvelle liste restants plutôt que d’écrire fruits.remove(fruit) dans la boucle for fruit in fruits ?
Réponse
Supprimer un élément d’une liste pendant qu’on la parcourt décale les éléments suivants : la boucle saute alors l’élément qui suit chaque suppression. Construire une nouvelle liste avec les éléments à garder évite ce problème.
Les états du jeu : menu, partie, fin
Un jeu passe par plusieurs écrans : l’accueil, la partie, l’écran de fin. Plutôt que plusieurs variables booléennes (accueil = True, partie = False, fin = False), qu’il faut toutes tenir à jour et qui peuvent se contredire, on utilise une seule variable etat, qui vaut "menu", "jeu" ou "fin". Dans la boucle, une condition choisit ce qu’on traite et ce qu’on dessine selon l’état.
etat = "menu"
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.KEYDOWN and evenement.key == pygame.K_RETURN:
if etat == "menu" or etat == "fin":
nouvelle_partie()
etat = "jeu"
if etat == "jeu":
mettre_a_jour() # déplacements, collisions, score
if partie_finie():
etat = "fin"
if etat == "menu":
dessiner_menu()
elif etat == "jeu":
dessiner_jeu()
else:
dessiner_fin()
pygame.display.flip()
horloge.tick(60)
Chaque écran a ses fonctions de mise à jour et de dessin : la boucle principale reste courte et lisible.
Un projet complet : attraper les fruits
Ce jeu réunit tout ce qui précède. Le joueur déplace un panier avec les flèches ; des fruits tombent du haut ; chaque fruit attrapé rapporte un point, chaque fruit raté en fait perdre un. La partie dure 30 secondes. Il n’utilise aucun fichier d’image ni de son : il fonctionne tel quel.
import random
import pygame
LARGEUR, HAUTEUR = 600, 400
DUREE = 30 # durée d'une partie, en secondes
FOND = (25, 35, 60)
BLANC = (255, 255, 255)
JAUNE = (255, 210, 0)
COULEURS_FRUITS = [(230, 50, 50), (255, 150, 0), (120, 200, 60)]
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Attraper les fruits")
horloge = pygame.time.Clock()
grande = pygame.font.SysFont("Arial", 48)
petite = pygame.font.SysFont("Arial", 24)
# État de la partie
etat = "menu"
panier = pygame.Rect(260, 360, 80, 20)
fruits = [] # liste de fruits, chacun une liste [rectangle, couleur]
score = 0
debut = 0
derniere_chute = 0
def ecrire(texte, police, couleur, centre):
"""Écrit un texte centré sur le point centre."""
image = police.render(texte, True, couleur)
ecran.blit(image, image.get_rect(center=centre))
def nouvelle_partie():
"""Remet à zéro tout l'état de la partie."""
global fruits, score, debut, derniere_chute
panier.centerx = LARGEUR // 2
fruits = []
score = 0
debut = pygame.time.get_ticks()
derniere_chute = debut
def mettre_a_jour():
"""Déplace le panier et les fruits, gère les collisions et le score."""
global fruits, score, derniere_chute
touches = pygame.key.get_pressed()
if touches[pygame.K_LEFT]:
panier.x = panier.x - 7
if touches[pygame.K_RIGHT]:
panier.x = panier.x + 7
panier.x = max(0, min(panier.x, LARGEUR - panier.width))
maintenant = pygame.time.get_ticks()
if maintenant - derniere_chute >= 600: # un nouveau fruit toutes les 0,6 s
x = random.randint(0, LARGEUR - 30)
fruits.append([pygame.Rect(x, -30, 30, 30), random.choice(COULEURS_FRUITS)])
derniere_chute = maintenant
restants = []
for fruit in fruits:
rect = fruit[0]
rect.y = rect.y + 4
if rect.colliderect(panier):
score = score + 1
elif rect.top > HAUTEUR:
score = score - 1
else:
restants.append(fruit)
fruits = restants
def temps_restant():
return DUREE - (pygame.time.get_ticks() - debut) // 1000
def dessiner_jeu():
ecran.fill(FOND)
for fruit in fruits:
pygame.draw.ellipse(ecran, fruit[1], fruit[0])
pygame.draw.rect(ecran, JAUNE, panier, border_radius=6)
ecran.blit(petite.render(f"Score : {score}", True, BLANC), (10, 10))
ecran.blit(petite.render(f"Temps : {temps_restant()} s", True, BLANC), (460, 10))
def dessiner_menu():
ecran.fill(FOND)
ecrire("Attraper les fruits", grande, JAUNE, (LARGEUR // 2, 150))
ecrire("Flèches gauche et droite pour déplacer le panier", petite, BLANC, (LARGEUR // 2, 230))
ecrire("Entrée pour commencer", petite, BLANC, (LARGEUR // 2, 280))
def dessiner_fin():
ecran.fill(FOND)
ecrire("Partie terminée", grande, JAUNE, (LARGEUR // 2, 150))
ecrire(f"Score : {score}", grande, BLANC, (LARGEUR // 2, 220))
ecrire("Entrée pour rejouer", petite, BLANC, (LARGEUR // 2, 290))
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.KEYDOWN and evenement.key == pygame.K_RETURN:
if etat != "jeu":
nouvelle_partie()
etat = "jeu"
if etat == "jeu":
mettre_a_jour()
if temps_restant() <= 0:
etat = "fin"
if etat == "menu":
dessiner_menu()
elif etat == "jeu":
dessiner_jeu()
else:
dessiner_fin()
pygame.display.flip()
horloge.tick(60)
pygame.quit()

L’état « menu ».

L’état « jeu ».
Quelques choix méritent d’être remarqués.
- Tout l’état de la partie tient dans quelques variables globales :
etat,panier,fruits,score,debut,derniere_chute. Les fonctions qui les réaffectent les déclarent avecglobal;panier.x = ...modifie le rectangle sans réaffecter la variablepanier, d’où l’absence deglobal panier. nouvelle_partie()remet tout à zéro : rejouer ne demande pas de relancer le programme.- Chaque fruit est une liste
[rectangle, couleur]:fruit[0]est sonRect,fruit[1]sa couleur. - Les constantes en majuscules (
LARGEUR,DUREE…) rassemblent les réglages en haut du fichier : pour rendre le jeu plus difficile, on change un seul nombre.
Question. Comment faire accélérer les fruits au fil de la partie ?
Réponse
Remplacer la vitesse fixe 4 par une valeur qui dépend du temps écoulé, par exemple rect.y = rect.y + 4 + (DUREE - temps_restant()) // 10 : la vitesse passe à 5 après 10 secondes, puis à 6 après 20 secondes.
Organiser un projet plus grand
- Une fonction par tâche :
mettre_a_jour,dessiner_jeu,dessiner_menu… Une boucle principale de vingt lignes se relit facilement ; une boucle de trois cents lignes, non. - Des constantes en majuscules pour les tailles, vitesses et couleurs, plutôt que des nombres répétés dans tout le code.
- Des données plutôt que du code répété : les boutons dans un dictionnaire, les niveaux ou les dialogues dans des listes, chaque élément traité par la même boucle.
- Éviter
from pygame import *: on ne sait plus d’où vient chaque nom, et un nom du programme peut en écraser un de Pygame. On écritimport pygame, puispygame.QUIT. - Pygame propose aussi des classes (
pygame.sprite.Sprite) pour représenter chaque objet du jeu avec ses données et ses méthodes. La programmation orientée objet est au programme de terminale ; tout ce qui précède s’écrit sans elle.
Erreurs fréquentes
| Symptôme | Cause | Correction |
|---|---|---|
| la fenêtre ne répond pas, la croix ne ferme rien | pas de pygame.event.get() dans la boucle | lire les événements à chaque tour |
| la fenêtre reste noire | pygame.display.flip() oublié | l’appeler à la fin de chaque tour |
| l’objet laisse une traînée | le fond n’est pas repeint | ecran.fill(...) au début du dessin |
| le jeu va trop vite, ou à des vitesses différentes selon l’ordinateur | pas de horloge.tick(60) | l’ajouter à la fin de la boucle |
| le jeu rame | pygame.image.load ou pygame.font.SysFont dans la boucle | charger images, sons et polices avant la boucle |
FileNotFoundError: No file 'images/fond.png' found in working directory | chemin relatif au dossier courant, ou chemin absolu d’un autre ordinateur | os.path.join(DOSSIER, "images", "fond.png") |
pygame.error: video system not initialized | pygame.init() oublié, ou dessin après pygame.quit() | pygame.init() au début, pygame.quit() après la boucle seulement |
pygame.error: font not initialized | police créée avant pygame.init() | créer les polices après pygame.init() |
AttributeError: 'pygame.event.Event' object has no attribute 'key' | evenement.key lu sans vérifier le type de l’événement | tester d’abord evenement.type == pygame.KEYDOWN |
TypeError: size must be two numbers | set_mode(600, 400) sans les doubles parenthèses | set_mode((600, 400)) |
| l’objet ne bouge que d’un cran quand on maintient la touche | déplacement placé dans KEYDOWN | pygame.key.get_pressed() pour les déplacements |
la boucle ne s’arrête jamais, ou NameError: name 'En_cours' is not defined | deux noms différents pour la même variable (en_cours et En_cours) | Python distingue les majuscules : un seul nom |
| l’objet monte au lieu de descendre | y diminué au lieu d’augmenté | l’axe des y va vers le bas |
Pour aller plus loin
- Ajouter au jeu des fruits des « bombes » noires qui font perdre trois points, et un meilleur score gardé d’une partie à l’autre.
- Ajouter un bruitage quand un fruit est attrapé, avec
pygame.mixer.Sound. - Remplacer le panier et les fruits par des images, en gardant les mêmes
Rectpour les collisions. - La documentation de Pygame (en anglais) décrit toutes les fonctions et propose des tutoriels.
À retenir
- Un jeu est une boucle qui, à chaque image, lit les événements, met à jour l’état, dessine tout, puis attend avec
horloge.tick(60). pygame.event.get()à chaque tour,ecran.fill(...)avant de dessiner,pygame.display.flip()après : sans eux, la fenêtre se fige, laisse des traînées ou reste noire.- L’origine est en haut à gauche,
yaugmente vers le bas ; une couleur est un triplet(rouge, vert, bleu). - Une animation modifie une position d’une vitesse à chaque image ; un rebond change le signe de la vitesse.
KEYDOWNréagit une fois par appui ;pygame.key.get_pressed()donne les touches maintenues.pygame.Rectrange position et taille ;colliderectetcollidepointdétectent les collisions et les clics.- Images, sons et polices se chargent une seule fois, avant la boucle, avec des chemins construits depuis le dossier du programme.
- Une variable
etat("menu","jeu","fin") organise les écrans ; des listes rangent les objets nombreux.
Résumé
| Concept | Syntaxe | Exemple |
|---|---|---|
| Démarrer | pygame.init(), set_mode((l, h)) | ecran = pygame.display.set_mode((600, 400)) |
| Événements | for e in pygame.event.get(): | if e.type == pygame.QUIT: |
| Touche enfoncée | e.type == pygame.KEYDOWN, e.key | e.key == pygame.K_SPACE |
| Touches maintenues | pygame.key.get_pressed() | touches[pygame.K_LEFT] |
| Clic | e.type == pygame.MOUSEBUTTONDOWN, e.pos | bouton.collidepoint(e.pos) |
| Fond | ecran.fill(couleur) | ecran.fill((0, 0, 0)) |
| Formes | pygame.draw.rect, circle, line, ellipse, polygon | pygame.draw.circle(ecran, c, (x, y), 20) |
| Afficher | pygame.display.flip() | fin de chaque tour |
| Cadence | horloge.tick(60) | 60 images par seconde |
| Rectangle | pygame.Rect(x, y, l, h) | joueur.colliderect(ennemi) |
| Texte | police.render(texte, True, c) puis blit | ecran.blit(image, (10, 10)) |
| Image | pygame.image.load(chemin).convert_alpha() | avant la boucle |
| Chemin | os.path.join(DOSSIER, "images", "a.png") | DOSSIER tiré de __file__ |
| Son | pygame.mixer.Sound(chemin).play() | bruitage |
| Temps | pygame.time.get_ticks() | durée en millisecondes |