Aller au contenu

TP 3 — Animer et rendre un programme interactif⚓︎

Objectifs

À la fin de ce TP, vous saurez :

  • distinguer un affichage statique d'un affichage animé ;
  • expliquer les rôles des fonctions update et draw ;
  • lancer la boucle principale d'un programme avec pyxel.run ;
  • modifier la position d'un élément au cours du temps ;
  • utiliser pyxel.btn et pyxel.btnp pour lire le clavier ;
  • limiter le déplacement d'un personnage à la fenêtre ;
  • afficher et orienter un sprite mobile ;
  • utiliser pyxel.frame_count pour effectuer une action périodique.

Prérequis

Fonctions, variables globales, conditions, boucles, listes et dictionnaires, ainsi que les notions des TP 1 et 2.

👏 Crédits

Ce TP est adapté d'une ressource conçue et partagée par Florian Picard. Le fichier 4.pyxres utilisé pour le personnage provient des univers Python 2024 de la Nuit du Code. Le TP s'appuie également sur le tutoriel de la Nuit du Code et sur la documentation officielle de Pyxel.

1. La boucle principale d'un programme Pyxel⚓︎

Affichage dynamique

Dans les TP précédents, l'ensemble de l'image était dessiné une seule fois, puis affiché avec pyxel.show() : l'affichage était statique.

Pour un affichage dynamique (programme animé ou interactif), Pyxel affiche une succession d'images, appelées aussi frames. Par défaut, il produit 30 images par seconde. Avant chaque nouvel affichage, Pyxel appelle successivement deux fonctions :

  • update met à jour l'état du programme : positions, clavier, score, collisions, etc. ;
  • draw dessine l'image correspondant à cet état.

La fonction pyxel.run(update, draw) lance cette boucle principale. Elle remplace pyxel.show().

flowchart LR
    A["update : mettre à jour"] --> B["draw : dessiner"]
    B --> C["Afficher l'image"]
    C --> A

Exercice 1 — Une première animation

Recopier puis exécuter le programme suivant. On pourra le nommer tp3.py et on le modifiera au fil du TP.

🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pyxel

LARGEUR = 128
HAUTEUR = 128
TAILLE = 8

CARRE = {"x": 0, "y": 60, "vitesse": 1}

pyxel.init(LARGEUR, HAUTEUR, title="Première animation")


def update():
    """Met à jour les variables du programme."""
    CARRE["x"] = CARRE["x"] + CARRE["vitesse"]


def draw():
    """Dessine une image du programme."""
    pyxel.cls(0)
    pyxel.rect(CARRE["x"], CARRE["y"], TAILLE, TAILLE, 3)


pyxel.run(update, draw)

Question 1

Quel est le rôle de chacune des clefs "x", "y" et "vitesse" du dictionnaire CARRE ?

Question 2

Quelle instruction provoque le déplacement du carré ? Dans quelle fonction se trouve-t-elle ?

Question 3

Supprimer temporairement l'instruction pyxel.cls(0), puis relancer le programme. Expliquer les traces observées.

Question 4

Compléter la fonction update pour que le carré réapparaisse à gauche lorsqu'il est entièrement sorti de la fenêtre.

Aide

Le carré est entièrement sorti lorsque son abscisse est supérieure ou égale à LARGEUR.

Résultat attendu

alt

2. Commander un déplacement avec le clavier⚓︎

Gestion du clavier

Pyxel associe un nom à chaque touche du clavier. Par exemple, les quatre touches fléchées sont représentées par :

🐍 Script Python
pyxel.KEY_LEFT
pyxel.KEY_RIGHT
pyxel.KEY_UP
pyxel.KEY_DOWN

Les fonctions suivantes permettent de connaître l'état d'une touche.

Syntaxe Valeur renvoyée
pyxel.btn(touche) True pendant toute la durée où la touche est maintenue enfoncée.
pyxel.btnp(touche) True au moment où la touche vient d'être pressée.
pyxel.btnr(touche) True au moment où la touche vient d'être relâchée.

Pour obtenir un déplacement continu, on utilise donc généralement pyxel.btn.

Exercice 2 — Déplacer le carré

Remplacer la fonction update de l'exercice précédent par le squelette suivant.

🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def update():
    if pyxel.btn(pyxel.KEY_LEFT):
        CARRE["x"] = ...
    if pyxel.btn(pyxel.KEY_RIGHT):
        CARRE["x"] = ...
    if pyxel.btn(pyxel.KEY_UP):
        CARRE["y"] = ...
    if pyxel.btn(pyxel.KEY_DOWN):
        CARRE["y"] = ...
    

Question 1

Compléter la fonction update pour déplacer le carré de deux pixels dans chacune des quatre directions.

Question 2

Maintenir deux touches fléchées enfoncées simultanément. Pourquoi le carré peut-il se déplacer en diagonale ?

Résultat attendu

alt

Question 3

Remplacer temporairement les quatre if par une structure ifelifelifelif. Quel changement observez-vous ?

Question 4

Remplacer temporairement pyxel.btn par pyxel.btnp. Quelle différence observez-vous lorsqu'une touche est maintenue enfoncée ?

Question 5

Compléter la fonction update pour que le carré réapparaisse par le bord opposé s'il sort par un bord.

Question 6

Modifier le programme pour commander le carré avec les touches Z, Q, S et D. Par exemple, la touche Z est désignée par pyxel.KEY_Z.

3. Utiliser un dictionnaire global⚓︎

Variable globale et dictionnaire

Les fonctions imposées à pyxel.run ne reçoivent aucun argument. Les variables qui décrivent l'état du programme sont donc souvent définies en dehors de ces fonctions.

La fonction update peut lire le dictionnaire global CARRE et modifier son contenu, par exemple avec CARRE["x"] = 10. La variable CARRE continue alors de désigner le même dictionnaire : le mot-clé global n'est pas nécessaire.

En revanche, une instruction comme CARRE = {"x": 10, "y": 20} affecterait un nouveau dictionnaire à la variable. Pour effectuer cette affectation dans une fonction, il faudrait déclarer global CARRE.

Dans la suite, l'état du personnage sera regroupé dans un dictionnaire :

🐍 Script Python
PERSONNAGE = {"x": 60, "y": 60, "sens": 1}

La clef "sens" vaudra 1 lorsque le personnage regarde vers la droite et -1 lorsqu'il regarde vers la gauche.

4. Maintenir le personnage dans la fenêtre⚓︎

Vérification des coordonnées

Un sprite carré de côté TAILLE est entièrement dans une fenêtre de dimensions LARGEUR × HAUTEUR si :

\[0 \leqslant x \leqslant \text{LARGEUR}-\text{TAILLE}\]

et

\[0 \leqslant y \leqslant \text{HAUTEUR}-\text{TAILLE}.\]

Exercice 3 — Traiter les bords

On remplace le dictionnaire CARRE par :

🐍 Script Python
PERSONNAGE = {"x": 60, "y": 60, "sens": 1}

Remplacer la fonction update par les deux fonctions suivantes.

Il faut compléter la fonction update_personnage afin que le personnage ne puisse pas sortir de la fenêtre.

Il faut aussi remplacer CARRE par PERSONNAGE dans la fonction draw.

🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def update_personnage():
    if pyxel.btn(pyxel.KEY_LEFT) and ...:
        PERSONNAGE["x"] = PERSONNAGE["x"] - 2
        PERSONNAGE["sens"] = -1

    if pyxel.btn(pyxel.KEY_RIGHT) and ...:
        PERSONNAGE["x"] = PERSONNAGE["x"] + 2
        PERSONNAGE["sens"] = 1

    if pyxel.btn(pyxel.KEY_UP) and ...:
        PERSONNAGE["y"] = PERSONNAGE["y"] - 2

    if pyxel.btn(pyxel.KEY_DOWN) and ...:
        PERSONNAGE["y"] = PERSONNAGE["y"] + 2


def update():
    update_personnage()

Question 1

Compléter les quatre conditions.

Question 2

Pourquoi la condition concernant le bord droit fait-elle intervenir LARGEUR - TAILLE et non LARGEUR ?

Question 3

Tester le programme dans les quatre coins de la fenêtre.

Résultat attendu

alt

5. Afficher et orienter un sprite mobile⚓︎

On utilise maintenant le fichier 4.pyxres de la Nuit du Code 2024, déjà employé dans le TP 2. On va utiliser dans ce TP le sprite qui commence au point (0, 16) de la banque d'images 0 et mesure 8 × 8 pixels. La couleur 5 constitue son fond transparent.

La largeur fournie à pyxel.blt peut être positive ou négative :

🐍 Script Python
largeur_affichee = TAILLE * PERSONNAGE["sens"]

Elle vaut 8 lorsque le personnage regarde vers la droite et -8 lorsqu'il regarde vers la gauche.

Exercice 4 — Déplacer un sprite

Question 1

Télécharger le fichier 4.pyxres, le placer dans le dossier du programme et ajouter l'instruction suivante après pyxel.init :

🐍 Script Python
pyxel.load("4.pyxres")

Question 2

Compléter la fonction draw pour effacer l'écran, puis afficher le sprite à la position du personnage. Sa largeur devra dépendre de PERSONNAGE["sens"].

🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def draw():
    pyxel.cls(0)
    pyxel.blt(
        PERSONNAGE["x"],
        PERSONNAGE["y"],
        0, # banque d'image
        0, # abscisse du sprite dans la banque
        16, # ordonnée du sprite dans la banque
        ..., # largeur du sprite
        TAILLE, # hauteur du sprite
        5
    )

Question 3

Vérifier que le sprite change d'orientation lorsque le personnage se déplace vers la gauche ou vers la droite.

Résultat attendu

alt

6. Déclencher une action périodique⚓︎

Gestion du temps

La variable pyxel.frame_count contient le nombre d'images affichées depuis le lancement du programme. Avec la fréquence par défaut de 30 images par seconde :

  • pyxel.frame_count % 30 == 0 est vraie une fois par seconde ;
  • pyxel.frame_count % 15 == 0 est vraie deux fois par seconde.

Exercice 5 — Faire clignoter un objet

Question 1

Combien d'images correspondent à une demi-seconde lorsque le programme fonctionne à 30 images par seconde ?

Question 2

On veut afficher un disque pendant une demi-seconde, puis le masquer pendant une demi-seconde.

Compléter la condition suivante, puis ajouter ces lignes dans la fonction draw :

🐍 Script Python
periode = pyxel.frame_count // 15
if ... : # si periode est pair on affiche le disque
    pyxel.circ(10, 10, 3, 10)
Résultat attendu

alt

Question 3

Modifier le programme pour que le disque change d'état une fois par seconde.

7. Production finale — Un personnage sous l'averse⚓︎

Exercice 6 : à vous de jouer

Programmer un personnage qui se déplace horizontalement sous une averse avec un éclair qui change en jaune la couleur du ciel toutes les secondes pendant deux dixièmes de seconde. Le ciel est noir par défaut.

Le programme devra respecter le cahier des charges suivant :

  • le personnage se déplace vers la gauche ou vers la droite avec les touches fléchées ;
  • il ne peut pas sortir de la fenêtre ;
  • son sprite s'oriente dans le sens du déplacement ;
  • les gouttes descendent automatiquement ;
  • lorsqu'une goutte atteint le sol, elle réapparaît en haut de la fenêtre ;
  • l'éclair change la couleur du ciel en jaune pendant deux dixièmes de seconde toutes les secondes ;
  • les mises à jour sont effectuées dans update et l'affichage dans draw.

Compléter le squelette suivant. La liste des gouttes est fournie.

🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import random
import pyxel

LARGEUR = 128
HAUTEUR = 128
TAILLE = 8
SOL = 104
NB_GOUTTES = 30

PERSONNAGE = {
    "x": 60,
    "y": SOL - TAILLE,
    "sens": 1
}

GOUTTES = [
{
    "x": random.randint(1, LARGEUR - 1),
    "y": 0,
    "vitesse": random.randint(1, 4),
}
for _ in range(NB_GOUTTES)
]

CIEL = {"couleur": 0}  # couleur du ciel noir (0) ou jaune (10) si eclair

pyxel.init(LARGEUR, HAUTEUR, title="Sous l'averse")
pyxel.load("4.pyxres")


def update_personnage():
    """Déplace le personnage et met à jour son orientation."""
    # À compléter
    pass


def update_gouttes():
    """Fait descendre les gouttes et les replace en haut."""
    for goutte in GOUTTES:
        # Faire descendre la goutte selon sa vitesse
        goutte["y"] = ...

        if goutte["y"] >= SOL:
            # Choisir une nouvelle abscisse
            goutte["x"] = ...
            # Replacer la goutte en haut de la fenêtre
            goutte["y"] = ...
            # changer la vitesse
            goutte["vitesse"] = ...

def update_eclair():
    # vrai pendandt 2 dixièmes de seconde toutes les secondes
    if ... :  
        CIEL["couleur"] = 10  # jaune
    else:
        CIEL["couleur"] = 0  # noir


def update():
    update_eclair()
    update_personnage()
    update_gouttes()


def draw():
    # Changer la couleur du ciel jaune pour éclaire et noir sinon
    pyxel.cls( ... )

    # Dessiner le sol
    pyxel.rect(0, SOL, LARGEUR, HAUTEUR - SOL, 3)

    # Afficher le personnage avec la bonne orientation
    # À compléter

    # Afficher toutes les gouttes
    for goutte in GOUTTES:
        # À compléter avec pyxel.circ
        pass


pyxel.run(update, draw)

Question 1

Compléter update_personnage en réutilisant le travail de l'exercice 3. Seuls les déplacements horizontaux sont nécessaires.

Question 2

Compléter update_gouttes. Une goutte replacée en haut pourra recevoir une ordonnée aléatoire comprise entre -HAUTEUR et -1.

Question 3

Sachant que la vitesse de rafraichissement est de 30 frames par secondes, donner les numéros des frames qui correspondent aux premier éclair, puis au second et au troisième.

Compléter update_eclair pour changer la couleur du ciel en jaune pendant deux dixièmes de seconde toutes les .

Question 4

Compléter draw pour afficher les éclairs, le sprite du personnage et toutes les gouttes. Chaque goutte pourra être représentée par un disque de rayon 1.

Question 5

Tester séparément les déplacements du personnage, l'animation des gouttes et leur réapparition, vérifier le déclenchement périodique d'un éclair.

Résultat attendu

alt

Prolongements facultatifs

Choisir une ou plusieurs améliorations :

  • mettre le jeu en pause avec la barre d'espace et pyxel.btnp ;
  • donner aux gouttes des couleurs ou des vitesses différentes ;
  • alterner deux sprites pour animer la marche du personnage ;
  • détecter les collisions entre le personnage et les gouttes ;
  • ajouter un score ou un nombre de vies ;
  • afficher un écran de fin.

8. Mémento⚓︎

Mémento du TP3

Syntaxe Explication / sens
pyxel.run(update, draw) Lance la boucle principale et appelle successivement les deux fonctions.
def update(): Définit la fonction qui met à jour l'état du programme.
def draw(): Définit la fonction qui dessine une image du programme.
pyxel.btn(touche) Renvoie True tant que la touche est maintenue enfoncée.
pyxel.btnp(touche) Renvoie True au moment où la touche est pressée.
pyxel.btnr(touche) Renvoie True au moment où la touche est relâchée.
pyxel.KEY_LEFT, pyxel.KEY_RIGHT Désignent les touches fléchées gauche et droite.
pyxel.KEY_UP, pyxel.KEY_DOWN Désignent les touches fléchées haut et bas.
pyxel.frame_count Contient le nombre d'images affichées depuis le lancement.
pyxel.blt(x, y, img, u, v, -largeur, hauteur, couleur) Affiche un sprite retourné horizontalement.