Quatrième chapitre de l’apprentissage de Pyxel, framework de retro game avec son moteur en Rust (pour la fluidité) et son API en Python (pour la simplicité).
Cette démo explore le jeu de plateforme avec une carte de tuiles créée dans un logiciel spécifique.
Démo
Le héros, nommé Jones, explore les dangereuses ruines d’un temple oublié.
Assets
Jusqu’à présent, les démos n’utilisaient que peu de graphismes, mais là, pour expliquer l’utilisation des cartes et des collisions, il va falloir plus de pixels, beaucoup plus.
Pour le défi, et pour avoir un aspect encore plus vintage, les sprites seront cette fois-ci de 8 pixels de côté.
Carte de tuiles
Historiquement, pour gérer l’affichage avec très peu de RAM et une carte graphique antique, tout était tuiles. L’écran est alors rempli de gros carrés qui peuvent être répétés.
La Game Boy, par exemple, utilise un Picture Processing Unit capable de gérer jusqu’à 40 sprites de 8 x 8 ou 8 x 16 sur un écran de 160 x 144 pixels, sur 3 calques, et seul le dernier permet de positionner le sprite au pixel près.
Pyxel se contente de rendre hommage aux vieilles consoles, la plupart des contraintes sont des conventions, comme la palette réduite ou le tout petit écran.
Les cartes permettent de composer une grande image (et même plus grande que l’écran, permettant ainsi de déplacer la caméra pour un effet de scroll) à partir de tuiles. Certaines tuiles , par convention, peuvent être considérées comme solides, pour que le personnage puisse marcher dessus et ne pas tomber.
Pyxel (arbitrairement) ne peut gérer que 3 cartes.
Concrètement, les cartes permettent de travailler à 3 sans se marcher sur les pieds:
- Le graphiste crée le jeu de tuiles et le peaufine ensuite.
- Le game designer compose la carte à partir du jeu de tuiles pour avoir quelque chose de jouable et d’intéressant.
- Le développeur utilise la carte pour vérifier les actions (courir, sauter…) et les comportements (tomber, se cogner dans un mur…) du personnage.
Pyxel fournit un éditeur de carte, mais il est un peu rugueux (comme tout ce que propose son éditeur d’assets).
Tiled est un éditeur de cartes un poil plus civilisé, et il est utilisable par quantité de moteurs de jeux. Attention, Pyxel ne sait pas utiliser toutes les options de Tiled, ni tous les formats de sortie.
Tiled
Pyxel gère les cartes TMX avec les calques (pas plus de 3).
Les TSX (jeu de tuiles) doivent être embarqués dans la carte TMX.
Les tuiles non définies dans la carte seront remplacées par le premier bloc du jeu de tuiles.
Pour que la carte gère la transparence (pour avoir un fond uni, par exemple), il faut utiliser une tuile uniforme, dont la couleur sera utilisée comme transparence.
La palette de couleurs utilisée par Pyxel sera issue de la carte.
Nouvelle carte
Nouveau fichier : ctrl-N
- Orientation
- orthogonale
- Format de calque des tuiles
- CSV
- Ordre d’affichage des tuiles
- En bas à droite
- Taille de la carte
- 20 tuiles x 15 tuiles, soit 160 x 120 pixels
- Taille des tuiles
- 8 x 8 pixels
Le jeu de tuiles doit être disponible pour créer une carte, mais ne vous inquiétez pas, il est possible de modifier le PNG, Tiled le rechargera tout seul (sinon, un ctrl-r
fera l’affaire).
Tout en bas à droite, le bouton Nouveau jeu de tuiles…
- Nom
- mettez ce que vous voulez, sinon, le nom par défaut est basé sur le nom du fichier.
- Type
- Basé sur l’image du jeu de tuiles
- Embarquer la carte
- ✔︎
- Source
- le chemin du PNG, sélectionnable via [ Parcourir… ]
- Largeur/Hauteur
- 8 pix
- Marge/espacement
- 0 pix
- Utiliser la couleur transparente
- ✔︎ puis le second carré, qui fait apparaitre une mini-carte avec le PNG pour sélectionner la couleur.
La carte apparait dans le coin bas-droite, microscopique, il faut zoomer avec le menu déroulant, pour atteindre au moins les 400%.
Créer sa carte
La grille dans la zone principale, trop petite et mal centrée.
ctrl-+
et ctrl--
pour zoomer, sinon, on peut tricher avec ctrl-:
.
Pour les adeptes du menu, ces réglages sont rangés dans Vue
.
Sélectionnez une tuile dans le jeu de tuiles en cliquant dessus, elle devient grisée.
Avec l’outil Tampon
(raccourci : B
), vous pouvez tamponner de la tuile à volonté sur la carte.
Pour l’esthétique, pour les plateformes, faites attention à commencer par une tuile début, terminer par une tuile fin, et au milieu des tuiles milieu.
Il ne faut pas utiliser le personnage (qui sera utilisé comme sprite), la carte ne doit contenir que des éléments statiques.
Enregistrer le fichier, ctrl-s
, au format TMX.
Tuiles
La carte a besoin d’éléments regroupés dans un fichier PNG. Sélectionnez un éditeur spécialisé dans le pixel-art et non un gros éditeur qui sera frustrant (Krita, Gimp, PowerPoint…).
Comme outil libre et multi-plateformes, je vous propose :
- Piskel est un éditeur web. OK, il est abandonné depuis 6 ans, mais il fonctionne bien, et il est possible de sauvegarder les fichiers source en local.
- Pikopixel est un éditeur conçu pour GNUStep, mais qui fonctionne sur la plupart des plateformes.
Il doit surement en exister d’autres, dont certains propriétaires, comme Pixen.
Utilisez une palette de couleur réduite, ça aidera à rendre l’ensemble cohérent, et ça rend hommage aux anciens. Il y a des sites pour ça, comme Colorkit.
Décors
Le plus important dans ce type de jeu sont les plateformes. Il faut donc un début, un milieu, une fin de plateforme. Pour simplifier la gestion des collisions, il faut que ces blocs utilisent au maximum les 8 pixels des sprites.
Il est aussi possible d’utiliser des tuiles pour décorer, qui ne gênent pas la progression du personnage.
Le jeu de tuiles (32 x 32 pixels) de la démo est organisé par ligne :
- La plateforme : début, milieu, fin
- Des décors : herbes, torche, vase
- Le héros : marche 1, marche 2, tombe, saute
Les tuiles sont transparentes, mais pour la lisibilité, le héros a un contour noir (la couleur du fond dans le jeu), pour se détacher du fond.
Les ruines vides
On va commencer tranquillement, en se contentant d’afficher le décor.
Le PNG est chargé dans l’emplacement 1 de la banque d’images.
Le calque 0 du TMX est chargé dans l’emplacement 1 de la banque de cartes.
L’image 1 est connectée à la carte 1.
Dans draw
, la carte est affichée.
Le troisième élément désigne la carte 1.
Le dernier élément désigne la couleur de transparence.
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="The temple") # width, height, title
pyxel.images[1] = pyxel.Image.from_image("temple.png", incl_colors=True)
pyxel.tilemaps[1] = pyxel.Tilemap.from_tmx("temple.tmx", 0)
pyxel.tilemaps[1].imgsrc = 1 # The map uses this image for its sprites
pyxel.run(self.update, self.draw) # Starts Pyxel loop
def update(self):
if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
pyxel.quit()
def draw(self):
pyxel.cls(9) # Clear screen
# x, y, tm, u, v, w, h
pyxel.bltm(0, 0, 1, 0, 0, pyxel.width, pyxel.height, 0)
App()
Il y a une astuce pour obtenir la palette et connaitre le rang des couleurs :
Pyxel fournit un raccourci pour sauvegarder la palette de couleurs sous forme de PNG : Ctrl-Shift-Alt-0
.
Tomber dans le vide
Pour bien isoler les parties du code, une class abstraite Sprite
est utilisée, ainsi qu’un moteur (primitif) de physiques.
Collisions
connait la liste des tuiles solides (leurs coordonnées dans le jeu de tuiles), qui ne peuvent pas être traversées.
Dans ce jeu, point de mur, juste du sol pour atterrir avec grâce.
Pour éviter les références croisées, Collisions
est un attribut de App
confié à la méthode update
du Sprite
.
Le personnage (un Sprite
) va pouvoir s’avancer dans le vide au bord d’une plateforme, jusqu’à ce que son dernier pixel soit au-delà du dernier pixel de la plateforme.
import pyxel
class Sprite:
def __init__(self, x, y):
self.x = x
self.y = y
self.direction = 1 # 1: -> -1: <-
class Physics:
def __init__(self, solids: list[tuple[int, int]]):
"List of solid tiles"
self.solids = solids
self.map = 1
def floor(self, sprite: Sprite) -> bool:
"Does the sprite have floor under its feet?"
if sprite.direction == -1: # the tile before
x = pyxel.ceil(sprite.x / 8)
else: # the tile after
x = pyxel.floor(sprite.x / 8)
# the tile under the feet of Jones
tile = pyxel.tilemaps[self.map].pget(x, sprite.y // 8 + 1)
return tile in self.solids
def too_low(self, sprite: Sprite) -> bool:
"Does the sprite fall under the bottom of the screen?"
return sprite.y >= (pyxel.height - 8)
Jones
hérite de Sprite
.
De manière similaire aux chapitres précédents, Jones
va gérer un state
qui va permettre de choisir la bonne image, et de gérer la physique, essentiellement les sauts et les chutes.
C’est le héros qui va gérer l’affichage clignotant du tant redouté GAME OVER.
TRANSPARENT = 0
WAITING = 0
WALKING = 1
FALLING = 2
JUMPING = 3
DEAD = 4
class Jones(Sprite):
def __init__(self, x, y):
super().__init__(x, y)
self.images_right = [(i * 8, 16, 8, 8, TRANSPARENT) for i in range(4)]
self.images_left = [(i * 8, 16, -8, 8, TRANSPARENT) for i in range(4)]
self.state = WAITING
self.how_high = 0
self.death_time = 0
def image(self) -> tuple[int, int, int, int, int]:
if self.direction == 1:
images = self.images_right
else:
images = self.images_left
if self.state == FALLING:
return images[2]
if self.state == WALKING:
return images[(pyxel.frame_count // 5) % 2] # move legs every 5 frames
if self.state == JUMPING:
return images[3]
# WAITING
return images[0]
def physic(self, world: Collisions):
if self.state == DEAD: # there is no physics when you are a soul
return
if self.state == JUMPING:
if self.y > self.how_high:
self.y -= 2
self.x += self.direction
else: # start falling soon
self.state = WAITING
return
if world.too_low(self):
self.death_time = pyxel.frame_count + 30
self.state = DEAD
return
if world.floor(self):
if self.state == FALLING:
self.state = WAITING # soft landing
return
else:
self.state = FALLING
self.y += 2
def move(self, direction):
if direction == 0:
self.state = WAITING
else:
self.state = WALKING
self.direction = direction
self.x += self.direction * 2
def jump(self, direction):
self.state = JUMPING
self.how_high = self.y - 16
self.direction = direction
def update(self, world: Collisions):
if self.state in (WAITING, WALKING):
if pyxel.btn(pyxel.KEY_RIGHT):
dx = 1
elif pyxel.btn(pyxel.KEY_LEFT):
dx = -1
else:
dx = 0
if pyxel.btnp(pyxel.KEY_UP):
self.jump(dx)
else:
self.move(dx)
if self.state == DEAD and pyxel.frame_count > self.death_time:
pyxel.quit()
self.physic(world)
def draw(self):
if self.state == DEAD:
if pyxel.frame_count % 10 < 5:
pyxel.text(70, 10, "GAME OVER", 8, None)
else:
pyxel.blt(self.x, self.y, 1, *(self.image()))
L’application, App
, est super classique.
Elle va charger les assets, instancier le moteur de physique avec les trois premières tuiles comme étant solides, puis instancier le héros, qui commence la partie sur une chute.
class App:
def __init__(self):
pyxel.init(160, 120, title="The temple") # width, height, title
pyxel.images[1] = pyxel.Image.from_image("temple.png", incl_colors=True)
pyxel.tilemaps[1] = pyxel.Tilemap.from_tmx("temple.tmx", 0)
pyxel.tilemaps[1].imgsrc = 1 # The map uses this image for its sprites
self.world = Physics([(0, 0), (1, 0), (2, 0)])
self.jones = Jones(8, 0)
pyxel.run(self.update, self.draw) # Starts Pyxel loop
def update(self):
if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
pyxel.quit()
self.jones.update(self.world)
def draw(self):
pyxel.cls(9) # Clear screen
# x, y, tm, u, v, w, h
pyxel.bltm(0, 0, 1, 0, 0, pyxel.width, pyxel.height, 0)
self.jones.draw()
App()
Les sources de Jones sont disponible sur Github.
La suite
Le personnage pourrait courir (en plus de marcher) pour pouvoir faire des sauts plus longs.
Il pourrait aussi s’accrocher quand il finit son saut sur un mur, puis remonter sur la plateforme (comme dans Prince of Persia).
Il pourrait s’arrêter en dérapant juste au bord du vide (comme dans Flashback).
Pour l’ambiance, ce serait bien d’avoir la flamme des torches animées (en recouvrant l’image de fond par un autre sprite).
Les herbes pourraient onduler au passage du héros (et continuer quelques secondes après).