Evoland.gb : Développer un jeu GameBoy en 2019 (PARTIE 2)
Bienvenue dans cette seconde partie de mon retour d'expérience sur le développement d'Evoland.gb. Dans la première partie, je vous avais expliqué comment j'avais obtenu et adapté les divers éléments graphiques du jeu, comment j'avais reproduit et géré la map, et je vous avais laissés après vous avoir expliqué comment fonctionnait le personnage du joueur (son animation, les collisions,...).
On va voir cette fois-ci comment le joueur peut interagir avec les différents objets, comment on affiche du texte,... On parlera également de la réalisation d'une cartouche utilisable sur une vraie GameBoy.
Note
Cet article fait partie d'une série de 4 articles consacrés au projet Evoland.gb :
Interactions avec les objets
On avait donc vu dans le dernier article comment était géré le joueur. C'est bien joli de pouvoir se promener sur la carte du monde, mais ça sert pas à grand-chose si on ne peut rien faire de plus.
L'interaction avec divers objets a été l'un des points les plus difficiles de ce projet : j'ai dû m'y reprendre de nombreuses fois avant d'arriver à un résultat fonctionnel et satisfaisant.
Avant de commencer, je vais définir ce que j'entends par « objet ». Les objets sont des éléments interactifs présents à une position fixe sur la carte (ils ne se déplacent pas, contrairement au joueur ou à des ennemis). Dans Evoland.gb, on a deux types d'objets différents :
- des buissons, qui interdisent l'accès à certaines zones tant que le joueur n'a pas récupéré l'épée,
- des coffres, qui débloquent un élément de gameplay (mouvement, scrolling, épée...), ou qui contiennent un bonus comme une carte à jouer ou une étoile (ces éléments ne sont pas encore présents dans Evoland.gb).
L'interaction avec ces deux types d'objets est différente :
- Pour les buissons, il faut posséder l'épée pour pouvoir interagir avec, et la seule chose que l'on peut faire, c'est les couper en appuyant sur le bouton d'action (A) afin de libérer le passage.
- Pour les coffres, il est possible d'interagir avec dès le début du jeu, en avançant dans leur direction à l'aide de la croix directionnelle.
Maintenant que la notion d'objet est claire pour tout le monde et qu'on a fait le tour de ceux présents dans le jeu, voyons comment je me suis vautré en essayant de les implémenter.
Première tentative
Dans un premier temps, j'ai voulu gérer les objets avec des sprites (comme le joueur). Pour cela il faut faire un grand tableau contenant tous les objets présents sur la carte. Quand le joueur se déplace, il faut parcourir ce tableau, afficher à l'écran les objets qui doivent être visibles et virer ceux qui ne sont plus présents dans la zone affichée à l'écran. Seulement ça pose plusieurs difficultés.
La première, c'est que le positionnement des sprites n'est pas lié à la couche Background. Il faut donc synchroniser le déplacement du background (lorsque l'on fait défiler la carte) et la position de tous les sprites visibles. Le problème c'est que je n'ai jamais réussi à parfaitement synchroniser les deux : il y avait toujours un petit flottement, presque imperceptible, mais bel et bien présent (et cela empire lorsque l'on commence à ne plus tenir parfaitement les 60 FPS). Ceci dit, en me repenchant sur des jeux d'époque, comme Pokémon, je me suis aperçu qu'ils souffraient du même problème. Il est donc probable que je ne puisse pas y faire grand-chose...
La seconde difficulté majeure, c'est que le nombre de sprites supportés par la console est limité à 40. Problème : on utilise déjà deux sprites pour afficher le joueur, plus tard il en faudra deux de plus pour son épée, et il faudra aussi en garder pour les ennemis... Et comme il faut également deux sprites pour afficher chaque objet... on se retrouve vite limité. Il est donc impossible d'attribuer directement deux sprites à chaque objet dans le code du jeu, on n'en a pas assez. Il va donc falloir gérer un pool de sprites, c'est-à-dire qu'on va réserver par exemple 20 sprites aux objets, et lorsqu'un nouvel objet doit être affiché, on va lui attribuer les premiers sprites libres de ce pool. Et lorsque cet objet quitte la zone visible à l'écran, on libère les sprites qui servaient à l'afficher ; ils pourront ainsi être réutilisés pour afficher un autre objet.
Malgré ces difficultés, j'étais arrivé à un résultat fonctionnel... enfin ça c'est ce que je croyais quand je n'avais qu'un seul objet sur la carte. Dans mes premiers tests, il y avait en effet qu'un seul objet, et il était toujours « géré », c'est-à-dire que je ne cherchais pas à savoir quand il était censé être visible ou non et donc je gérais sa position, même en dehors de l'écran. Mais dès que j'ai voulu ajouter des objets, et donc réellement mettre en place le système de pool, ça a été une catastrophe. La carte est trop vaste et contient trop d'objets. C'est beaucoup trop long de parcourir tout le tableau, et beaucoup trop lourd de tester la position de chaque objet pour savoir s'il doit être visible ou non. Et comme si cela ne suffisait pas, il faut également mettre à jour la position de tous les objets visibles à l'écran pour qu'ils restent au-dessus de la bonne case de la map.
Pour résumer, ça fonctionnait... mais au prix de ralentissements inacceptables. J'ai donc totalement abandonné cette solution et me suis creusé la tête pour trouver d'autres idées.
On repart de zéro
Je me suis donc mis à réfléchir à la manière dont je pourrais gérer les objets... Et en réalité, il ne me restait pas 50 solutions : si je ne pouvais pas les gérer avec des sprites, il fallait les gérer directement sur la couche Background, c'est-à-dire les intégrer directement dans la carte du monde.
Le souci, c'est qu'une fois « utilisés » (coffres ouverts ou buissons coupés), les objets doivent disparaître et ne plus jamais être réaffichés, et comme la carte est contenue dans la ROM (Read Only Memory, ça veut bien dire ce que ça veut dire), il n'est pas possible de la modifier.
Ma première idée totalement irréfléchie a été de me dire qu'il suffisait de copier la totalité de la carte dans la RAM de la console... mais un rapide calcul m'a vite rappelé à la réalité. La carte du monde occupe environ 10 Ko sur la ROM, et la console ne dispose que de 8 Ko de RAM, sans compter qu'elle contient déjà beaucoup de choses (des tas de variables, la pile,...). Bref, c'est mort, on oublie.
J'ai continué un petit moment à réfléchir et j'ai finalement trouvé une solution réalisable.
La solution
Résumons un peu le besoin :
- il faut savoir si un objet a déjà été activé ou non (information booléenne, c'est-à-dire qui ne peut prendre que deux états : oui ou non),
- il faut que l'accès à cette information soit rapide (pas de parcours de tableau),
- le tout en utilisant le moins d'espace possible.
La solution : une bitmap. Pour faire simple, il s'agit d'un tableau dans lequel on utilise un seul bit pour stocker l'état de chaque case de la map. Si ce bit est à 0, l'objet se trouvant sur la case correspondante n'a pas encore été activé, si ce bit est à 1, l'objet a été activé.
La carte étant composée de 69 cases de large et de 37 cases de haut, et étant donné qu'un octet est composé de 8 bits, il me faut seulement 320 octets pour stocker l'état des cases en mémoire. Ça reste quand même beaucoup, mais au moins cette fois, c'est réalisable ! Maintenant que les mathématiques valident la solution, tout du moins au niveau de la mémoire, il ne reste plus qu'à l'implémenter pour voir si ça fonctionne bien ou pas.
Voici donc ce qu'il va se passer lorsque le joueur se déplace et que l'on fait défiler la carte :
- Comme avant, on charge une ligne ou une colonne de cases dans la mémoire vidéo, au fur et à mesure que le joueur avance.
- On vérifie ensuite la partie de la bitmap correspondante pour savoir s'il y a des objets déjà activés sur l'une des cases que l'on vient de charger,
- si c'est le cas, on patche la mémoire vidéo (donc ce que le joueur voit) avec une tuile de pelouse, ni vu, ni connu.
Et pour ce qui est des interactions avec les objets, c'est la même chose :
- On regarde dans la ROM si la case devant le joueur est un buisson ou un coffre (rappel, la carte dans la ROM reste inchangée, c'est la mémoire vidéo que l'on a éventuellement patchée).
- Si c'est le cas, on regarde dans la bitmap si l'objet a déjà été activé ou non.
- S'il n'a pas été activé, on le marque activé et on effectue l'action correspondant à l'objet,
- s'il a déjà été activé, on l'ignore : officiellement, il n'y a plus rien ici.
Plutôt simple et efficace ! :)
À ce stade, on est capable d'activer des objets, ce qui les fait disparaître et libère le chemin. C'est parfait pour les buissons, mais les coffres vont demander un peu plus de travail puisqu'ils font un peu plus que seulement disparaitre.
L'épée
Maintenant que le plus dur est derrière nous, passons à quelque chose de plus facile, comme l'épée, histoire de souffler un peu... Au final, son action (couper des buissons) est déjà faite, il ne reste plus qu'à faire la partie visuelle... Il s'agit donc seulement d'afficher deux sprites supplémentaires, de les placer au bon endroit lorsque le joueur appuie sur le bouton d'action, et de les animer conjointement aux sprites du joueur.
Pas de grosses difficultés ici, mais un gros plus visuellement.
Les textes
Je me suis ensuite intéressé aux textes. Rien de très compliqué en soi, mais ça demande tout de même un peu de travail.
Première étape : ajouter une police d'écriture... Bien évidemment, une GameBoy ne va pas lire une police d'écriture dans les mêmes formats que celles que l'on utilise sur nos ordinateurs. Sur les consoles de l'époque, il faut placer chaque caractère dans la mémoire vidéo, sous forme de tuiles, exactement comme pour n'importe quel autre élément graphique que l'on souhaite afficher.
J'avais dès le début du projet laissé suffisamment de place libre dans la mémoire vidéo pour pouvoir y placer les quelques caractères dont j'aurais besoin, j'ai donc créé le tileset suivant, qui contient la police d'écriture :
L'ordre des caractères n'est bien évidemment pas dû au hasard : il correspond plus ou moins à celui de la table ASCII afin de faciliter la reproduction du texte, puisque je vais l'écrire dans un éditeur de texte sur mon PC, dans lequel cette norme est en vigueur.
Maintenant que la police est disponible, il suffit d'écrire une petite fonction qui fait correspondre chaque caractère supporté au numéro de la tuile le représentant... J'en dis pas plus, car ce n'est pas bien compliqué à faire... mais pas très intéressant non plus.
Dernière chose à faire : afficher le texte à l'écran. Pour ce faire, je copie la tilemap (carte de tuile) générée par ma fonction dans la mémoire vidéo, mais dans une couche appelée Window cette fois-ci. Cette couche a pour particularité de pouvoir être superposée au background et donc me permet de ne pas modifier ce dernier. Cela me permet également d'animer l'apparition et la disparition de la boîte de texte à l'écran.
J'ai cependant été assez vite confronté à un petit problème : je générais une tilemap contenant tout le texte de la boîte de dialogue d'un coup, et je la stockais dans une variable. Dès que le texte devenait un peu trop long, je me retrouvais à court de mémoire pour le stocker. J'ai donc dû repenser ma manière de faire : j'ai au final généré le texte directement dans la mémoire vidéo.
Cette dernière modification m'a également permis d'animer l'apparition caractère par caractère du texte tel qu'on le voit aujourd'hui dans le jeu :
Les coffres
À ce stade, les coffres sont visibles et on peut les activer, mais ils n'ont toujours pas le moindre effet. Il est donc temps de changer cela.
Pour commencer, j'ai détourné la fonction « texte » de l'éditeur Tiled, afin de numéroter chaque coffre. Ça me permettra de leur associer une action dans le code du jeu.
Puis j'ai écrit un petit script permettant d'extraire cette information du fichier .tmx dans lequel Tiled sauvegarde le projet. Ce script génère en sortie un fichier de code C contenant des données permettant de faire le lien entre la position d'un coffre et son numéro.
Et voilà, il ne me reste plus qu'à écrire les actions correspondant à chaque coffre...
La deadline approche !
Pas mal de choses sont prêtes, mais pour l'instant ça ne ressemble pas trop à un jeu... La deadline approchant, je n'ai plus le temps de rajouter de nouvelles fonctionnalités comme la musique ou les ennemis, mais j'ai encore le temps de finaliser un certain nombre d'éléments de gameplay et de lier tout ce qui a été implémenté ensemble afin que ça ressemble à peu près à un jeu.
Pour commencer, il faut rajouter un écran de titre affichant le logo du jeu, parce que ça fait pas très pro un jeu sans écran de titre. Pour faire le logo, j'ai repris le logo original du jeu, je l'ai réduit dans Gimp, et je l'ai décalqué pixel par pixel. Ce fut long, mais c'était la seule solution pour avoir un résultat correct.
J'ai également implémenté un effet de fondu en jouant avec la palette de couleurs lors de l'affichage et de la disparition du logo, pour que la transition entre l'écran de titre et le jeu soit plus douce.
J'ai ensuite rajouté tous les textes spécifiques à chaque coffre, à la fois en anglais et en français, mais une seule des deux langues est disponible dans la ROM par manque de place. La langue dépend donc de quels fichiers sont inclus lors de la compilation.
Puis j'ai implémenté le scrolling par écran fixe que j'avais repoussé à plus tard lors de l'implémentation de la carte et du joueur. Dans ce mode de défilement, la carte reste statique, et les sprites du joueur se déplacent à l'écran. Je n'ai pas rencontré de difficultés particulières, mais je n'ai pas eu le temps d'optimiser ce mode de défilement, ce qui se traduit par des saccades lors du passage d'un écran à l'autre. La seule chose à laquelle il a fallu faire attention, c'est de bien calculer les endroits où l'on passe d'un écran à un autre pour que le personnage se retrouve au centre de l'écran lorsqu'il ouvre le coffre permettant de passer au défilement continu (sinon il faudrait le « téléporter » au centre, et ça ne serait pas très joli).
Ensuite, j'ai « placé » l'épée dans le coffre où elle était censée se trouver (avant ça, elle était disponible dès le début du jeu). En termes de programmation, il s'agit seulement de rajouter une variable indiquant si le joueur a ouvert le coffre contenant l'épée et d'ignorer ses appuis sur le bouton d'action si ce n'est pas le cas.
Pour finir, j'ai travaillé à la limitation des mouvements et de la vision du joueur au tout début du jeu. En effet, le jeu Evoland commence dans un environnement à une dimension (1D). Au début il est seulement possible de se déplacer vers la droite, puis après avoir ouvert le premier coffre on peut se déplacer vers la gauche. Une fois le deuxième coffre ouvert, l'univers gagne une dimension (2D). Enfin un troisième coffre « active » le scrolling par écran...
Voilà, on a enfin le tout début du jeu à peu près fonctionnel, et présentable. Il resterait encore beaucoup à faire, mais il n'y a plus le temps, il faut passer à la réalisation de la cartouche pour avoir un objet tangible à montrer le jour J (et puis j'adooooore les cartouches :D).
Faire une cartouche, parce que c'est cool les cartouches
Il y a pas mal de possibilités pour mettre une ROM de jeu sur une cartouche qui sera jouable sur une vraie GameBoy. Certaines sont chères mais très faciles, d'autres plus « authentiques »... Je ne vais pas vous faire le tour de toutes les possibilités, je réserve ça pour un futur article de ma série sur le développement GameBoy. On va donc se concentrer sur la solution retenue pour Evoland.gb.
Pour ce projet, j'ai décidé de créer une cartouche individuelle (pas multi-ROM), dédiée au jeu, sur laquelle je pourrais coller une jolie étiquette... comme à l'époque quoi. Pour réaliser cette cartouche, il nous faut un peu de matériel :
- Une Flash cart, c'est-à-dire une cartouche contenant de la mémoire réinscriptible. Je m'en suis procuré une de 32 ko chez Catskull Electronics (oui oui, il y a encore des gens qui fabriquent et vendent ce genre de matériel en 2019 !).
- Et un linker : il s'agit un appareil dans lequel on peut insérer une cartouche afin de la programmer. J'ai pu m'en procurer un chez insideGadgets.
Une fois en possession de ce matériel, c'est assez simple : on branche le linker à un PC, on lance un petit programme auquel on indique quel type de mémoire se trouve dans la cartouche, et la ROM à y inscrire. C'est l'affaire de quelques secondes ou de quelques minutes suivant la taille de la ROM, et le tour est joué.
Il ne reste plus qu'à faire un sticker... et pour commencer, il faut faire un visuel... De ce côté-là, pas de problème : je n'ai même pas eu besoin de me poser la question. Étienne m'a envoyé un email contenant un visuel pour le sticker et pour la boîte du jeu, avant même que je n'aie eu le temps d'y réfléchir (merci beaucoup :D).
Une fois en possession du visuel, il faut l'imprimer sur un papier autocollant, et c'est là que les choses se gâtent... Je ne voulais pas l'imprimer moi-même : je n'ai pas accès à une imprimante de qualité suffisante pour obtenir un résultat qui me satisfasse.
J'ai donc commencé à regarder en ligne s'il était possible d'imprimer des petites quantités de sticker de dimensions personnalisées à un prix raisonnable... mais je n'ai rien trouvé. Généralement les plus petites quantités c'était 100 exemplaires, et ça coûtait plusieurs dizaines d'euros...
Finalement, c'est une collègue qui m'a donné la solution (merci Katia !) : à côté de mon boulot il y a un imprimeur (il s'agit de COPY-TOP pour ceux que ça intéresse), qui était en mesure de m'imprimer n'importe quel visuel sur une feuille A4 autocollante. Je les avais à l'origine écartés de ma recherche car cette prestation ne figure pas sur leur site... Mais il suffisait en fait de leur demander par email. La feuille est par contre fournie en l'état, il faudra découper les stickers soi-même... mais ça, ce n'est pas un problème : un cutter et une règle, ça fait des miracles :)
J'ai donc créé une planche de 24 stickers dans un document au format A4, exporté le tout en PDF et envoyé à l'imprimeur par email... Il ne restait plus qu'à aller chercher la planche imprimée un peu plus tard. Au final ça m'a coûté 2,87 €, soit 12 centimes le sticker, ce qui reste raisonnable au vu de la qualité du résultat.
Sortie le 21 avril
Pour la sortie de la première version publique du jeu, le 21 avril, je n'étais pas du tout prêt. J'avais commencé l'article pour annoncer le projet la veille et je l'ai fini à 5h du matin le jour même... J'avais également prévu de développer un mini-site pour héberger le jeu et permettre de le tester dans un émulateur en ligne... ce site a lui aussi été terminé le jour même... Bref, j'étais un peu à l'arrache. :)
Niveau communication, je n'avais absolument rien prévu... je me suis donc contenté d'un Tweet pour annoncer le projet, et quelques jours plus tard j'ai envoyé un message à PDRoms pour qu'ils référencent le jeu parmi les autres homebrew, et c'est tout (ouais je suis pas très bon en com' ^^').
Heureusement l'équipe du Studio Renegade en a parlé dans l'émission RGB (épisode spécial GameBoy \o/) qui était diffusée 2 jours plus tard, ce qui a aidé à donner un peu de visibilité au projet. Je vous mets ci-dessous la vidéo de l'émission sur Youtube :
Projet terminé ?
Comme je l'avais déjà évoqué, ce projet n'est pas terminé : il reste pas mal de choses à faire et à améliorer, et je ne compte pas l'abandonner. Je vais par contre prendre mon temps. Je vais déjà commencer par me remettre à ma série d'articles sur le développement GameBoy qui est toujours en cours (j'ai encore pas mal de sujets à traiter). Je vais aussi essayer de me trouver un nouveau projet de jeu sur GameBoy à développer, et de temps en temps je repasserai sur Evoland.gb pour le compléter et l'améliorer (je vous tiendrai bien sûr au courant via ce blog et sur Twitter).
Je pensais à l'origine que ce retour d'expérience tiendrait en deux articles, mais j'ai dû laisser certaines choses de côté car cette seconde partie est déjà bien plus longue que je ne l'avais prévu... Je vous retrouve donc prochainement dans un troisième et dernier article « bonus », dans lequel je vous parlerai des incidents techniques, des glitches, des pétouilles, bref, des bugs !
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. :)