Evoland.gb : Développer un jeu GameBoy en 2019 (PARTIE 1)

Cet article fait suite à celui qui était sorti à l'occasion de l'anniversaire de la GameBoy, dans lequel je vous avais annoncé le projet Evoland.gb : le portage du jeu Evoland sur la célèbre console portable de Nintendo. J'y expliquais notamment dans quelles circonstances ce projet avait commencé.

Je vais cette fois-ci vous parler du développement du jeu en lui-même : par quelles étapes je suis passé, quelles difficultés j'ai rencontrées,... Le tout en essayant de ne pas aller trop loin dans les détails techniques.

Note

Cet article fait partie d'une série de 4 articles consacrés au projet Evoland.gb :

  1. Evoland sur GameBoy !
  2. Evoland.gb : Développer un jeu GameBoy en 2019 (PARTIE 1)
  3. Evoland.gb : Développer un jeu GameBoy en 2019 (PARTIE 2)
  4. Evoland.gb : Développer un jeu GameBoy en 2019 (bonus) (bientôt disponible !)

Contraintes et périmètre du projet

Avant de se lancer tête-baissée dans la programmation du jeu, il faut d'abord déterminer ce que l'on va programmer exactement et quelles sont nos contraintes.

Pour commencer, le périmètre du projet. Il est évident que je n'allais pas reproduire le jeu dans sa totalité, et ce pour plusieurs raisons, la principale étant tout simplement technique. Evoland est un voyage à travers l'évolution technologique du jeu vidéo... et il n'est pas question de faire de la 3D sur une GameBoy (je sais qu'il existe quelques démos techniques mais c'est de la GameBoy Color, et ça reste très limité). Au mieux on pourra donc aller jusqu'au premier passage en couleurs, dont les graphismes correspondent plus où moins à de la GameBoy Color. En dehors de cette contrainte liée aux graphismes, tous les gameplays du début du jeu devraient pouvoir être reproduits, le but sera donc d'aller le plus loin possible en fonction des autres contraintes.

Capture d'écran des graphismes noir et blanc et couleur 8 bits d'Evoland

Graphismes en noir et blanc et en couleur 8 bits d'Evoland

Une fois que l'on a décidé ce que l'on voulait développer, on va faire le tour des contraintes techniques. Certaines sont incontournables, et d'autres sont imposées par nos choix. Par exemple la mémoire vidéo de la console est limitée et il faudra faire avec (donc probablement appauvrir un peu les graphismes pour que ça rentre). Par contre pour la taille de la ROM on peut choisir d'en avoir une plus ou moins grande mais cela aura diverses incidences sur le projet. J'ai par exemple choisi de tout faire rentrer sur une cartouche de 32k afin d'être en mesure de fabriquer une vraie cartouche à un prix raisonnable, mais en contrepartie, ça va limiter la zone de jeu que je vais être en mesure d'implémenter et je vais devoir bien optimiser mes ressources.

Quand on se lance dans un projet de cette envergure, il faut également se fixer des dates si on veut que ça avance un minimum. J'avais à la base choisi de sortir le jeu pour le 1er avril, mais j'ai assez rapidement repoussé la date au 21 avril pour deux raisons. Déjà je n'aurais pas été prêt pour le 1er avril, surtout avec les problèmes techniques que j'ai rencontrés. Et puis finalement ce n'est plus un poisson d'avril si on développe réellement le jeu derrière (ceci dit je me suis rattrapé en faisant un autre poisson lié à la GameBoy pour le 1er avril ;))... La date du 21 avril quant à elle marque les 30 ans de la GameBoy, elle a beaucoup plus de sens pour sortir un hommage à la console.

Donc on résume :

  • On va reproduire la partie du jeu accessible en graphismes noir et blanc et couleur 8 bit.
  • On ne va pas nécessairement reproduire cette partie en entier, en fonction des autres contraintes (place sur la ROM, temps,...).
  • On va tout faire rentrer sur une ROM de 32 ko pour pouvoir faire des cartouches physiques à un prix raisonnable.
  • On doit avoir avancé au maximum pour avoir un truc présentable pour le 21 avril 2019.

Récupération des éléments du jeu

Une fois nos contraintes et notre périmètre connus, il faut s'atteler à récupérer les différentes ressources du jeu.

Je pensais à ce stade que je devrais me débrouiller tout seul pour extraire les différents éléments graphiques du jeu (récupérer les tiles et les sprites sur des captures d'écran, ou autre). Mais alors que je réfléchissais à quelle solution serait la moins fastidieuse pour extraire les assets (sachant que je ne possède qu'une version Nintendo Switch du jeu...), j'ai reçu un e-email d'Étienne Makowski (le graphiste 3D de Shiro Games), avec toutes les tiles et tous les sprites du début du jeu. Il y avait même une carte du monde qui indiquait où se trouvait chaque élément. Pour le coup c'était une très bonne surprise, ça m'a fait gagner pas mal de temps ! :D

Capture d'écran des assets fournies par Shiro Games

Assets fournies par Shiro Games

Alors forcément, les éléments graphiques fournis par Shiro Games n'étaient pas directement utilisables sur une GameBoy, il a fallu faire quelques adaptations, mais ce fut assez facile car les principales contraintes avaient été respectées (dimension des tuiles et des sprites notamment). J'ai donc sorti Gimp et créé le tileset et la spritesheet pour cette version GameBoy du jeu.

Tilesets du jeu Evoland.gb

Il s'agit ici de la version actuelle des assets, mais elles n'ont pas évoluées tant que ça depuis le début du projet.

Reproduction de la carte du monde

Une fois les assets rassemblées et adaptées, je me suis lancé dans la reproduction de la carte du monde. J'ai pour cela utilisé l'excellent éditeur de carte Tiled qui m'a été recommandé par un collègue (merci Yannick !). C'est un logiciel très complet qui permet de faire à peu près tout ce que l'on veut avec une carte 2D : couches de tuiles, couches d'objets, cases carrées ou hexagonales, droites ou isométrique,... Il a en plus l'avantage d'être libre et gratuit, que demander de plus !

J'ai commencé par reproduire la zone où le joueur débute son aventure, mais sans y placer tous les détails (comme les petites fleurs et autres herbes) afin de pouvoir commencer le développement du jeu. J'ai étendu la carte et rajouté des détails tout au long du projet, en fonction des besoins ou pour faire une pause entre le développement de deux fonctionnalités.

Capture d'écran de l'éditeur de carte Tiled

Capture d'écran de l'état actuel de la carte du monde

Importer les assets

Maintenant en possession des assets et d'une version basique de la carte du monde, il est temps de commencer le développement du jeu.

La première étape est de transformer les éléments du jeu (tilesets, spritesheet, worldmap), qui sont des images au format PNG, en un format utilisable par la GameBoy... parce que forcément, la console ne sait pas du tout lire des images dans les formats qu'on a l'habitude d'utiliser aujourd'hui.

Ceci dit, rien de bien compliqué de ce côté, j'avais déjà développé un outil, img2gb, qui s'occupe de tout ça automatiquement : on lui donne en entrée une image dans presque n'importe quel format courant, et il génère en retours des fichiers de code en langage C qui contiennent les images et les tilemaps au bon format.

Capture d'écran du résultat de la conversion des assets avec img2gb

Exemple de fichiers générés par img2gb

Afficher la map

Maintenant qu'on a importé toutes nos données, on va commencer par afficher la carte du jeu à l'écran et permettre de la scroller (la faire défiler). Et là se pose un premier problème : la carte du monde est beaucoup plus grande que ce qui peut être contenu dans la zone de la mémoire vidéo destinée à stocker les données de la couche Background. Pour vous donner une idée, la version actuelle de la carte est composée de 138×74 tuiles, alors que la mémoire vidéo de la console ne peut en contenir que 32×32, et l'écran quant à lui ne peut en afficher que 20×18. Je vous mets un petit dessin pour que vous puissiez mieux vous rendre compte :

Image comparative des dimensions de la carte du monde, de la mémoire vidéo et de l'écran de la GameBoy

Pour utiliser une carte aussi grande, il va donc falloir charger la carte en continue, au fur et à mesure que le joueur avance. Il s'agit là de la même technique que celle utilisée par des jeux comme Pokemon ou Zelda à l'époque.

Animation montrant le chargement continu de la carte du monde

Capture vidéo du jeu tournant dans l'émulateur / debuggeur BGB. À gauche, ce que l'on voit sur l'écran de la GameBoy, à droite, le contenu de la couche Background dans la mémoire vidéo de la console.

Ça peut sembler facile, mais ça ne l'est pas tant que ça au final. Copier des éléments de la ROM vers la mémoire vidéo est une opération qui peut s'avérer assez lente, il ne faut donc pas que le code autours ne prenne trop de temps à s'exécuter, car l'affichage de la GameBoy est rafraîchit à une fréquence de 60 Hz (soit 60 images par seconde), ce qui signifie que tout traitement doit s'effectuer en moins de 16 ms, sinon le jeu va saccader !

J'ai donc passé pas mal de temps à optimiser cette partie du code, car au fur et à mesure que je rajoutais des éléments de gameplay, le nombre de calculs à effectuer à chaque frame a augmenté. En l'état actuel, il faudrait encore optimiser ce point (même si c'est globalement assez fluide).

Afficher le joueur

Une fois toute la mécanique nécessaire à l'affichage de la carte du monde implémentée, je me suis lancé dans l'affichage du joueur, car la carte seule, ça fait un peu vide.

Capture d'écran des deux sprites du joueur en mémoire dans l'émulateur BGB

Le joueur à besoin de deux sprites

Pour afficher le joueur, il faut utiliser des éléments graphiques spécifique : les sprites. Ce sont des éléments que l'on peut positionner plus ou moins librement à l'écran, contrairement aux tuiles composant la carte, qui elles sont contraintes sur une grille. Sur la GameBoy, les sprites peuvent avoir pour dimension 8×8 px ou 8×16 px. Mon personnage faisant quant à lui 16×16 px, j'ai dû utiliser deux sprites pour l'afficher à l'écran. Cela ne pose toutefois pas particulièrement de problème, car la GameBoy peut en afficher jusqu'à 40 simultanément.

Déplacement du joueur

Déplacement de la carte sous le joueur

J'ai donc commencé par afficher les sprites représentant le joueur au centre de l'écran, statiques, sans gérer aucune animation. Puis j'ai enchaîné sur le déplacement case par case du joueur... ou plutôt de la carte. Le joueur reste en effet immobile au centre de l'écran, et c'est la carte du monde qui se déplace sous ses pieds.

Ce mode de scrolling est en fait le deuxième disponible dans le jeu : le premier est un scrolling par écran fixe où le joueur se déplace à l'écran et la map reste statique, mais j'ai préféré commencer par le scrolling homogène, qui me semblait le plus naturel (puis quasiment tout le code nécessaire avait déjà été écrit à l'étape précédente, lors de l'implémentation du chargement continue de la carte). Quoi qu'il en soit, je n'ai implémenté le scrolling par écran fixe que bien plus tard.

Tuiles sur lesquelles on peut marcher ou non dans Evoland.gb

En vert les tuiles sur lesquelles le joueur peut marcher, en rouge celles qui le bloquent

J'ai ensuite implémenté les collisions avec le décor : c'est-à-dire empêcher le joueur de passer à travers un arbre ou un rocher. Je vous ai montré plus tôt le tileset qui contient les tuiles de la carte du monde. Eh bien l'organisation de ces tuiles n'est pas due au hasard : toutes celles sur lesquelles le joueur peut marcher se trouvent au début du tileset, et celles qui l'empêchent de passer à la fin. Il suffit donc de regarder quel est le numéro de la tuile devant le joueur pour savoir s'il a le droit ou non de marcher dessus.

Conflit entre deux animations

Conflit entre deux animations qui se jouent en même temps

Enfin, je me suis lancé dans l'animation des sprites du joueur, car il faut bien avouer que ça fait bizarre de voir le personnage flotter au-dessus de la carte du monde, tel un fantôme.

Pour animer le personnage, ce n'est pas très compliqué : il suffit de compter les frames, et remplacer l'image des sprites toutes les « n » frames, tant que le joueur se déplace. Pour savoir à quelle vitesse mettre à jour les sprites du joueur, le plus simple c'est d'essayer. Par exemple, j'ai commencé par remplacer les images toutes les 4 frames, mais c'était trop rapide, j'ai essayé 8, c'était trop lent, finalement 6 c'était bien.

Le seul problème que j'ai rencontré à cette étape, ça a été d'oublier de stopper une animation avant d'en commencer une autre... Lorsque cela arrive, les deux animations sont jouées en même temps et on se retrouve avec un joueur qui semble victime d'une crise d'épilepsie...

Une fois tous les petits détails réglés, on se retrouve avec un déplacement qui semble naturel, comme dans les jeux de l'époque :

Animation du joueur et défilement de la carte

La suite au prochain épisode

Je m'arrête ici pour cette première partie, et je vous donne rendez-vous dans une semaine environ pour la suite. J'y aborderai notamment les interactions avec les objets (coffres, buissons), les difficultés rencontrées, la création d'une cartouche, et bien d'autres choses. :)

Note

Si vous souhaitez tester le jeu ou télécharger la ROM, vous pouvez vous rendre sur le site ci-dessous. Je vous mets également le lien du dépôt Github si vous voulez voir à quoi ressemble le code source. :)