Tuto pyxel : Ajouter un fond

Tutoriels Pyxel

Ajouter un fond à notre application Pyxel
Python
NSI
Programmation
Pyxel
Nuit du code
Auteur·rice

F. LALLEMAND

Date de publication

22 octobre 2023

Ce tuto fait suite au précédent : Tuto pyxel : Animer un personnage.

Objectifs du tutoriel
  • Créer un fond constitué de divers éléments ;
  • Animer le fond pour simuler un déplacement du personnage.

Préparation

Nous reprenons le code du tuto précédent comme base de travail, mais nous allons le réorganiser. Plusieurs nouveaux éléments vont en effet devoir être ajoutés au fur et à mesure du développement du jeu, et certains d’entre eux devront, comme le personnage, être animés ou voir certaines de leurs propriétés modifiées au cours du temps. Nous allons donc définir des classes pour chacun de ces éléments, afin de pouvoir les manipuler plus facilement et pour rendre notre code plus lisible. Chacune de ces classes comportera au moins trois méthodes : __init__ pour initialiser l’objet et ses paramètres de départ, puis update et draw. La première sera appelée à chaque frame pour mettre à jour les propriétés de l’objet, la seconde sera appelée pour afficher l’objet à l’écran.

Voici donc le code de départ, réorganisé avec la création d’une classe Personnage. Les méthodes update et draw de cette classe sont appelées depuis les méthodes update et draw de l’application.

import pyxel

class Personnage:

    def __init__(self):
        self.x = 0
        self.y = pyxel.height - 16
        self.direction = 1
        self.marche = False
        self.vitesse = 2
        self.personnage_marche = False

    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT):
            self.x = max(self.x - self.vitesse, 0)
            self.direction = -1
            self.personnage_marche = True
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.x = min(self.x + self.vitesse, pyxel.width - 16)
            self.direction = 1
            self.personnage_marche = True
    
    def draw(self):
        if self.personnage_marche:
            if pyxel.frame_count % 6 < 3:
                pyxel.blt(self.x, pyxel.height-16, 0, 0, 0, self.direction*16, 16)
            else:
                pyxel.blt(self.x, pyxel.height-16, 0, 16, 0, self.direction*16, 16)
            self.personnage_marche = False
        else:
            pyxel.blt(self.x, pyxel.height-16, 0, 0, 0, self.direction*16, 16)
    

class App:
    def __init__(self):
        pyxel.init(256, 128, fps=30)
        pyxel.load("jump_game.pyxres")
        self.personnage = Personnage()
        pyxel.run(self.update, self.draw)
        
    def update(self):
        self.personnage.update()

    def draw(self):
        pyxel.cls(12)
        self.personnage.draw()

App()

Fond statique : Montagne et ciel

L’objectif est de créer le fond de la fenêtre de jeu à l’aide de quatre composantes :

  • une montagne : elle sera fixe et située au milieu de la fenêtre ;
  • un ciel dégradé : il sera fixe et situé sous la montagne ;
  • des nuages : ils seront animés, situés au-dessus de la montagne et défileront dans le ciel ;
  • une forêt : elle sera animée, située derrière le personnage et défilera dans le bas de la fenêtre lorsque le personnage se déplace.
Idée à retenir

D’une manière générale, les objets statiques (dont la position ne change pas et dont les propriétés ne changent pas) peuvent être définis directement dans la classe App. Par contre, pour les objets dynamiques (soit parce qu’ils se déplacent, soit parce que leurs propriétés changent), on crée une classe indépendante dotée (au moins) des trois méthodes __init__, update et draw.

L’examen du fichier de ressources nous permet de déterminer les coordonnées de la zone de l’image correspondant à ces éléments.

  • Montagne : le coin supérieur gauche a pour coordonnées 0 et 64, l’image a une largeur de 160 pixels et une hauteur de 24 pixels.
  • Ciel : le coin supérieur gauche a pour coordonnées 0 et 88, l’image a une largeur de 160 pixels et une hauteur de 32 pixels.

On ajoute donc ces deux éléments à l’arrière plan de la fenêtre de jeu en modifiant comme suit la méthode draw de la classe App :

def draw(self): # classe App
        pyxel.cls(12)
         # Tracé de la montagne
        pyxel.blt(pyxel.width//2 - 80, pyxel.height-50, 0, 0, 64, 160, 24)
        # Tracé du ciel dégradé
        pyxel.blt(0,pyxel.height-32, 0, 0, 88, 160, 32, 12)
        pyxel.blt(160,pyxel.height-32, 0, 0, 88, 160, 32, 12)
        self.personnage.draw()

Explications :

  • Ligne 4 : pyxel.width//2 - 80 est l’abscisse du coin supérieur gauche de l’image dans la fenêtre de jeu. pyxel.width//2 est l’abscisse du milieu de la fenêtre, 80 est la moitié de la largeur de l’image. L’ordonnée pyxel.height-50 est choisie par tâtonnement pour que la montagne soit bien positionnée verticalement dans la fenêtre.
  • Lignes 6 et 7 : la largeur de la fenêtre est 256 pixels, la largeur de l’image est 160 pixels, il faut donc répéter l’image deux fois pour remplir toute la largeur de la fenêtre. On a ajouté ici le dernier paramètre 12 à la fonction blt pour que la couleur 12 (bleu) soit transparente. Cela permet de voir la montagne à travers le ciel dégradé.

Comme le montre l’image ci-dessous, il faut aussi ajouter un dernier paramètre à la fonction blt pour que la couleur 12 (bleu) soit transparente également pour le personnage (dons dans la méthode draw de la classe Personnage).

La couleur bleue derrière le personnage devient transparente

Animation des nuages

Avec le temps qui passe, les nuages se déplacent lentement vers la gauche et d’autres nuages apparaissent sur la droite. Lorsqu’un nuage a complètement disparu de la fenêtre, il est retiré de la liste des nuages.

Nous allons définir une classe Nuage qui possède ses propres méthodes update et draw, comme le montre le code ci-dessous.

class Nuage:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.replaced = False

    def update(self):
        if pyxel.frame_count % 10 == 0:
            self.x = self.x -1

    def draw(self):
        pyxel.blt(self.x, self.y, 0, 0, 24, 160, 16, 12)

Explications :

  • Ligne 8 : if pyxel.frame_count % 10 == 0: permet de faire avancer le nuage de 1 pixel vers la gauche trois fois par seconde, puisque notre application a un framerate de 30 fps.
  • Ligne 5 : Un attribut booléen replaced est ajouté à la classe Nuage pour indiquer si le nuage a été remplacé par un nouveau nuage. Cela permet de ne pas créer un nouveau nuage à chaque frame (voir plus loin).

L’idée est de faire apparaître un nouveau nuage dans le ciel dès que le nuage précédent s’approche du bord gauche de l’écran. Pour cela, il sera nécessaire de définir une liste de nuages dans la classe App et de la mettre à jour à chaque frame. Voici le code modifié de la méthode __init__ de la classe App, dans lequel on initialise la liste des nuages avec un seul nuage placé au milieu de la fenêtre (lignes 6, 7 et 8).

def __init__(self): # class App
    pyxel.init(256, 128, fps=30)
    pyxel.load("jump_game.pyxres")
    self.personnage = Personnage()
    # liste des nuages visibles
    self.nuages = []
    premier_nuage = Nuage(x = pyxel.width//2-80, y = pyxel.height//4)
    self.nuages.append(premier_nuage)
    pyxel.run(self.update, self.draw)

On modifie ensuite la méthode update de la classe App pour mettre à jour la liste des nuages et faire apparaître un nouveau nuage dès que le nuage précédent s’approche du bord gauche de la fenêtre.

def update(self): # class App
        self.personnage.update()
        for nuage in self.nuages:
            nuage.update()
            if nuage.x < 10 and not nuage.replaced:
                nouveau_nuage = Nuage(pyxel.width, pyxel.height//4)
                self.nuages.append(nouveau_nuage)
                nuage.replaced = True
            if nuage.x < -160:
                self.nuages.remove(nuage)

Explications :

  • Lignes 3 et 4 : la boucle for nuage in self.nuages permet de parcourir la liste des nuages et d’appliquer la méthode update à chacun d’entre eux.
  • Lignes 5 à 8 : chaque nuage de la liste est mis à jour et, dès qu’un nuage a son abscisse inférieure à 10 (et qu’il n’a pas encore été remplacé), un nouveau nuage est créé. L’ancien nuage est alors marqué comme remplacé, afin d’éviter de créer un nouveau nuage à chaque frame : nuage.replaced = True.
  • Lignes 9 et 10 : lorsque le nuage a complètement disparu de la fenêtre, il est retiré de la liste : self.nuages.remove(nuage).

Dernière étape, on modifie la méthode draw de la classe App pour afficher tous les nuages de la liste.

def draw(self): # class App
        pyxel.cls(12)
         # Tracé de la montagne
        pyxel.blt(pyxel.width//2 - 80, pyxel.height-50, 0, 0, 64, 160, 24)
        # Tracé du ciel dégradé
        pyxel.blt(0,pyxel.height-32, 0, 0, 88, 160, 32, 12)
        pyxel.blt(160,pyxel.height-32, 0, 0, 88, 160, 32, 12)
        # Tracé des nuages
        for nuage in self.nuages:
            nuage.draw()
        self.personnage.draw()

Animation des nuages

Animation de la forêt

Pour rendre plus réaliste le déplacement du personnage, on peut provoquer un déplacement de la forêt en sens contraire du déplacement du personnage. La montagne et le ciel, eux, peuvent rester fixes, car situés à une grande distance du personnage. Cela donne un effet de profondeur à la scène.

Dans le fichier de ressources, la forêt est constituée d’une image de largeur 160 pixels et de hauteur 16 pixels. Nous allons donc avoir besoin de répéter cette image pour remplir toute la largeur de la fenêtre. De plus, comme la forêt est animée, il faut prendre garde à créer une nouvelle copie de l’image à chaque fois que l’image précédente ne rempli plus totalement la largeur de la fenêtre.

Pour cela, on va créer une classe Foret qui possède ses propres méthodes update et draw, comme le montre le code ci-dessous.

class Foret:
    def __init__(self):
        self.x = 0
        self.vitesse = 1
        self.direction = 1

    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT):
            self.x = self.x + self.vitesse
            self.direction = 1
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.x = self.x - self.vitesse
            self.direction = -1
    
    def draw(self):
        k = 0
        while self.x - k*160 > -160 :
            pyxel.blt(self.x - k*160,pyxel.height-16, 0, 0, 48, 160, 16, 12)
            k = k + 1
        k = 1
        while self.x + k*160 < pyxel.width :
            pyxel.blt(self.x + k*160,pyxel.height-16, 0, 0, 48, 160, 16, 12)
            k = k + 1

Explications :

  • Ligne 3 : self.x est l’abscisse du coin supérieur gauche de l’image dans la fenêtre de jeu. On initialise cette abscisse à 0.
  • Ligne 8 : if pyxel.btn(pyxel.KEY_LEFT): permet de faire avancer la forêt vers la droite lorsque la touche flèche gauche est enfoncée.
  • Ligne 10 : if pyxel.btn(pyxel.KEY_RIGHT): permet de faire avancer la forêt vers la gauche lorsque la touche flèche droite est enfoncée.
  • Lignes 17 à 23 : on utilise une boucle while pour répéter l’image de la forêt autant de fois que nécessaire pour remplir toute la largeur de la fenêtre. La variable k permet de compter le nombre de répétitions nécessaires. On commence par répéter l’image vers la gauche, puis vers la droite.

Pour intégrer cet objet Foret à notre application, il faut maintenant ajouter une ligne à chacune des méthodes de la classe App :

  • Dans la méthode __init__, on crée un objet Foret et on l’ajoute à la liste des attributs de la classe App.

    self.foret = Foret()
Avertissement

Attention : il faut ajouter cette ligne après la ligne pyxel.init(256, 128, fps=30) et avant la ligne pyxel.run(self.update, self.draw).

  • Dans la méthode update, on met à jour l’objet Foret.

    self.foret.update()
  • Dans la méthode draw, on affiche l’objet Foret.

    self.foret.draw()
Avertissement

Attention : il faut ajouter cette ligne avant la ligne self.personnage.draw() : sinon, la forêt sera dessinée devant le personnage.

Animation de la forêt