Programmer en Python
Leçon 4 - Programmer un jeu vidéo avec Pygame
Ce que tu sauras faire à la fin. Ouvrir une fenêtre, dessiner, écouter le clavier, animer, gérer les chocs, compter les points, et assembler le tout en un jeu jouable.
Programmer un jeu vidéo
avec Pygame
Douze marches, de la fenêtre vide au jeu complet
Sixième et cinquième
Ce que ce cours suppose déjà acquis. Les variables, les boucles et les conditions en Python. Rien d’autre.
Ce que ce cours n’aborde pas. Les classes et les objets. Tout le code reste écrit en instructions simples, dans l’ordre.
Ce que tu sauras faire à la fin. Ouvrir une fenêtre, dessiner, écouter le clavier, animer, gérer les chocs, compter les points, et assembler le tout en un jeu jouable.
Une promesse sur le code. Les quatorze programmes de ce cours ont été exécutés avant d’être imprimés, avec Pygame 2.6.1. Les chiffres annoncés dans les encadrés de mesure ont été relevés, pas estimés.
Pour imprimer. Dans la boîte de dialogue du navigateur, choisis « Marges : Par défaut » plutôt que « Aucune » : les marges hautes et basses en dépendent.
🧭 Comment lire ce cours
Quelques minutes de lecture qui t’en feront gagner beaucoup.
Ce cours est bâti comme un escalier. Chaque chapitre ajoute une marche, une seule, et s’appuie sur toutes les précédentes. Lire le chapitre 9 sans avoir fait le 5 te ferait perdre ton temps.
Les trois moments de chaque chapitre
| Moment | Ce que tu fais |
|---|---|
| 🎯 Point de départ | Une question posée avant la leçon. Prends une feuille et cherche seul quelques minutes. Se tromper à ce moment-là fait partie du travail. |
| Le corps du chapitre | La notion est expliquée, montrée en schéma, puis écrite en code. Tape les programmes toi-même plutôt que de les recopier par copier-coller : la main retient. |
| 🚀 À toi de jouer | Trois défis à traiter seul. Les réponses figurent à la fin du fascicule, mais cherche d’abord. |
Les six encadrés et ce qu’ils contiennent
| Signature | Contenu |
|---|---|
| 📘 Définition | À connaître par cœur. |
| 💡 Le savais-tu ? | Un fait surprenant, vrai et vérifié. |
| 📜 Histoire vraie | Un épisode réel de l’histoire de l’informatique. |
| 🔬 À l’essai | Une manipulation à faire, ou une mesure relevée pour ce cours. |
| 🧵 Fil rouge | Le lien avec le chapitre suivant. |
| 📋 Récapitulatif | Les cinq points à retenir. |
💡 Le savais-tu ?
Une habitude à prendre dès maintenant : garde une feuille et un crayon à côté du clavier. Les programmeurs professionnels dessinent beaucoup, bien plus que le public ne l’imagine. Un schéma de trois traits règle souvent un problème sur lequel une heure de code aurait buté.
Chapitre 1
🧰 La boîte à outils
Python sait compter et raisonner. Dessiner, il ne sait pas. Voyons ce qu’il faut lui ajouter.
🎯 Point de départ
Tu sais déjà écrire un programme qui affiche du texte, qui range des nombres dans des variables et qui répète une action avec une boucle. Imagine maintenant que tu veuilles faire glisser un vaisseau spatial sur ton écran.
Avant de tourner la page, prends une feuille et note trois choses que Python, tel que tu le connais, ne sait pas encore faire. Puis reviens ici.
Voici ce que la plupart des élèves trouvent, et que nous allons confirmer point par point. Python ne sait pas ouvrir une fenêtre. Il ne sait pas tracer un rectangle rouge à l’endroit de ton choix. Il ne sait pas remarquer que tu viens d’appuyer sur la flèche gauche. Et il ne sait pas mesurer le temps assez finement pour animer quelque chose.
Ces quatre manques ont le même remède. Des programmeurs ont écrit, une fois pour toutes, le code qui parle à l’écran, au clavier et à l’horloge de la machine. Ils l’ont rangé dans un paquet que n’importe quel programme peut ouvrir. Ce paquet porte un nom.
📘 Définition
Une bibliothèque est un ensemble de programmes déjà écrits, rangés côte à côte, que ton propre programme peut utiliser sans les réécrire. Tu ne les recopies pas : tu les appelles.
Pygame est une bibliothèque Python destinée aux jeux et aux animations. Elle fournit la fenêtre, le dessin, la lecture du clavier, le son et la mesure du temps.
Une bibliothèque posée sur une autre
Pygame ne parle pas directement à ta carte graphique. Elle s’appuie sur une bibliothèque plus ancienne et plus proche de la machine, écrite en langage C et nommée SDL, pour Simple DirectMedia Layer. SDL se charge des détails pénibles, différents sur Windows, sur macOS et sur Linux. Pygame traduit tout cela en instructions Python courtes et lisibles.
Cet empilement explique une chose que tu verras plus tard : certains messages d’erreur de Pygame parlent de SDL. Ce n’est pas une faute de ta part, c’est l’étage du dessous qui se manifeste.
📜 Histoire vraie
En l’an 2000, un programmeur américain nommé Pete Shinners découvre en même temps deux outils qui l’enthousiasment : le langage Python et la bibliothèque SDL, créée par Sam Lantinga. Il veut marier les deux et cherche si quelqu’un l’a déjà fait. La réponse est oui : un projet nommé PySDL existe.
Mais PySDL vient de s’arrêter. Son auteur ne le poursuit plus. Plutôt que d’attendre, Shinners décide d’écrire lui-même le pont entre Python et SDL. Le projet démarre en octobre 2000. Six mois plus tard, en avril 2001, la version 1.0 de Pygame est publiée.
Vingt-cinq ans plus tard, cette bibliothèque née de l’abandon d’un autre projet sert encore à apprendre la programmation de jeux dans le monde entier.
Installer Pygame
Cette bibliothèque ne vient pas avec Python : elle s’ajoute. L’outil qui installe les bibliothèques Python se nomme pip. Ouvre un terminal, aussi appelé invite de commandes, et tape la ligne correspondant à ton système.
| Système | Commande à taper dans le terminal |
|---|---|
| Windows | pip install pygame |
| macOS | pip3 install pygame |
| Linux | pip3 install pygame --break-system-packages |
🔬 À l’essai
Une fois l’installation terminée, vérifie qu’elle a réussi. Crée un fichier nommé essai.py et tape ces deux lignes :
import pygame print(pygame.version.ver)
En exécutant ce programme, tu dois voir s’afficher un numéro de version, par exemple 2.6.1. Si Python répond plutôt ModuleNotFoundError, l’installation n’a pas abouti et il faut reprendre la commande précédente.
💡 Le savais-tu ?
Depuis 2023, Pygame existe en deux versions. La version historique s’installe avec pip install pygame. Une seconde version, nommée pygame-ce pour Community Edition, a été créée par d’anciens développeurs du projet qui souhaitaient des publications plus fréquentes et une gouvernance différente.
Les deux se ressemblent beaucoup et le code de ce cours fonctionne avec l’une comme avec l’autre. Une précaution tout de même : installer les deux sur la même machine crée des conflits, car elles se déclarent sous le même nom pygame. Choisis-en une.
Ce que contient la boîte
Pygame n’est pas un bloc unique. Elle est découpée en modules, chacun chargé d’une tâche. Tu les reconnaîtras à leur nom, qui suit la même forme : pygame. suivi du module, puis de l’action. Voici ceux que ce cours utilise.
| Module | Rôle | Chapitre où tu le rencontreras |
|---|---|---|
pygame.display | Créer la fenêtre et rafraîchir l’image | 2 |
pygame.draw | Tracer rectangles, cercles et traits | 4 |
pygame.event | Recevoir les actions du joueur | 6 |
pygame.key | Savoir quelles touches sont enfoncées | 6 |
pygame.time | Mesurer le temps, régler la vitesse | 8 |
pygame.Rect | Manipuler des rectangles, détecter les chocs | 10 |
pygame.font | Écrire du texte à l’écran | 11 |
🧵 Fil rouge
La boîte est posée sur la table et ouverte. Au chapitre suivant, nous en sortons le premier outil, celui sans lequel rien ne peut être montré : la fenêtre.
🚀 À toi de jouer
Défi 1.1. Installe Pygame et fais afficher son numéro de version par ton programme.
Défi 1.2. Le numéro de version de SDL se trouve dans pygame.version.SDL. Écris un programme qui affiche les deux numéros, celui de Pygame et celui de SDL, sur deux lignes séparées.
Défi 1.3. Sans regarder le tableau ci-dessus, cite de mémoire le module chargé de créer la fenêtre, et celui chargé de tracer un cercle.
📋 Récapitulatif
- Python seul ne dessine pas, n’écoute pas le clavier et ne mesure pas finement le temps.
- Une bibliothèque est un ensemble de programmes déjà écrits que ton programme appelle.
- Pygame est une bibliothèque de jeu posée sur SDL, elle-même écrite en C.
- Elle s’installe avec
pip install pygameet se vérifie avecpygame.version.ver. - Elle se divise en modules :
display,draw,event,key,time,font.
Chapitre 2
🖥️ La première fenêtre
Quatre lignes, une fenêtre. Et une surprise, qui cache la leçon principale du chapitre.
🎯 Point de départ
Voici un programme complet de quatre lignes. Lis-le attentivement.
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.quit()
Avant de le taper, écris sur ta feuille ce que tu penses qu’il va se passer. Combien de temps la fenêtre restera-t-elle visible ? Puis exécute-le et compare.
Ce qui se passe réellement
La fenêtre apparaît et disparaît si vite que tu la vois à peine. Beaucoup pensent d’abord à une erreur. Il n’y en a pas : le programme a fait ce que tu lui as demandé, jusqu’au bout, en quelques millièmes de seconde.
Un programme Python se lit de haut en bas. Il ouvre la fenêtre avec set_mode, puis il rencontre pygame.quit(), qui range tout. Ensuite le fichier se termine et Python s’arrête. Une fenêtre appartient au programme qui l’a créée : le programme fini, la fenêtre s’en va avec lui.
Pour la voir, nous devons demander au programme d’attendre.
Programme 2 : une fenêtre qui reste trois secondes
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Ma première fenêtre") pygame.time.wait(3000) pygame.quit()
pygame.time.wait(3000) met le programme en pause pendant 3000 millisecondes, c’est-à-dire trois secondes.
📘 Définition
pygame.init() réveille tous les modules de Pygame. Cette ligne se place au début, avant toute autre instruction de la bibliothèque.
pygame.display.set_mode((800, 600)) crée la fenêtre et renvoie la surface sur laquelle tu vas dessiner. Remarque les deux paires de parenthèses : les dimensions voyagent ensemble, sous forme d’un couple (largeur, hauteur).
Une surface est une zone de mémoire qui contient une image. La fenêtre en est une, et nous en rencontrerons d’autres.
💡 Le savais-tu ?
Les dimensions de la fenêtre se comptent en pixels, contraction de l’anglais picture element. Une fenêtre de 800 sur 600 contient donc 480 000 pixels, que ton programme peut peindre un par un.
Ce format 800 sur 600 n’a rien d’arbitraire. Il correspond à une résolution d’écran très répandue dans les années 1990, restée depuis un point de repère commode pour les exercices.
Peindre la surface, puis la montrer
Pour l’instant la fenêtre est grise et vide. Repeignons-la en blanc. Cela demande deux instructions, et leur ordre compte.
Programme 3 : une fenêtre repeinte en blanc
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Une fenêtre repeinte") fenetre.fill((255, 255, 255)) pygame.display.flip() pygame.time.wait(3000) pygame.quit()
📘 Définition
fenetre.fill(couleur) remplit toute la surface d’une couleur unique.
pygame.display.flip() montre à l’écran ce qui vient d’être dessiné.
Ces deux instructions sont distinctes pour une raison précise, expliquée juste en dessous.
🔬 À l’essai
Essaie ceci. Supprime la ligne pygame.display.flip() et relance le programme. La fenêtre reste grise : ton remplissage blanc a bien eu lieu, mais il n’a pas été montré.
Pygame travaille sur deux feuilles. Tu dessines sur une feuille cachée, tranquillement, autant que tu veux. flip retourne les feuilles et présente au joueur celle que tu viens de préparer. Ce procédé porte le nom de double tampon.
Son intérêt est considérable. Sans lui, le joueur verrait ton dessin se construire morceau par morceau, et l’image scintillerait.
📜 Histoire vraie
Pete Shinners ne s’est pas contenté d’écrire Pygame : il s’en est servi. En mai 2001, quelques semaines après la version 1.0, il publie SolarWolf, un jeu d’action et d’arcade entièrement construit avec sa propre bibliothèque.
Il raconta plus tard qu’une chose l’avait surpris : un an après la publication, le jeu n’avait réclamé ni correctif ni mise à jour. Pour un programmeur, cette phrase vaut une médaille. Elle signifie que la bibliothèque tenait debout toute seule.
SolarWolf fut ensuite distribué comme démonstration de Pygame, et servit longtemps d’exemple aux débutants.
| Instruction | Ce qu’elle fait | Ce qui arrive si tu l’oublies |
|---|---|---|
pygame.init() | Réveille les modules de Pygame | Certaines fonctions refusent de travailler |
set_mode((800, 600)) | Crée la fenêtre et renvoie sa surface | Rien à dessiner, rien à afficher |
set_caption("...") | Écrit le titre de la fenêtre | Le titre reste pygame window |
fenetre.fill(couleur) | Repeint toute la surface | Le fond garde son gris d’origine |
pygame.display.flip() | Montre le dessin au joueur | Ton dessin existe mais reste invisible |
pygame.quit() | Range proprement avant de partir | La fenêtre peut se figer à la fermeture |
🧵 Fil rouge
Tu sais ouvrir une surface de 800 sur 600 et la repeindre. Reste à savoir désigner un point précis à l’intérieur. C’est l’objet du chapitre suivant, et c’est le point sur lequel butent le plus d’élèves.
🚀 À toi de jouer
Défi 2.1. Transforme le programme 3 pour obtenir une fenêtre carrée de 500 sur 500, intitulée « Mon premier carré », remplie d’un bleu ciel.
Défi 2.2. Fais rester la fenêtre dix secondes au lieu de trois. Quelle valeur faut-il écrire dans wait ?
Défi 2.3. Déplace la ligne fenetre.fill après pygame.display.flip(). Exécute, observe, puis explique en une phrase pourquoi la fenêtre reste grise.
📋 Récapitulatif
- Un programme qui se termine emporte sa fenêtre avec lui.
set_modecrée la fenêtre et renvoie une surface.- Dessiner et afficher sont deux gestes séparés :
fillpeint,flipmontre. - Le double tampon évite le scintillement en préparant l’image à l’abri des regards.
- Une fenêtre de 800 sur 600 contient 480 000 pixels.
Chapitre 3
📐 Le repère de l’écran
Le chapitre le plus court à lire et le plus long à oublier. Ici, l’axe des ordonnées descend.
🎯 Point de départ
Prends une feuille quadrillée. Trace un repère comme en cours de mathématiques et place le point de coordonnées (3 ; 2).
Garde cette feuille sous les yeux. Nous allons demander à Pygame de placer le même point, et le résultat ne sera pas au même endroit.
En mathématiques, l’origine se trouve en bas à gauche, les abscisses augmentent vers la droite et les ordonnées augmentent vers le haut. Un point d’ordonnée élevée est un point haut.
Sur un écran, l’origine se trouve en haut à gauche. Les abscisses augmentent vers la droite, comme en mathématiques. Mais les ordonnées augmentent vers le bas. Un point d’ordonnée élevée est un point bas.
📘 Définition
Dans le repère de l’écran, un point est désigné par un couple (x, y) où x est la distance depuis le bord gauche et y celle depuis le bord haut, toutes deux comptées en pixels.
L’origine (0, 0) est le coin supérieur gauche.
Pourquoi vers le bas ? La réponse tient dans un tube
Cette convention peut sembler une lubie de programmeur. Elle vient en réalité de la façon dont les premiers écrans fabriquaient leur image.
📜 Histoire vraie
Dans un téléviseur à tube cathodique, un canon envoie un faisceau d’électrons qui frappe l’écran et y allume un point. Pour couvrir toute la surface, ce faisceau balaie l’écran : il part du coin supérieur gauche, trace une ligne vers la droite, revient à gauche une ligne plus bas, et recommence jusqu’en bas de l’écran.
La mémoire qui contient l’image suit le même ordre. La première case mémoire correspond au premier point balayé, donc au coin supérieur gauche. La deuxième case correspond au point d’à côté, vers la droite. Et ainsi de suite, ligne après ligne, vers le bas.
Compter les pixels depuis le haut revenait donc à lire la mémoire dans l’ordre, sans conversion. Sur des machines lentes, cette économie pesait lourd. La convention est restée bien après la disparition des tubes.
Précision d’honnêteté : la raison pour laquelle le faisceau part du haut plutôt que du bas est généralement rattachée au sens de lecture occidental, de haut en bas. Cette explication est plausible et souvent citée, mais elle n’est pas établie par un document d’époque.
💡 Le savais-tu ?
La convention de l’écran n’a pas gagné partout. OpenGL, la bibliothèque de graphisme en trois dimensions employée dans les jeux modernes, place son origine en bas à gauche, comme en mathématiques.
Les deux conventions coexistent donc dans la même machine, et les programmeurs qui passent de l’une à l’autre commettent régulièrement des erreurs d’inversion. Tu es prévenu avant eux.
La preuve par les quatre coins
Programme 4 : un point dans chaque coin, et un cinquième au choix
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Les quatre coins") fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (200, 40, 40), (0, 0), 14) pygame.draw.circle(fenetre, (30, 140, 90), (800, 0), 14) pygame.draw.circle(fenetre, (40, 90, 200), (0, 600), 14) pygame.draw.circle(fenetre, (150, 90, 20), (800, 600), 14) pygame.draw.circle(fenetre, (120, 60, 160), (300, 200), 14) pygame.display.flip() pygame.time.wait(5000) pygame.quit()
Exécute ce programme avant de lire la suite, puis observe les quatre coins.
🔬 À l’essai
Regarde attentivement les quatre coins. Tu ne vois pas quatre cercles entiers, mais quatre quarts de cercle.
La raison est instructive. Un cercle de centre (0 ; 0) et de rayon 14 occupe les points d’abscisse comprise entre -14 et 14. Or les abscisses négatives sortent de la fenêtre : elles sont en dehors de la surface, donc perdues. Seul le quart situé dans la fenêtre s’affiche.
Cette découpe silencieuse porte un nom : le découpage, en anglais clipping. Pygame ne proteste pas quand tu dessines hors de la fenêtre, il ignore simplement ce qui dépasse.
| Coin de la fenêtre | Coordonnées | Lecture |
|---|---|---|
| Supérieur gauche | (0, 0) | l’origine |
| Supérieur droit | (800, 0) | tout à droite, tout en haut |
| Inférieur gauche | (0, 600) | tout à gauche, tout en bas |
| Inférieur droit | (800, 600) | le coin le plus éloigné |
| Centre | (400, 300) | la moitié de chaque dimension |
🧵 Fil rouge
Tu sais désormais désigner n’importe quel point de la fenêtre. Au chapitre suivant, nous y déposons des formes et des couleurs.
🚀 À toi de jouer
Défi 3.1. Sans exécuter de programme, dis lequel de ces deux points est le plus haut à l’écran : (100 ; 50) ou (100 ; 400) ?
Défi 3.2. Place un cercle au centre précis d’une fenêtre de 640 sur 480. Quelles coordonnées écris-tu ?
Défi 3.3. Écris un programme qui trace cinq cercles alignés en diagonale, du coin supérieur gauche au coin inférieur droit d’une fenêtre de 800 sur 600, régulièrement espacés.
📋 Récapitulatif
- L’origine de l’écran est en haut à gauche.
xaugmente vers la droite,yaugmente vers le bas.- Cette convention vient du balayage du faisceau dans les écrans à tube.
- Ce qui dépasse de la fenêtre est découpé sans message d’erreur.
- Pour une fenêtre de 800 sur 600, le centre est en (400 ; 300).
Chapitre 4
🎨 Dessiner et colorer
Trois nombres suffisent à décrire plus de seize millions de couleurs. Voici comment.
🎯 Point de départ
Ton écran sait afficher une quantité de teintes distinctes. Combien au juste ? Propose un nombre, même approximatif, et note-le. Note aussi comment tu l’as obtenu.
La réponse figure plus bas dans ce chapitre, et le raisonnement qui y mène tient en une multiplication.
Une couleur, trois nombres
Chaque pixel de ton écran contient trois minuscules sources de lumière : une rouge, une verte, une bleue. En réglant l’intensité de chacune, ton écran fabrique toutes les autres couleurs.
Pygame décrit donc une couleur par un triplet de nombres entiers, chacun compris entre 0 et 255. Le premier règle le rouge, le deuxième le vert, le troisième le bleu.
📘 Définition
Le codage RVB, pour rouge, vert, bleu, décrit une couleur par trois nombres entiers de 0 à 255.
(255, 0, 0) est un rouge vif, (0, 0, 0) le noir, (255, 255, 255) le blanc.
Ce mélange est dit additif : les lumières s’ajoutent. Additionner les trois au maximum donne du blanc, ce qui surprend qui a l’habitude de la peinture, où mélanger toutes les couleurs donne un brun sale.
🔬 À l’essai
Comptons. Le rouge admet 256 valeurs, de 0 à 255. Le vert aussi, et le bleu aussi. Chaque combinaison donne une couleur différente, donc le total vaut :
256 × 256 × 256 = 16 777 216
Plus de seize millions de couleurs, avec trois nombres seulement. L’œil humain, lui, en distingue nettement moins, ce qui rend cette palette largement suffisante.
📜 Histoire vraie
Le 17 mai 1861, devant les membres de la Royal Institution de Londres, le physicien écossais James Clerk Maxwell présente une image qui n’avait alors pas d’équivalent : une photographie en couleurs.
Sa méthode, imaginée dès 1855, est celle-là même que ton écran applique. Le photographe Thomas Sutton photographie un ruban de tartan écossais trois fois de suite, chaque fois derrière un filtre différent : rouge, vert, puis bleu-violet. Les trois clichés sont ensuite projetés au même endroit par trois projecteurs munis des mêmes filtres. Superposées, les trois images redonnent les couleurs du ruban.
Une nuance que l’histoire retient : des chercheurs ont montré plus tard que les plaques de l’époque étaient peu sensibles au rouge. Ce que le filtre rouge a probablement capté, ce sont des ultraviolets renvoyés par la teinture. La démonstration a donc réussi en partie par chance. Le principe, lui, était juste, et c’est celui que ton écran applique cent soixante-cinq ans plus tard.
Quatre façons de dessiner
Programme 5 : quatre formes dans une fenêtre
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Quatre formes") fenetre.fill((255, 255, 255)) pygame.draw.rect(fenetre, (200, 40, 40), (100, 80, 220, 140)) pygame.draw.rect(fenetre, (40, 90, 200), (420, 80, 220, 140), 6) pygame.draw.circle(fenetre, (30, 140, 90), (250, 420), 90) pygame.draw.line(fenetre, (120, 60, 160), (420, 340), (720, 500), 8) pygame.display.flip() pygame.time.wait(5000) pygame.quit()
Observe le deuxième rectangle : il est creux, alors que le premier est plein. La différence tient au dernier nombre, 6, qui indique l’épaisseur du trait. Sans lui, la forme est remplie.
| Instruction | Arguments | Ce qu’elle trace |
|---|---|---|
pygame.draw.rect | surface, couleur, (x, y, largeur, hauteur) | un rectangle dont le coin supérieur gauche est en (x, y) |
pygame.draw.circle | surface, couleur, (x, y), rayon | un disque dont le centre est en (x, y) |
pygame.draw.line | surface, couleur, départ, arrivée, épaisseur | un segment entre deux points |
pygame.draw.ellipse | surface, couleur, (x, y, largeur, hauteur) | une ellipse inscrite dans ce rectangle |
💡 Le savais-tu ?
Ajouter un dernier nombre à rect ou à circle transforme la forme pleine en contour. pygame.draw.rect(fenetre, couleur, (10, 10, 50, 50), 3) trace un cadre de trois pixels d’épaisseur.
Une conséquence utile en classe : dessiner des contours au lieu de formes pleines réduit fortement la consommation d’encre quand tu imprimes une capture de ton jeu.
🧵 Fil rouge
Tu sais peindre une image fixe. Un jeu, lui, change à chaque instant. Le chapitre suivant introduit le mécanisme qui fait battre tous les jeux du monde.
🚀 À toi de jouer
Défi 4.1. Quelle couleur donne le triplet (255, 255, 0) ? Réponds avant d’essayer, en t’aidant de la figure 3.
Défi 4.2. Dessine un drapeau français : trois rectangles verticaux de largeur égale dans une fenêtre de 600 sur 400. Calcule la largeur de chaque bande avant de coder.
Défi 4.3. Dessine un bonhomme de neige : trois disques alignés verticalement, de rayons décroissants, plus deux petits disques noirs pour les yeux.
📋 Récapitulatif
- Une couleur se décrit par trois entiers de 0 à 255, dans l’ordre rouge, vert, bleu.
- Le mélange est additif : les trois au maximum donnent du blanc.
- Le nombre de couleurs possibles vaut 256 × 256 × 256, soit 16 777 216.
rectse repère par son coin supérieur gauche,circlepar son centre.- Un dernier argument numérique transforme une forme pleine en contour.
Chapitre 5
🔁 La boucle de jeu
Le cœur de tout jeu vidéo, sans exception. Trois gestes répétés soixante fois par seconde.
🎯 Point de départ
Le programme du chapitre 4 affiche son dessin, attend cinq secondes, puis se ferme. Un vrai jeu, lui, reste ouvert aussi longtemps que le joueur le souhaite, et son image change sans arrêt.
Réfléchis avant de lire. Avec les outils Python que tu connais déjà, quelle structure permettrait de répéter quelque chose un nombre de fois inconnu à l’avance ? Écris ta réponse en un mot.
La réponse est la boucle while. Elle répète un bloc tant qu’une condition reste vraie. Ici, la condition sera « le joueur n’a pas demandé à partir ».
Les trois temps, dans cet ordre
À chaque tour de boucle, un jeu accomplit trois gestes, dans le même ordre, et cet ordre ne varie pas.
📘 Définition
La boucle de jeu est une boucle qui répète sans interruption trois étapes :
- Écouter ce que fait le joueur : touches, souris, fermeture de la fenêtre.
- Calculer les conséquences : nouvelles positions, score, collisions.
- Dessiner l’image qui en résulte, puis l’afficher.
Un tour complet produit une image, en anglais frame. Un jeu fluide en produit environ soixante par seconde.
🔬 À l’essai
Fais-le avec tes mains, sans ordinateur. Prends trois petits papiers et écris dessus « écouter », « calculer », « dessiner ». Pose-les en triangle sur la table.
Prends un jeton et fais-lui parcourir le triangle, dans l’ordre, encore et encore. Chaque tour complet du jeton, c’est une image affichée. Pour égaler un jeu véritable, ta main devrait accomplir ce circuit soixante fois par seconde.
Cette petite mise en scène vaut mieux qu’une explication : la boucle de jeu n’est rien d’autre que ce circuit, parcouru très vite.
Programme 6 : la boucle de jeu, réduite à l’essentiel
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("La boucle de jeu") en_marche = True while en_marche: # 1. ECOUTER for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False # 2. CALCULER # (rien ne bouge encore) # 3. DESSINER fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (40, 90, 200), (400, 300), 80) pygame.display.flip() pygame.quit()
Cette fenêtre reste ouverte jusqu’à ce que tu cliques sur la croix de fermeture. La pause devient inutile.
Décortiquons la nouveauté. La variable en_marche vaut True au départ. La boucle tourne tant qu’elle le reste. Quand Pygame signale que le joueur a cliqué sur la croix, nous la faisons passer à False, et le tour suivant n’a pas lieu.
📜 Histoire vraie
En 1977, Atari met en vente une console de salon, la 2600. À l’intérieur : un processeur cadencé à environ 1,19 mégahertz et 128 octets de mémoire vive. Pas 128 kilo-octets : 128 octets, de quoi stocker à peine la longueur d’un message court.
Cette mémoire était trop petite pour contenir l’image. Les programmeurs devaient donc composer chaque ligne de l’écran au moment précis où le faisceau la traçait, en réglant des registres au passage. Cette pratique porte un nom resté célèbre : racing the beam, courir après le faisceau.
Leur boucle de jeu se confondait avec le balayage de l’écran. La tienne est bien plus confortable : Pygame prépare l’image à l’abri, et flip la présente d’un coup.
| Temps | Dans le programme 6 | Ce qui s’y ajoutera plus tard |
|---|---|---|
| 1. Écouter | for evenement in pygame.event.get() | les touches du clavier, au chapitre 6 |
| 2. Calculer | rien pour l’instant | le déplacement, les chocs, le score |
| 3. Dessiner | fill, draw, flip | le texte du score, au chapitre 11 |
💡 Le savais-tu ?
Pourquoi repeindre toute la fenêtre à chaque tour, avec fill, plutôt que d’effacer seulement ce qui a bougé ?
Parce que repeindre est simple et sûr. Si tu retires le fill d’un programme où un objet se déplace, l’objet laisse une traînée derrière lui : ses positions anciennes restent affichées. Certains jeux exploitent volontairement cet effet pour dessiner des traces, mais cela se décide, cela ne s’improvise pas.
🧵 Fil rouge
La boucle tourne, mais elle n’écoute encore qu’une seule chose : la fermeture de la fenêtre. Au chapitre suivant, nous lui apprenons à entendre le clavier.
🚀 À toi de jouer
Défi 5.1. Dans le programme 6, remplace la couleur de fond par du jaune pâle et le cercle par un rectangle. La boucle doit rester intacte.
Défi 5.2. Supprime la ligne en_marche = False. Exécute, essaie de fermer la fenêtre, puis explique ce que tu observes. Pour reprendre la main, presse les touches Ctrl et C dans le terminal.
Défi 5.3. Ajoute un compteur qui augmente de 1 à chaque tour de boucle et affiche sa valeur dans le terminal avec print. Laisse tourner cinq secondes : combien de tours ton ordinateur a-t-il accomplis ?
📋 Récapitulatif
- Un jeu répète trois gestes : écouter, calculer, dessiner.
- Cette répétition s’écrit avec une boucle
whilepilotée par une variable vraie ou fausse. - Un tour complet de boucle produit une image.
- L’événement
pygame.QUITsignale le clic sur la croix de fermeture. - Repeindre tout le fond à chaque tour évite les traînées.
Chapitre 6
⌨️ Les événements et le clavier
Comment ton programme apprend que tu viens d’appuyer sur une touche.
🎯 Point de départ
Ton programme calcule des milliers de fois par seconde. Ta main, elle, appuie sur une touche à un instant imprévisible.
Question à creuser avant de lire. Comment un programme peut-il ne rien manquer d’une action qui survient à n’importe quel moment, alors qu’il ne regarde le clavier qu’une fois par tour de boucle ? Propose une idée.
La solution employée par Pygame est celle d’une boîte aux lettres. Quand tu appuies sur une touche, le système ne dérange pas ton programme : il dépose un message dans une file d’attente. Ton programme relève sa boîte une fois par tour et traite tout ce qui s’y trouve.
📘 Définition
Un événement est un message décrivant une action : une touche pressée, la souris déplacée, la fenêtre fermée.
Les événements s’accumulent dans une file d’attente. pygame.event.get() vide cette file et te rend la liste des messages en attente.
Chaque événement possède un type, qui dit de quoi il s’agit : pygame.QUIT, pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN.
Deux façons de lire le clavier, et elles ne servent pas au même usage
Voici le point qui mérite toute ton attention. Pygame propose deux méthodes, et les confondre produit des jeux au comportement étrange.
KEYDOWN dans la file | pygame.key.get_pressed() | |
|---|---|---|
| Ce qu’elle rapporte | l’instant où la touche est enfoncée | l’état de la touche à cet instant |
| Nombre de déclenchements | une seule fois par appui | à chaque tour, tant que la touche reste enfoncée |
| Usage adapté | tirer, sauter, valider, mettre en pause | marcher, avancer, tourner |
| Si tu te trompes | le personnage avance d’un pas puis se fige | le personnage tire des centaines de fois par seconde |
🔬 À l’essai
Éprouve la différence. Maintiens la flèche droite enfoncée dans un jeu quelconque. Ton personnage avance de façon continue : c’est get_pressed qui travaille.
Maintiens maintenant la touche de tir. Si le personnage tire un coup unique, le jeu écoute KEYDOWN. S’il arrose sans discontinuer, le jeu lit l’état de la touche.
Ce simple test te permet de deviner, dans presque tous les jeux, quelle méthode le programmeur a choisie.
Programme 7 : un carré piloté aux quatre flèches
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Le clavier") x = 370 y = 270 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False touches = pygame.key.get_pressed() if touches[pygame.K_LEFT]: x = x - 3 if touches[pygame.K_RIGHT]: x = x + 3 if touches[pygame.K_UP]: y = y - 3 if touches[pygame.K_DOWN]: y = y + 3 fenetre.fill((255, 255, 255)) pygame.draw.rect(fenetre, (200, 40, 40), (x, y, 60, 60)) pygame.display.flip() pygame.quit()
Les quatre tests sont indépendants, ce qui autorise les déplacements en diagonale : deux flèches enfoncées ensemble modifient x et y au même tour.
Note bien l’emplacement de pygame.key.get_pressed() : dans la boucle, après la relève des événements, et avant le dessin. C’est le temps « calculer » de notre cycle.
📜 Histoire vraie
En 1986, la société japonaise Konami adapte pour la console NES son jeu d’arcade Gradius. Le portage est confié au programmeur Kazuhisa Hashimoto. Un ennui se présente : le jeu est si difficile que Hashimoto échoue à le terminer, ce qui l’empêche de tester les derniers niveaux.
Il ajoute alors une séquence secrète de touches qui lui accorde immédiatement tous les équipements : haut, haut, bas, bas, gauche, droite, gauche, droite, B, A. Dix événements de clavier, dans un ordre précis.
Cette séquence est restée dans la version vendue. Les récits diffèrent sur le motif exact de ce maintien. Elle fut reprise dans quantité de jeux et devint le code de triche le plus célèbre de l’histoire du jeu vidéo. Hashimoto, né en 1958, est mort en 2020.
💡 Le savais-tu ?
Les constantes de touches portent des noms courts commençant par K_ : K_LEFT, K_RIGHT, K_UP, K_DOWN, K_SPACE, K_ESCAPE, K_a à K_z.
Ces constantes sont des nombres entiers, souvent très grands. K_LEFT vaut par exemple 1073741904. Retenir le nom suffit largement : le nombre, lui, ne présente pas d’intérêt.
🧵 Fil rouge
Le carré du programme 7 se déplace parce que tu appuies. Au chapitre suivant, nous faisons bouger un objet sans que personne n’appuie sur quoi que ce soit.
🚀 À toi de jouer
Défi 6.1. Ajoute au programme 7 une sortie par la touche Échap : quand K_ESCAPE est pressée, en_marche passe à False.
Défi 6.2. Fais changer la couleur du carré quand la barre d’espace est maintenue enfoncée.
Défi 6.3. Ajoute un test sur pygame.KEYDOWN qui affiche dans le terminal, avec print, un message à chaque appui. Compare le nombre de messages obtenus en tapotant une touche et en la maintenant : que conclus-tu ?
📋 Récapitulatif
- Les actions du joueur deviennent des événements déposés dans une file d’attente.
pygame.event.get()vide la file et rend la liste des événements.KEYDOWNsignale l’instant de l’appui : une fois par appui.pygame.key.get_pressed()donne l’état des touches : à chaque tour.- Action ponctuelle : la file. Mouvement continu : l’état.
Chapitre 7
🏃 Le mouvement
Rien ne bouge réellement à l’écran. Tout tient dans une variable qui change.
🎯 Point de départ
Au chapitre 6, le carré se déplaçait parce que tu appuyais sur une touche. Nous voulons maintenant un objet qui traverse la fenêtre tout seul, sans que personne n’y touche.
Avant de lire. Tu disposes d’une boucle qui tourne des dizaines de fois par seconde et d’une instruction qui trace un cercle à la position x. Que faudrait-il faire, à chaque tour, pour que le cercle avance ? Une seule ligne suffit.
La réponse tient en effet en une ligne : x = x + 4. Elle mérite un instant d’attention, car elle choque le lecteur habitué aux yeux du mathématicien.
📘 Définition
En mathématiques, x = x + 4 serait une équation sans solution. En programmation, le signe égal n’annonce pas une égalité : c’est une affectation.
La machine lit la ligne de droite à gauche. Elle calcule d’abord x + 4 avec l’ancienne valeur, puis range le résultat dans x, qui perd son ancienne valeur.
Si x valait 100, il vaut 104 après cette ligne.
Programme 8 : un cercle qui traverse la fenêtre
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Le mouvement") x = 0 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False x = x + 4 fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (40, 90, 200), (x, 300), 40) pygame.display.flip() pygame.quit()
Le cercle part du bord gauche, poursuit sa route, sort à droite et ne revient pas. Nous corrigerons cela au chapitre 9.
Suivons la valeur de x tour après tour. Elle vaut 0 au départ, puis 4, puis 8, puis 12. Le cercle est redessiné un peu plus à droite chaque fois, et ton œil interprète cette succession comme un déplacement.
| Tour de boucle | Valeur de x | Position du cercle |
|---|---|---|
| 1 | 4 | presque collé au bord gauche |
| 2 | 8 | 4 pixels plus à droite |
| 3 | 12 | 4 pixels plus à droite |
| 100 | 400 | au milieu de la fenêtre |
| 200 | 800 | il sort par la droite |
| 300 | 1200 | il est loin hors de la fenêtre, et continue |
🔬 À l’essai
Vérifie-le toi-même. Ajoute la ligne print(x) juste après x = x + 4, puis laisse tourner le programme une dizaine de secondes.
Le terminal affiche des valeurs qui dépassent largement 800, puis 10 000, puis davantage. Le cercle existe encore : il est simplement dessiné hors de la surface, et Pygame découpe silencieusement, comme au chapitre 3.
📜 Histoire vraie
Le 18 octobre 1958, au laboratoire national de Brookhaven, dans l’État de New York, le physicien William Higinbotham présente aux visiteurs une curiosité. Les journées portes ouvertes du laboratoire l’ennuient : panneaux fixes, photographies, rien à toucher.
En lisant le manuel d’un calculateur analogique Donner Model 30, il remarque que la machine sait calculer la trajectoire d’un projectile avec résistance de l’air. Il conçoit alors en quelques heures, puis construit en trois semaines avec le technicien Robert V. Dvorak, un jeu de tennis affiché sur un oscilloscope de cinq pouces.
Deux boîtiers munis d’un bouton rotatif et d’un poussoir permettent de renvoyer la balle. Les visiteurs font la queue. Tennis for Two est souvent présenté comme le premier jeu vidéo, une paternité que les historiens discutent selon la définition retenue.
Ce qui nous intéresse ici : la position de cette balle était recalculée en permanence, comme ton x = x + 4. Le principe a soixante-sept ans.
💡 Le savais-tu ?
Pourquoi ton œil voit-il un déplacement continu là où il n’y a que des images fixes successives ?
L’explication la plus répandue invoque la « persistance rétinienne », l’idée que l’œil garderait chaque image imprimée un instant. Cette explication est contestée depuis longtemps : le psychologue Max Wertheimer l’a réfutée dès 1912, dans le contexte du cinéma.
Les chercheurs attribuent aujourd’hui l’illusion à deux phénomènes de perception, l’effet phi et le mouvement bêta, qui se produisent dans le cerveau plutôt que dans l’œil. La persistance rétinienne existe, mais elle explique le scintillement, pas le mouvement. Le mythe survit pourtant dans beaucoup de manuels.
🧵 Fil rouge
Ton cercle avance. Mais sur un ordinateur récent il file, et sur une machine plus lente il se traîne. Le chapitre suivant règle ce problème.
🚀 À toi de jouer
Défi 7.1. Fais descendre le cercle du haut vers le bas au lieu de le faire aller vers la droite. Quelle variable modifies-tu, et dans quel sens ?
Défi 7.2. Fais avancer le cercle en diagonale, vers le bas et vers la droite en même temps.
Défi 7.3. Remplace x = x + 4 par x = x + 1, puis par x = x + 20. Décris en une phrase ce que change ce nombre.
📋 Récapitulatif
- Le mouvement à l’écran est une suite de positions, redessinées chaque tour.
x = x + 4est une affectation : la droite est calculée, puis rangée à gauche.- Le nombre ajouté à chaque tour est la vitesse, exprimée en pixels par tour.
- Un objet sorti de la fenêtre continue d’exister, invisible.
- L’illusion de mouvement se produit dans le cerveau, non dans l’œil.
Chapitre 8
⏱️ L’horloge et la vitesse
Sans horloge, ton jeu va aussi vite que la machine qui l’exécute. C’est un défaut.
🎯 Point de départ
Imagine que tu envoies le programme 8 à deux camarades. Le premier possède un ordinateur récent et rapide, le second une vieille machine familiale.
Question. Le cercle traversera-t-il la fenêtre à la même vitesse chez les deux ? Justifie ta réponse en une phrase, puis lis la suite.
La réponse est non, et la raison est simple. Ta boucle tourne aussi vite que la machine le permet. Un ordinateur rapide accomplit peut-être 3000 tours par seconde, une machine lente 400. Comme chaque tour ajoute 4 pixels, le cercle du premier file sept ou huit fois plus vite.
Il faut donc brider la boucle : lui imposer un nombre de tours par seconde identique partout.
📘 Définition
pygame.time.Clock() crée une horloge, que nous rangeons dans une variable.
horloge.tick(60), placé à la fin de la boucle, endort le programme juste assez pour que le tour complet dure environ un soixantième de seconde.
Le nombre d’images produites en une seconde s’appelle la fréquence d’images, souvent notée en anglais frames per second, abrégé en FPS.
Programme 9 : le même cercle, mais à vitesse maîtrisée
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("L'horloge") horloge = pygame.time.Clock() x = 0 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False x = x + 4 fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (40, 90, 200), (x, 300), 40) pygame.display.flip() horloge.tick(60) pygame.quit()
Deux lignes ajoutées seulement : la création de l’horloge avant la boucle, et l’appel à tick en fin de tour.
🔬 À l’essai
Mesure réalisée pour ce cours. Le programme 9 a été exécuté sur 1200 tours avec tick(60). La fréquence mesurée par horloge.get_fps() s’est établie à 61,7 images par seconde.
Ce chiffre mérite que tu t’y arrêtes. tick(60) n’est pas une garantie de soixante : c’est un plafond. La fonction empêche d’aller plus vite, et la valeur obtenue oscille légèrement autour de la cible selon la charge de la machine.
Reproduis la mesure toi-même : ajoute print(horloge.get_fps()) dans la boucle.
| Réglage | Effet sur l’image | Effet sur la machine |
|---|---|---|
pas de tick | vitesse différente sur chaque ordinateur | le processeur tourne à fond sans raison |
tick(30) | mouvement un peu saccadé | charge légère |
tick(60) | fluide, valeur usuelle des jeux | charge modérée |
tick(120) | très fluide, visible sur les écrans rapides | charge deux fois plus élevée |
📜 Histoire vraie
En 1978, l’ingénieur japonais Tomohiro Nishikado achève pour la société Taito un jeu dont il a tout fait : le concept, les dessins, le son, le programme, et jusqu’au matériel de la borne. Ce jeu s’appelle Space Invaders.
Cinquante-cinq créatures descendent vers le joueur. Nishikado constate pendant ses essais qu’elles avancent plus lentement qu’il ne l’avait prévu : le processeur peine à redessiner cinquante-cinq objets et à vérifier leurs collisions à chaque image.
Mais à mesure que le joueur en détruit, la machine a moins de travail et les survivantes accélèrent. La tension monte toute seule, et le rythme sonore suit. Nishikado aurait pu corriger ce défaut. Il a préféré le garder.
Cette accélération, devenue l’une des mécaniques de jeu les plus célèbres de l’histoire, n’était donc au départ qu’une boucle de jeu mal bridée. Ton tick(60) est précisément ce qui l’aurait empêchée.
💡 Le savais-tu ?
tick ne se contente pas d’attendre : il renvoie le nombre de millisecondes écoulées depuis l’appel précédent. Écrire ecoule = horloge.tick(60) donne accès à cette durée.
Les programmeurs de jeux professionnels s’en servent pour rendre le mouvement indépendant de la fréquence : au lieu d’ajouter 4 pixels par tour, ils ajoutent une distance proportionnelle au temps écoulé. Cette technique dépasse le cadre de ce cours, mais tu sais désormais qu’elle existe et à quoi elle sert.
🧵 Fil rouge
La vitesse est maîtrisée. Reste que notre cercle sort de la fenêtre et n’en revient pas. Le chapitre suivant lui pose des murs.
🚀 À toi de jouer
Défi 8.1. Règle l’horloge à 10 images par seconde et décris ce que tu vois.
Défi 8.2. À 60 images par seconde et 4 pixels par tour, calcule combien de secondes le cercle met pour traverser une fenêtre de 800 pixels de large. Vérifie ensuite au chronomètre.
Défi 8.3. Garde tick(60) mais remplace x = x + 4 par x = x + 2. La traversée dure combien de temps ? Compare avec ta prévision.
📋 Récapitulatif
- Sans horloge, la vitesse du jeu dépend de la machine.
pygame.time.Clock()crée l’horloge,horloge.tick(60)la fait respecter.tickimpose un plafond : la mesure donne environ 60, et non 60 précisément.tickse place à la fin de la boucle.- La vitesse d’un objet se règle par le nombre de pixels ajoutés à chaque tour.
Chapitre 9
🧱 Les bords de la fenêtre
Deux façons de traiter un mur : s’y arrêter, ou rebondir dessus.
🎯 Point de départ
Reprends mentalement le programme 9. Le cercle sort par la droite et poursuit sa course dans le vide.
Réfléchis. Écris en français, sans code, la condition qui devrait déclencher une réaction. « Si le cercle... alors il faut... ». Deux réponses différentes sont possibles, et nous les étudierons toutes les deux.
Première solution : s’arrêter au mur
La règle s’énonce ainsi : si la position dépasse la limite, ramène-la à la limite. Attention, la limite n’est pas 0 et 800, car un cercle est repéré par son centre : un centre posé sur 0 laisse la moitié du disque hors de la fenêtre.
Pour un rayon de 40, le centre peut descendre jusqu’à 40 et monter jusqu’à 800 moins 40, soit 760.
Programme 10 : un cercle qui refuse de sortir
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Rester dans la fenêtre") horloge = pygame.time.Clock() x = 370 RAYON = 40 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False touches = pygame.key.get_pressed() if touches[pygame.K_LEFT]: x = x - 6 if touches[pygame.K_RIGHT]: x = x + 6 if x < RAYON: x = RAYON if x > 800 - RAYON: x = 800 - RAYON fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (40, 90, 200), (x, 300), RAYON) pygame.display.flip() horloge.tick(60) pygame.quit()
Les deux tests de limite se placent après les déplacements et avant le dessin : ils corrigent la position avant qu’elle soit affichée.
📘 Définition
Contraindre une valeur, c’est la ramener dans un intervalle autorisé dès qu’elle en sort.
Le schéma est constant : si valeur < minimum, alors valeur = minimum, et symétriquement pour le maximum.
Seconde solution : rebondir
Le rebond demande une idée de plus. La vitesse devient une variable, et toucher un bord inverse son signe. Une vitesse de 5 devient -5, et l’objet repart dans l’autre sens.
Programme 11 : le rebond
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Le rebond") horloge = pygame.time.Clock() x = 400 vitesse = 5 RAYON = 40 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False x = x + vitesse if x < RAYON: x = RAYON vitesse = -vitesse if x > 800 - RAYON: x = 800 - RAYON vitesse = -vitesse fenetre.fill((255, 255, 255)) pygame.draw.circle(fenetre, (200, 40, 40), (x, 300), RAYON) pygame.display.flip() horloge.tick(60) pygame.quit()
🔬 À l’essai
Observe l’ordre des deux lignes dans le programme 11. Nous replaçons d’abord le cercle sur la limite, puis nous inversons la vitesse.
Supprime la ligne x = RAYON et relance. Le cercle se met à trembler contre le bord au lieu de repartir. La raison : sans repositionnement, il reste au-delà de la limite, le test se déclenche à nouveau au tour suivant, et la vitesse s’inverse sans arrêt.
Ce défaut porte un nom chez les programmeurs de jeux : l’objet reste « collé » au mur. Corriger la position avant d’inverser la vitesse le résout.
| S’arrêter | Rebondir | |
|---|---|---|
| Ce qui change | la position seulement | la position et le signe de la vitesse |
| Variables nécessaires | la position | la position et une vitesse |
| Usage typique | un personnage dirigé par le joueur | une balle, un projectile |
| Piège | oublier le rayon ou la largeur | inverser la vitesse sans replacer l’objet |
📜 Histoire vraie
En 1972, Nolan Bushnell confie à un ingénieur fraîchement recruté, Allan Alcorn, un exercice d’entraînement : programmer un jeu de tennis. Alcorn ignore que c’est un test. Il livre Pong.
Pour piloter la hauteur des raquettes, il emploie un circuit minuteur très simple, le fameux 555. Ce circuit présente une limite : les raquettes n’atteignent pas le haut de l’écran. Il reste une zone inaccessible.
Alcorn raconta plus tard qu’il avait découvert, en jouant avec Bushnell, que ce défaut améliorait le jeu. Sans lui, deux joueurs habiles auraient pu échanger la balle indéfiniment ; grâce à lui, envoyer la balle dans cette zone devenait une tactique gagnante.
Il n’a pas corrigé le circuit. De cet épisode, il tira une devise restée célèbre chez les développeurs : si tu ne peux pas le réparer, appelle-le une fonctionnalité.
Un prototype fut installé en septembre 1972 dans un bar de Sunnyvale, en Californie. Quelques jours plus tard, le gérant appela pour signaler une panne : le monnayeur débordait de pièces.
💡 Le savais-tu ?
Les bords de ta fenêtre ne sont pas les seuls murs possibles. En changeant les deux nombres du test, tu délimites n’importe quelle zone de jeu : un terrain, un couloir, une arène plus petite que la fenêtre.
Beaucoup de jeux affichent d’ailleurs un décor plus large que la zone réellement praticable, ce qui donne de la profondeur sans agrandir le terrain.
🧵 Fil rouge
Ton objet respecte les murs. Il lui reste à remarquer les autres objets. C’est le chapitre suivant, et c’est là que le jeu commence vraiment.
🚀 À toi de jouer
Défi 9.1. Fais rebondir le cercle à la fois horizontalement et verticalement, en ajoutant une vitesse verticale sur le modèle de l’horizontale.
Défi 9.2. Fais grandir le rayon de 2 à chaque rebond. Que se passe-t-il au bout d’une minute, et pourquoi ?
Défi 9.3. Fais réapparaître le cercle par la gauche quand il sort par la droite, à la manière des jeux d’arcade. Une seule ligne suffit, et elle ne contient pas de signe moins.
📋 Récapitulatif
- Un cercle est repéré par son centre : la limite tient compte du rayon.
- S’arrêter revient à ramener la position sur la limite.
- Rebondir revient à inverser le signe de la vitesse.
- Il faut replacer l’objet sur la limite avant d’inverser sa vitesse.
- Les tests de bord se placent après le déplacement et avant le dessin.
Chapitre 10
💥 Les collisions
Comment un programme sait que deux objets se touchent. La réponse tient en un rectangle.
🎯 Point de départ
Deux carrés se déplacent dans la fenêtre. Le programme doit savoir s’ils se touchent.
Cherche d’abord. Tu connais les coordonnées des deux carrés et leurs dimensions. Essaie d’écrire, en français, la condition qui dit que les deux se chevauchent. Tu verras qu’elle est plus longue à formuler que prévu, et c’est justement pourquoi Pygame l’a écrite pour toi.
La condition complète compare quatre paires de valeurs. Elle est fastidieuse à écrire et facile à rater. Pygame fournit donc un objet qui la contient : le rectangle.
📘 Définition
Un Rect est un rectangle défini par quatre nombres : pygame.Rect(x, y, largeur, hauteur), où (x, y) est le coin supérieur gauche.
Il connaît ses propres repères : .left, .right, .top, .bottom, .centerx, .centery, .center.
Modifier l’un de ces repères déplace le rectangle entier.
🔬 À l’essai
Un piège utile à connaître. Un Rect ne retient que des nombres entiers. Écrire rect.y = 10.5 range en réalité 11, car Pygame arrondit au plus proche.
Les conséquences se mesurent. Avec une vitesse de 0,4 pixel par tour, le rectangle relit 10, ajoute 0,4, obtient 10,4, arrondit à 10 et réécrit 10 : il ne bouge pas du tout. Avec une vitesse de 0,5, il avance de 1 pixel par tour, soit le double de ce que tu voulais.
Ces deux résultats ont été mesurés avec Pygame 2.6.1. La règle à retenir : garde des vitesses entières, ou range la position exacte dans une variable décimale séparée et ne recopie dans le Rect qu’au moment de dessiner.
Détecter le chevauchement
La méthode qui remplace quatre comparaisons
if joueur.colliderect(tresor): # les deux rectangles se chevauchent score = score + 1
Une précision décisive, et vérifiée : colliderect exige un chevauchement réel. Deux rectangles qui se touchent bord contre bord renvoient False.
Programme 12 : un trésor qui s’illumine au contact
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("La collision") horloge = pygame.time.Clock() joueur = pygame.Rect(370, 480, 60, 60) tresor = pygame.Rect(370, 120, 50, 50) en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False touches = pygame.key.get_pressed() if touches[pygame.K_LEFT]: joueur.x = joueur.x - 6 if touches[pygame.K_RIGHT]: joueur.x = joueur.x + 6 if touches[pygame.K_UP]: joueur.y = joueur.y - 6 if touches[pygame.K_DOWN]: joueur.y = joueur.y + 6 if joueur.colliderect(tresor): couleur_tresor = (220, 160, 20) else: couleur_tresor = (160, 170, 180) fenetre.fill((255, 255, 255)) pygame.draw.rect(fenetre, couleur_tresor, tresor) pygame.draw.rect(fenetre, (40, 90, 200), joueur) pygame.display.flip() horloge.tick(60) pygame.quit()
Déplace le carré bleu avec les quatre flèches. Le trésor passe du gris à l’or dès que les deux rectangles se chevauchent.
📜 Histoire vraie
Sur la console Atari 2600 de 1977, la détection des collisions n’était pas écrite par le programmeur : elle était câblée dans le circuit d’affichage, la puce TIA.
Ce composant surveillait, pendant qu’il traçait l’image, si deux des six objets qu’il savait afficher se superposaient. Le programmeur lisait ensuite un registre pour savoir lesquels s’étaient rencontrés.
La contrainte inverse s’appliquait donc à celle d’aujourd’hui : détecter une collision coûtait presque rien, mais la console ne savait afficher que deux joueurs, deux missiles, une balle et un décor. Six objets, pas un de plus.
Ton colliderect est plus lent qu’un circuit dédié. En échange, tu peux comparer autant de rectangles que tu veux.
💡 Le savais-tu ?
Dans les jeux professionnels, le rectangle de collision ne coïncide pas avec le dessin visible. Il porte un nom : la hitbox, littéralement la boîte de contact.
Les concepteurs la règlent avec soin. Une hitbox légèrement plus petite que le personnage rend le jeu plus indulgent : le joueur a l’impression d’avoir évité le projectile de justesse. Une hitbox plus grande que l’ennemi rend les tirs plus satisfaisants. Ces réglages, invisibles, décident en grande partie du plaisir de jeu.
| Écriture | Ce qu’elle donne |
|---|---|
r = pygame.Rect(100, 50, 200, 120) | crée le rectangle |
r.x = r.x + 5 | déplace de 5 pixels vers la droite |
r.left = 0 | colle le rectangle au bord gauche |
r.right = 800 | colle le rectangle au bord droit |
r.center = (400, 300) | centre le rectangle dans la fenêtre |
a.colliderect(b) | True si a et b se chevauchent |
r.collidepoint(x, y) | True si le point est dedans |
pygame.draw.rect(fenetre, couleur, r) | dessine directement le Rect |
pygame.draw.rect, sans recopier ses quatre nombres.🧵 Fil rouge
Le trésor s’illumine, mais rien ne compte les prises. Au chapitre suivant, nous affichons un score dans la fenêtre.
🚀 À toi de jouer
Défi 10.1. Ajoute au programme 12 un second trésor, placé ailleurs, avec son propre test de collision.
Défi 10.2. Empêche le joueur de sortir de la fenêtre, en réutilisant la méthode du chapitre 9 avec .left, .right, .top et .bottom.
Défi 10.3. Fais disparaître le trésor au contact, en le déplaçant à une position aléatoire. Tu auras besoin de import random et de random.randint.
📋 Récapitulatif
- Un
Rectregroupe position et dimensions en un seul objet. - Il expose
.left,.right,.top,.bottom,.center. a.colliderect(b)teste le chevauchement de deux rectangles.- Un simple contact bord contre bord renvoie
False. - Un
Rectarrondit ses coordonnées à l’entier le plus proche.
Chapitre 11
🔢 Le texte et le score
Écrire dans la fenêtre demande trois gestes, et pas un seul.
🎯 Point de départ
Tu sais déjà afficher un nombre avec print. Essaie-le dans un programme Pygame et regarde où le nombre apparaît.
Constat à faire avant de lire. Le nombre s’affiche dans le terminal, pas dans la fenêtre. Le joueur, lui, regarde la fenêtre. Nous avons donc besoin d’autre chose.
Écrire du texte dans une fenêtre Pygame se fait en trois gestes, et chacun a sa raison d’être.
| Geste | Instruction | Quand l’exécuter |
|---|---|---|
| 1. Choisir une police | police = pygame.font.SysFont(None, 40) | une seule fois, avant la boucle |
| 2. Fabriquer l’image du texte | texte = police.render("Score : 3", True, couleur) | à chaque tour, dans la boucle |
| 3. Poser cette image | fenetre.blit(texte, (20, 20)) | à chaque tour, juste après |
📘 Définition
render ne dessine rien : il fabrique une surface contenant le texte, comme une petite image découpée.
blit colle une surface sur une autre, à la position indiquée. Le mot vient de l’anglais block transfer, transfert de bloc.
Le deuxième argument de render, ici True, demande le lissage des contours des lettres, ce qui rend le texte plus net à l’œil.
Programme 13 : un score qui monte à chaque appui sur la barre d’espace
import pygame pygame.init() fenetre = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Le score") horloge = pygame.time.Clock() police = pygame.font.SysFont(None, 40) score = 0 en_marche = True while en_marche: for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False if evenement.type == pygame.KEYDOWN: if evenement.key == pygame.K_SPACE: score = score + 1 fenetre.fill((255, 255, 255)) texte = police.render("Score : " + str(score), True, (30, 40, 60)) fenetre.blit(texte, (20, 20)) pygame.display.flip() horloge.tick(60) pygame.quit()
Remarque l’emploi de KEYDOWN plutôt que de get_pressed : compter un point est une action ponctuelle, conformément au tableau du chapitre 6.
Un détail de syntaxe mérite attention : "Score : " + str(score). Python refuse d’additionner du texte et un nombre. str transforme le nombre en texte, ce qui rend l’addition possible. Sans lui, Python signale une TypeError.
🔬 À l’essai
Éprouve le lissage. Remplace True par False dans render, puis relance en agrandissant la taille de police à 80.
Les bords des lettres deviennent dentelés, en marches d’escalier. Avec True, Pygame ajoute des pixels intermédiaires légèrement teintés qui adoucissent la transition. Ce procédé porte le nom d’anticrénelage.
📜 Histoire vraie
Dans le Pac-Man d’arcade de 1980, le numéro du niveau est rangé dans un seul octet. Un octet contient huit chiffres binaires, donc des valeurs de 0 à 255. Le compteur démarre à 0 : le niveau 256 y est donc rangé sous la valeur 255.
Au début de chaque niveau, le jeu dessine en bas de l’écran les petits fruits qui rappellent la progression. Pour cela, il ajoute 1 au numéro du niveau. Au niveau 256, il ajoute donc 1 à 255. L’octet déborde et le résultat retombe à 0.
La routine d’affichage, que personne n’avait prévue pour la valeur 0, interprète alors des données quelconques comme des consignes de dessin et couvre la moitié droite de l’écran de symboles incohérents. Il faut manger 244 pastilles pour finir un niveau ; la partie devient impossible à terminer.
Ce niveau infranchissable porte un nom chez les joueurs : l’écran de la mort. Un compteur mal dimensionné a ainsi fixé la fin d’un jeu conçu pour durer sans terme.
💡 Le savais-tu ?
render fabrique une nouvelle surface à chaque appel. L’exécuter soixante fois par seconde alors que le score ne change pas représente un gaspillage.
Les programmeurs expérimentés ne refabriquent le texte qu’au moment où sa valeur change. Pour un jeu de la taille du nôtre, la différence reste imperceptible, et la simplicité l’emporte. Retiens l’idée pour le jour où ton jeu affichera vingt textes à la fois.
🧵 Fil rouge
Tu possèdes maintenant les onze pièces. Le chapitre suivant les assemble en un jeu complet, du premier import à l’écran de fin.
🚀 À toi de jouer
Défi 11.1. Affiche le score en haut à droite plutôt qu’en haut à gauche. Quelle position choisis-tu, et pourquoi le texte risque-t-il de dépasser ?
Défi 11.2. Ajoute un second texte affichant le nombre de tours de boucle écoulés depuis le début.
Défi 11.3. Fais passer la couleur du score au rouge quand il dépasse 10, et au vert quand il dépasse 20.
📋 Récapitulatif
- Écrire du texte demande trois gestes : choisir, fabriquer, poser.
SysFontse place avant la boucle,renderetblità l’intérieur.str(nombre)convertit un nombre en texte avant de l’ajouter à une phrase.Truedansrenderactive l’anticrénelage.- Un compteur rangé dans un espace trop petit finit par déborder.
Chapitre 12
🕹️ Le mini-jeu complet
Onze chapitres, onze pièces. Voici la machine montée.
🎯 Point de départ
Avant de lire le programme final, fais l’inventaire. Prends une feuille et liste les onze notions apprises depuis le début : une par chapitre.
Puis, pour chacune, écris en un mot à quel endroit du jeu tu penses la retrouver. Tu compareras ta liste au tableau de la page suivante.
La règle du jeu
Des gouttes tombent du haut de la fenêtre. Un panier, piloté aux flèches gauche et droite, doit les attraper. Chaque goutte attrapée vaut un point. Tous les cinq points, la chute accélère d’un pixel par tour. Cinq gouttes manquées et la partie se termine.
📘 Définition
Les noms écrits en majuscules, comme LARGEUR ou BLANC, sont des constantes : des valeurs que le programme ne modifie pas.
Python ne les protège pas : la majuscule est une convention entre programmeurs, respectée par tous. Elle sert à deux choses. Elle prévient le lecteur que la valeur ne bougera pas, et elle rassemble en tête de fichier tous les réglages du jeu, faciles à retrouver et à modifier.
Programme 14 : L’Attrapeur de gouttes
import pygame import random pygame.init() LARGEUR = 800 HAUTEUR = 600 BLANC = (255, 255, 255) BLEU = (40, 90, 200) BOIS = (150, 96, 42) ENCRE = (30, 40, 60) BRIQUE = (200, 40, 40) fenetre = pygame.display.set_mode((LARGEUR, HAUTEUR)) pygame.display.set_caption("L'Attrapeur de gouttes") horloge = pygame.time.Clock() police = pygame.font.SysFont(None, 40) grande_police = pygame.font.SysFont(None, 72) panier = pygame.Rect(370, 540, 90, 25) goutte = pygame.Rect(390, 0, 20, 30) vitesse_goutte = 4 score = 0 gouttes_perdues = 0 en_marche = True while en_marche: # 1. ECOUTER for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False 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 # 2. CALCULER if panier.left < 0: panier.left = 0 if panier.right > LARGEUR: panier.right = LARGEUR goutte.y = goutte.y + vitesse_goutte if panier.colliderect(goutte): score = score + 1 if score % 5 == 0: vitesse_goutte = vitesse_goutte + 1 goutte.y = 0 goutte.x = random.randint(0, LARGEUR - goutte.width) elif goutte.top > HAUTEUR: gouttes_perdues = gouttes_perdues + 1 goutte.y = 0 goutte.x = random.randint(0, LARGEUR - goutte.width) if gouttes_perdues >= 5: en_marche = False # 3. DESSINER fenetre.fill(BLANC) pygame.draw.rect(fenetre, BOIS, panier) pygame.draw.ellipse(fenetre, BLEU, goutte) texte_score = police.render("Score : " + str(score), True, ENCRE) texte_perdues = police.render("Perdues : " + str(gouttes_perdues), True, BRIQUE) fenetre.blit(texte_score, (20, 20)) fenetre.blit(texte_perdues, (560, 20)) pygame.display.flip() horloge.tick(60) fenetre.fill(BLANC) fin = grande_police.render("Partie terminée", True, ENCRE) total = police.render("Score final : " + str(score), True, BLEU) fenetre.blit(fin, (200, 240)) fenetre.blit(total, (280, 330)) pygame.display.flip() pygame.time.wait(4000) pygame.quit()
Où se cachent les onze chapitres
| Chapitre | Notion | Ligne du programme 14 qui l’emploie |
|---|---|---|
| 1 | la bibliothèque | import pygame |
| 2 | la fenêtre et son affichage | set_mode, flip |
| 3 | le repère de l’écran | goutte.y = goutte.y + vitesse_goutte |
| 4 | les couleurs et les formes | BLANC, draw.rect, draw.ellipse |
| 5 | la boucle de jeu | while en_marche |
| 6 | les événements et le clavier | event.get, key.get_pressed |
| 7 | le mouvement | l’addition sur goutte.y |
| 8 | l’horloge | horloge.tick(60) |
| 9 | les bords | panier.left et panier.right |
| 10 | les collisions | panier.colliderect(goutte) |
| 11 | le texte et le score | police.render, fenetre.blit |
🔬 À l’essai
L’accélération, en détail. La ligne if score % 5 == 0 mérite un mot. L’opérateur % donne le reste d’une division entière.
Ainsi 12 divisé par 5 donne 2 et il reste 2, donc 12 % 5 vaut 2. Mais 15 % 5 vaut 0, car 15 est un multiple de 5. Tester un reste nul revient donc à demander « ce nombre est-il un multiple de cinq ? ».
Mesure effectuée. Une partie simulée de 1400 tours a produit un score de 12, avec une vitesse de chute passée de 4 à 6. Deux accélérations, déclenchées aux scores 5 et 10 : le compte est juste.
📜 Histoire vraie
En 2006, un petit studio finlandais nommé Unreal Voodoo présente à l’Assembly, une manifestation annuelle de programmeurs, un jeu nommé Frets on Fire. Le joueur y tient son clavier comme une guitare et suit des notes qui défilent.
Le jeu remporte le concours de développement de l’édition 2006. Il est écrit en Python et bâti sur Pygame, la bibliothèque même que tu viens d’apprendre. Sami Kyöstilä en signe la conception et le programme.
Le succès dépasse largement le cadre du concours : entre 2007 et mai 2017, le jeu a été téléchargé plus de quatorze millions de fois depuis la seule plateforme SourceForge.
La leçon n’est pas mince. Les outils qui t’ont servi à écrire L’Attrapeur de gouttes sont ceux qui ont servi à écrire un jeu téléchargé quatorze millions de fois. La différence tient au travail, pas à l’outillage.
💡 Le savais-tu ?
Le programme 14 tient en une soixantaine de lignes utiles. À titre de comparaison, Space Invaders occupait environ huit kilo-octets de mémoire morte, et l’ensemble du jeu Pong ne comportait pas une seule ligne de programme : il était entièrement câblé en circuits électroniques.
Tu disposes, sur ton bureau, d’un outillage que les créateurs de ces jeux auraient jugé somptueux.
🧵 Fil rouge
Le cours s’achève ici, mais le jeu, lui, commence. Les pages suivantes rassemblent de quoi te dépanner, réviser et t’exercer.
🚀 À toi de jouer
Défi 12.1. Affiche à l’écran de fin le nombre de gouttes attrapées et le nombre de gouttes perdues.
Défi 12.2. Ajoute une seconde goutte, de couleur différente, qui tombe indépendamment de la première. Tu auras besoin d’un second Rect et d’une seconde vitesse.
Défi 12.3. Ajoute une goutte empoisonnée, rouge, qui retire deux points au lieu d’en ajouter un.
Défi 12.4. Fais varier la largeur du panier : plus large au début, plus étroit à mesure que le score monte. Veille à ce qu’il garde une largeur jouable.
Défi 12.5. Remplace l’arrêt sec de la partie par un écran proposant de rejouer : la touche R relance, la touche Échap quitte. Cette dernière demande une boucle autour de la boucle.
📋 Récapitulatif
- Un jeu complet se construit en assemblant des notions simples, sans notion nouvelle.
- Les constantes en majuscules, placées en tête, rassemblent les réglages.
- La structure reste celle du chapitre 5 : écouter, calculer, dessiner.
%donne le reste d’une division et sert à repérer les multiples.- L’écran de fin se dessine après la boucle, avant
pygame.quit().
🩺 Quand ça ne marche pas
Quinze pannes courantes, leur cause et leur remède. Les messages d’erreur cités ont été relevés sur Pygame 2.6.1.
Un programme qui refuse de fonctionner n’est pas un échec : c’est la situation ordinaire du programmeur. Ce qui distingue le débutant du confirmé n’est pas le nombre d’erreurs commises, mais la vitesse à les identifier. Ce tableau est là pour l’accélérer.
Le programme s’arrête net
| Message affiché | Cause | Remède |
|---|---|---|
ModuleNotFoundError: No module named 'pygame' | La bibliothèque n’est pas installée, ou Python ne la trouve pas. | Reprendre pip install pygame (chapitre 1). |
pygame.error: font not initialized | pygame.init() manque avant l’emploi du texte. | Placer pygame.init() au début du programme. |
pygame.error: Display mode not set | flip appelé alors que la fenêtre n’a pas été créée. | Ajouter set_mode avant toute chose. |
pygame.error: display Surface quit | Un dessin a lieu après pygame.quit(). | Déplacer pygame.quit() à la toute fin. |
TypeError: can only concatenate str (not "int") to str | Un nombre est ajouté à du texte sans conversion. | Écrire "Score : " + str(score) (chapitre 11). |
TypeError: size must be two numbers | Les dimensions ont été données sans les parenthèses intérieures. | Écrire set_mode((800, 600)), avec les deux paires. |
TypeError: rect argument is invalid | Un rectangle a reçu trois nombres au lieu de quatre. | Fournir (x, y, largeur, hauteur). |
ValueError: invalid color argument | Une composante de couleur dépasse 255 ou descend sous 0. | Ramener les trois nombres entre 0 et 255. |
IndentationError | Les décalages en début de ligne sont irréguliers. | Employer partout quatre espaces, et proscrire le mélange avec des tabulations. |
Le programme tourne, mais se comporte mal
| Symptôme | Cause la plus probable | Remède |
|---|---|---|
| La fenêtre s’ouvre et se referme aussitôt. | Le programme arrive à sa fin sans attendre. | Ajouter la boucle de jeu du chapitre 5. |
La fenêtre reste grise malgré fill. | pygame.display.flip() manque. | Ajouter flip après les dessins. |
| L’objet laisse une traînée derrière lui. | fill se trouve hors de la boucle. | Repeindre le fond à chaque tour, avant les dessins. |
| La fenêtre refuse de se fermer. | Le test sur pygame.QUIT manque. | Rétablir le test et le passage de en_marche à False. |
| Le jeu file trop vite, ou rame. | L’horloge manque. | Créer pygame.time.Clock() et appeler tick(60). |
| Le personnage avance d’un pas puis se fige. | Le déplacement passe par KEYDOWN. | Employer pygame.key.get_pressed() (chapitre 6). |
| Le tir part en rafale continue. | Le tir passe par get_pressed. | Employer KEYDOWN dans la file d’événements. |
| Un objet à vitesse décimale ne bouge pas. | Un Rect arrondit à l’entier le plus proche. | Garder une vitesse entière, ou tenir la position dans une variable séparée (chapitre 10). |
| Deux objets se touchent sans rien déclencher. | colliderect exige un chevauchement réel. | Vérifier les positions : bord contre bord renvoie False. |
| L’objet tremble contre un mur au lieu de rebondir. | La vitesse s’inverse à chaque tour. | Replacer l’objet sur la limite avant d’inverser la vitesse (chapitre 9). |
| La moitié du cercle dépasse de la fenêtre. | Le rayon n’entre pas dans le calcul de la limite. | Borner le centre entre le rayon et la dimension moins le rayon. |
💡 Le savais-tu ?
Deux cas où Pygame reste silencieux là où l’on attendrait une protestation, tous deux vérifiés pour ce cours.
Un cercle de rayon négatif ne provoque pas d’erreur : rien ne s’affiche, tout simplement. Et une couleur donnée avec des nombres décimaux, comme (255.5, 0, 0), passe sans protestation.
Retiens le principe : l’absence de message ne prouve pas que ton programme fait ce que tu crois. Seule l’observation du résultat le prouve.
🔬 À l’essai
La méthode de dépannage qui marche. Quand un programme se comporte mal sans message d’erreur, ajoute des print.
print("tour", compteur, "x vaut", x)
Place cette ligne aux endroits stratégiques et lis le terminal. Neuf fois sur dix, la valeur affichée démasque le coupable : une variable qui ne change pas, une autre qui grandit sans fin, une condition qui ne se déclenche pas.
Cette méthode a un nom respectable : le débogage par traces. Elle est employée par les professionnels tous les jours.
🗃️ Fiche mémo
Tout Pygame utile à ce cours, sur deux pages.
Mise en route et fermeture
| Instruction | Rôle |
|---|---|
import pygame | charge la bibliothèque |
pygame.init() | réveille les modules |
fenetre = pygame.display.set_mode((800, 600)) | crée la fenêtre, renvoie sa surface |
pygame.display.set_caption("Titre") | écrit le titre de la fenêtre |
pygame.quit() | range tout à la fin |
Dessiner
| Instruction | Rôle |
|---|---|
fenetre.fill((255, 255, 255)) | repeint toute la surface |
pygame.draw.rect(fenetre, couleur, (x, y, l, h)) | rectangle plein, repéré par son coin |
pygame.draw.rect(fenetre, couleur, (x, y, l, h), 3) | le même, en contour de 3 pixels |
pygame.draw.circle(fenetre, couleur, (x, y), r) | disque, repéré par son centre |
pygame.draw.ellipse(fenetre, couleur, (x, y, l, h)) | ellipse inscrite dans le rectangle |
pygame.draw.line(fenetre, couleur, depart, arrivee, ep) | segment |
pygame.display.flip() | montre le dessin au joueur |
La boucle de jeu
en_marche = True while en_marche: # 1. ECOUTER for evenement in pygame.event.get(): if evenement.type == pygame.QUIT: en_marche = False # 2. CALCULER ... # 3. DESSINER fenetre.fill(BLANC) ... pygame.display.flip() horloge.tick(60)
Le squelette à recopier au début de chaque nouveau jeu.
Le clavier
| Instruction | Rôle |
|---|---|
touches = pygame.key.get_pressed() | état de toutes les touches à cet instant |
if touches[pygame.K_LEFT]: | vrai tant que la flèche gauche est enfoncée |
if evenement.type == pygame.KEYDOWN: | l’instant précis d’un appui |
if evenement.key == pygame.K_SPACE: | quelle touche vient d’être pressée |
| Constantes usuelles | K_LEFT, K_RIGHT, K_UP, K_DOWN, K_SPACE, K_ESCAPE, K_a à K_z |
Les rectangles et les chocs
| Instruction | Rôle |
|---|---|
r = pygame.Rect(x, y, largeur, hauteur) | crée un rectangle |
r.x, r.y | coin supérieur gauche |
r.left, r.right | bords gauche et droit |
r.top, r.bottom | bords haut et bas |
r.center | centre, sous forme de couple |
a.colliderect(b) | vrai si a et b se chevauchent |
r.collidepoint(x, y) | vrai si le point est à l’intérieur |
Le temps et le texte
| Instruction | Rôle |
|---|---|
horloge = pygame.time.Clock() | crée l’horloge, avant la boucle |
horloge.tick(60) | plafonne à 60 tours par seconde |
horloge.get_fps() | fréquence réellement obtenue |
pygame.time.wait(3000) | met en pause trois secondes |
police = pygame.font.SysFont(None, 40) | choisit une police, avant la boucle |
texte = police.render("Score : " + str(s), True, couleur) | fabrique l’image du texte |
fenetre.blit(texte, (20, 20)) | pose cette image |
Couleurs courantes
| Nom | Triplet | Nom | Triplet |
|---|---|---|---|
| blanc | (255, 255, 255) | rouge | (200, 40, 40) |
| noir | (0, 0, 0) | vert | (30, 140, 90) |
| gris | (150, 150, 150) | bleu | (40, 90, 200) |
| sable | (235, 215, 170) | violet | (120, 60, 160) |
| jaune | (240, 200, 40) | brun | (150, 96, 42) |
🧠 Quiz final
Dix questions pour vérifier ce qui est acquis. Les réponses figurent à la fin du fascicule.
Réponds sans revenir en arrière, puis compare. Une question manquée désigne un chapitre à relire, et c’est précisément à cela qu’elle sert.
Question 1. Dans le repère de l’écran, où se trouve le point de coordonnées (0 ; 0) ?
- Au centre de la fenêtre.
- En bas à gauche, comme en mathématiques.
- En haut à gauche.
- En haut à droite.
Question 2. Que se passe-t-il si tu oublies pygame.display.flip() ?
- Le programme s’arrête avec une erreur.
- Les dessins ont lieu mais restent invisibles.
- La fenêtre se ferme aussitôt.
- Les couleurs sont inversées.
Question 3. Combien de couleurs différentes le codage RVB permet-il de décrire ?
- 255
- 65 536
- 16 777 216
- un nombre illimité
Question 4. Quels sont les trois temps de la boucle de jeu, dans l’ordre ?
- Dessiner, écouter, calculer.
- Écouter, calculer, dessiner.
- Calculer, dessiner, écouter.
- Écouter, dessiner, calculer.
Question 5. Ton personnage doit avancer tant que la flèche droite est maintenue. Quelle méthode emploies-tu ?
KEYDOWNdans la file d’événements.pygame.key.get_pressed().pygame.event.wait().pygame.QUIT.
Question 6. À quoi sert horloge.tick(60) ?
- À compter les points du joueur.
- À garantir précisément soixante images par seconde.
- À empêcher la boucle de dépasser environ soixante tours par seconde.
- À mettre le jeu en pause pendant soixante secondes.
Question 7. Un cercle de rayon 40 se déplace dans une fenêtre de 800 pixels de large. Quelle est la plus grande abscisse que son centre peut prendre sans que le disque dépasse ?
- 800
- 780
- 760
- 840
Question 8. Deux rectangles occupent l’un les abscisses 0 à 10, l’autre 10 à 20. Que renvoie colliderect ?
True, puisqu’ils se touchent.False, car le chevauchement est nul.- Une erreur.
- Cela dépend de leur hauteur.
Question 9. Pourquoi faut-il écrire "Score : " + str(score) plutôt que "Score : " + score ?
- Pour que le texte soit plus joli.
- Parce que Python refuse d’ajouter un nombre à du texte.
- Parce que
renderexige un nombre. - Pour accélérer le programme.
Question 10. Dans le mini-jeu, que teste if score % 5 == 0 ?
- Si le score vaut 5.
- Si le score est inférieur à 5.
- Si le score est un multiple de 5.
- Si le score est un nombre pair.
✍️ Exercices de réflexion
Six questions sans ordinateur. Papier, crayon, et du temps.
Ces exercices ne demandent pas de taper du code. Ils demandent de raisonner, ce qui est le travail le plus utile et le plus négligé d’un programmeur.
Exercice 1. Le cercle fantôme
Un cercle part de l’abscisse 0 et avance de 4 pixels à chaque tour, dans une fenêtre de 800 pixels de large, à 60 tours par seconde. Calcule le temps qu’il met pour sortir entièrement de la fenêtre, sachant que son rayon vaut 40. Explique ensuite pourquoi le programme continue de calculer sa position alors qu’il n’est plus visible.
Exercice 2. Le piège de la vitesse décimale
Un camarade veut faire descendre une goutte très lentement. Il écrit goutte.y = goutte.y + 0.3. Rien ne bouge. Explique-lui pourquoi, puis propose deux solutions différentes.
Exercice 3. L’ordre des trois temps
Que se passerait-il si un programmeur dessinait avant de calculer, c’est-à-dire s’il inversait les temps 2 et 3 de la boucle ? Le jeu serait-il faux, ou seulement décalé ? Justifie.
Exercice 4. Le score qui déborde
Le compteur de niveau de Pac-Man tenait dans un octet, soit 256 valeurs. Si un jeu rangeait son score dans deux octets, quelle serait la plus grande valeur affichable ? Et avec quatre octets ? Que conclus-tu sur le choix de la taille d’un compteur ?
Exercice 5. Deux méthodes pour une touche
Conçois les règles d’un jeu de plateforme simple. Pour chacune des cinq actions suivantes, indique si tu emploierais la file d’événements ou l’état des touches, et justifie en une phrase : courir, sauter, s’accroupir, ouvrir l’inventaire, tirer.
Exercice 6. La hitbox honnête
Un jeu affiche un vaisseau de 60 pixels de large mais lui donne une boîte de collision de 40 pixels. Le joueur trouve le jeu agréable sans savoir pourquoi. Explique le mécanisme, puis dis dans quel cas ce réglage deviendrait malhonnête envers le joueur.
📖 Lexique
Vingt-trois termes, dans l’ordre alphabétique.
- Affectation
- Rangement d’une valeur dans une variable. Dans
x = x + 4, la droite est calculée puis rangée à gauche. - Anticrénelage
- Adoucissement des bords d’un dessin par des pixels intermédiaires. Activé par
Truedansrender. - Bibliothèque
- Ensemble de programmes déjà écrits qu’un autre programme peut appeler.
- Blit
- Copie d’une surface sur une autre. De l’anglais block transfer.
- Boucle de jeu
- Boucle qui répète sans interruption trois gestes : écouter, calculer, dessiner.
- Collision
- Chevauchement de deux objets. Testée par
colliderect. - Constante
- Valeur qu’un programme ne modifie pas. Écrite en majuscules par convention.
- Double tampon
- Procédé consistant à préparer l’image sur une surface cachée, puis à la montrer d’un coup.
- Découpage
- Suppression silencieuse de la partie d’un dessin qui sort de la surface. En anglais, clipping.
- File d’attente
- Suite de messages traités dans leur ordre d’arrivée.
- Fréquence d’images
- Nombre d’images affichées en une seconde. Notée FPS en anglais.
- Hitbox
- Rectangle de collision d’un objet, souvent différent de son dessin visible.
- Image
- Résultat d’un tour complet de boucle de jeu. En anglais, frame.
- Pixel
- Plus petit point d’un écran. De l’anglais picture element.
- Pygame
- Bibliothèque Python de création de jeux, publiée en octobre 2000 par Pete Shinners.
- RVB
- Codage d’une couleur par trois entiers de 0 à 255 : rouge, vert, bleu.
- Rect
- Objet Pygame regroupant la position et les dimensions d’un rectangle.
- Repère de l’écran
- Système de coordonnées dont l’origine est en haut à gauche et dont l’axe vertical descend.
- SDL
- Bibliothèque écrite en C sur laquelle Pygame s’appuie. Simple DirectMedia Layer.
- Surface
- Zone de mémoire contenant une image. La fenêtre en est une.
- Synthèse additive
- Mélange de lumières colorées. Les trois primaires au maximum donnent du blanc.
- Variable
- Nom associé à une valeur que le programme peut modifier.
- Événement
- Message décrivant une action du joueur, rangé dans une file d’attente.
✅ Corrigés
Les réponses du quiz, puis celles des défis, chapitre par chapitre.
Quiz final
| N° | Réponse | Énoncé de la bonne réponse | Pourquoi |
|---|---|---|---|
| 1 | c | En haut à gauche. | L’origine de l’écran est le coin supérieur gauche, et l’axe des ordonnées descend. |
| 2 | b | Les dessins ont lieu mais restent invisibles. | Les dessins se font sur une surface cachée. flip est le geste qui la montre au joueur. |
| 3 | c | 16 777 216 | Chaque composante admet 256 valeurs, donc 256 × 256 × 256 = 16 777 216. |
| 4 | b | Écouter, calculer, dessiner. | L’ordre compte : il faut connaître les actions du joueur avant d’en calculer les conséquences, et achever ce calcul avant de dessiner. |
| 5 | b | pygame.key.get_pressed(). | Un mouvement continu réclame l’état de la touche, relu à chaque tour. KEYDOWN ne se déclenche qu’une fois par appui. |
| 6 | c | À empêcher la boucle de dépasser environ soixante tours par seconde. | tick impose un plafond. La mesure effectuée pour ce cours donne 61,7 images par seconde, et non 60 précisément. |
| 7 | c | 760 | Le centre doit rester à au moins un rayon du bord : 800 - 40 = 760. |
| 8 | b | False, car le chevauchement est nul. | Le contact bord contre bord ne suffit pas. Il faut au moins un pixel commun, comme le montre la figure 6. |
| 9 | b | Parce que Python refuse d’ajouter un nombre à du texte. | Sans conversion, Python signale TypeError: can only concatenate str (not "int") to str. |
| 10 | c | Si le score est un multiple de 5. | L’opérateur % donne le reste d’une division. Un reste nul par 5 signale un multiple de 5. |
Défis, chapitre par chapitre
| Défi | Réponse |
|---|---|
| 1.1 | Le programme tient en deux lignes : import pygame puis print(pygame.version.ver). |
| 1.2 | Ajouter print(pygame.version.SDL). Deux print produisent deux lignes. |
| 1.3 | pygame.display crée la fenêtre, pygame.draw trace le cercle. |
| 2.1 | set_mode((500, 500)), set_caption("Mon premier carré"), et un remplissage avec un bleu clair, par exemple (170, 210, 240). |
| 2.2 | pygame.time.wait(10000), puisque la valeur s’exprime en millisecondes. |
| 2.3 | Le remplissage a lieu après l’affichage : il peint la surface cachée, que personne ne montre ensuite. Le joueur voit donc l’état précédent, c’est-à-dire le gris d’origine. |
| 3.1 | Le point (100 ; 50) est le plus haut. Une ordonnée plus petite signifie une position plus haute, puisque l’axe descend. |
| 3.2 | (320, 240), soit la moitié de 640 et la moitié de 480. |
| 3.3 | Une boucle for i in range(5) avec x = i * 200 et y = i * 150 répartit cinq cercles sur la diagonale. |
| 4.1 | Du jaune. Le rouge et le vert au maximum, sans bleu, donnent du jaune, comme le montre la zone d’intersection de la figure 3. |
| 4.2 | 600 divisé par 3 donne 200 pixels par bande. Les trois rectangles partent des abscisses 0, 200 et 400, chacun de 200 pixels de large et de 400 de haut. |
| 4.3 | Trois draw.circle de même abscisse, d’ordonnées croissantes et de rayons décroissants, puis deux petits disques noirs posés sur le disque du haut. |
| 5.1 | Remplacer le triplet du fill par un jaune pâle, par exemple (250, 240, 190), et draw.circle par draw.rect. La boucle reste identique. |
| 5.2 | La fenêtre refuse de se fermer : l’événement arrive bien, mais rien ne fait passer en_marche à False, donc la boucle continue. |
| 5.3 | Sans horloge, le nombre de tours dépend de la machine. Plusieurs milliers par seconde sur un ordinateur récent : c’est précisément le problème que le chapitre 8 résout. |
| 6.1 | Ajouter if touches[pygame.K_ESCAPE]: suivi de en_marche = False. |
| 6.2 | Tester touches[pygame.K_SPACE] et ranger la couleur dans une variable avant le dessin. |
| 6.3 | En tapotant, un message par appui. En maintenant, un seul message malgré la durée. Conclusion : KEYDOWN signale l’instant de l’appui, non sa durée. |
| 7.1 | Modifier y au lieu de x, et l’augmenter, puisque l’axe vertical descend. |
| 7.2 | Deux lignes : x = x + 4 et y = y + 4 dans le même tour. |
| 7.3 | Ce nombre est la vitesse, exprimée en pixels par tour. Augmenté, l’objet va plus vite ; diminué, il ralentit. |
| 8.1 | Le mouvement devient très saccadé : l’objet fait des bonds de 4 pixels visibles à l’œil, parce que les images sont trop espacées dans le temps. |
| 8.2 | À 4 pixels par tour et 60 tours par seconde, l’objet parcourt 240 pixels par seconde. Pour 800 pixels : 800 divisé par 240, soit environ 3,3 secondes. |
| 8.3 | À 2 pixels par tour, la vitesse est de 120 pixels par seconde, et la traversée dure environ 6,7 secondes, soit le double. |
| 9.1 | Ajouter une variable vitesse_y, puis l’additionner à y à chaque tour, et écrire les deux tests de bord vertical sur le modèle des tests horizontaux. |
| 9.2 | Le cercle finit par devenir plus large que la fenêtre. Les deux tests de bord se déclenchent alors au même tour, la vitesse s’inverse deux fois, et l’objet se bloque. |
| 9.3 | if x > 800: x = 0. L’objet réapparaît par la gauche sans changer de sens. |
| 10.1 | Créer un second Rect, et écrire un second if joueur.colliderect(...) avec sa propre couleur. |
| 10.2 | Quatre tests : if joueur.left < 0: joueur.left = 0, et de même pour right avec 800, top avec 0, bottom avec 600. |
| 10.3 | Dans le test de collision, écrire tresor.x = random.randint(0, 800 - tresor.width) et de même pour l’ordonnée avec la hauteur. |
| 11.1 | Une position telle que (600, 20). Le texte risque de dépasser parce que sa largeur grandit avec le nombre de chiffres du score ; il faudrait la mesurer avec texte.get_width() et soustraire. |
| 11.2 | Un compteur augmenté de 1 à chaque tour, converti par str, puis un second render et un second blit à une autre position. |
| 11.3 | Ranger la couleur dans une variable avant le render, décidée par une suite de if et elif portant sur le score. |
| 12.1 | Deux render supplémentaires après la boucle, posés à deux ordonnées différentes sur l’écran de fin. |
| 12.2 | Un second Rect nommé par exemple goutte2, sa propre vitesse, son propre test de collision et son propre test de sortie par le bas. |
| 12.3 | Un troisième Rect rouge dont le test de collision écrit score = score - 2. Prévoir le cas d’un score qui deviendrait négatif. |
| 12.4 | Modifier panier.width en fonction du score, en imposant une largeur minimale, par exemple 40, pour que la partie reste jouable. |
| 12.5 | Placer la boucle de jeu à l’intérieur d’une seconde boucle. L’écran de fin attend alors un événement KEYDOWN : la touche R réinitialise le score, les gouttes perdues et les positions ; la touche Échap sort des deux boucles. |
📋 Récapitulatif
Un corrigé consulté avant d’avoir cherché n’apprend presque rien. Un corrigé consulté après vingt minutes de recherche apprend beaucoup, même quand la réponse trouvée était fausse.
C’est une règle générale de l’apprentissage, et elle vaut bien au-delà de la programmation.