Ce tuto fait suite au précédent : Tuto pyxel : Animer un personnage.
- 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:
self.x, pyxel.height-16, 0, 0, 0, self.direction*16, 16)
pyxel.blt(else:
self.x, pyxel.height-16, 0, 16, 0, self.direction*16, 16)
pyxel.blt(self.personnage_marche = False
else:
self.x, pyxel.height-16, 0, 0, 0, self.direction*16, 16)
pyxel.blt(
class App:
def __init__(self):
256, 128, fps=30)
pyxel.init("jump_game.pyxres")
pyxel.load(self.personnage = Personnage()
self.update, self.draw)
pyxel.run(
def update(self):
self.personnage.update()
def draw(self):
12)
pyxel.cls(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.
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éepyxel.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 fonctionblt
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
).
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 classeNuage
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éthodeupdate
à 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 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 toucheflè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 toucheflè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 variablek
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 objetForet
et on l’ajoute à la liste des attributs de la classeApp
.
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’objetForet
.Dans la méthode
draw
, on affiche l’objetForet
.
Attention : il faut ajouter cette ligne avant la ligne self.personnage.draw()
: sinon, la forêt sera dessinée devant le personnage.