2018-11-12
Après trois premiers articles qui nous ont permis de nous familiariser avec l'écriture de programmes pour la GameBoy, on va enfin passer aux choses sérieuses : les graphismes. C'était bien sympa de faire des jeux en mode texte, mais ça nous limite vite dans nos réalisations.
Étant donné que le système vidéo de la GameBoy est un sujet assez vaste et central pour la création de jeu, je ne vais pas pouvoir en faire le tour en un seul article. Ce premier article sera donc une entrée en matière plus théorique histoire de bien poser les bases et les suivants seront un peu plus orientés pratique, mais promis je montrerai quand même comment afficher une image à la fin de l'article, il ne faudrait pas que le titre soit mensonger non plus. 😉️
--------------------------------------------------------------------------------
📝️ Note:
--------------------------------------------------------------------------------
Cet article fait partie d'une série sur le développement GameBoy en C avec le compilateur SDCC et la bibliothèque gbdk-n. Cette série est toujours en cours et de nouveaux articles paraissent de temps à autre.
Articles de la série :
--------------------------------------------------------------------------------
Avant toute chose, on va jeter un coup d'œil à l'écran de la GameBoy. Il s'agit d'un écran d'une définition de 160×144 pixels pouvant afficher 4 niveaux de « gris » (enfin niveau de jaune / vert / bleu serait plus juste dans le cas de la toute première GameBoy).
Jusque-là, rien de bien perturbant par rapport à ce que l'on a l'habitude de voir sur nos machines modernes (à part que l'on dispose de plus de couleurs et de pixels)... mais en fait ce n'est pas tout à fait aussi simple : les pixels de l'écran ne sont pas adressables. Cela signifie que l'on ne peut pas juste changer la couleur d'un seul pixel de l'écran, on n'y a tout simplement pas accès. « Mais alors comment qu'on affiche des images sur l'écran si on ne peut pas toucher aux pixels » me direz-vous ? Eh bien c'est très simple, l'écran est divisé en un quadrillage de 20×18 « cases » que l'on appelle des tuiles (ou tiles en anglais). Chaque tuile est un carré de 8×8 pixels :
Détail de l'écran de la GameBoy
Pour afficher une image, il faudra donc la découper en petits morceaux que l'on enverra dans la mémoire vidéo de la console, et il faudra ensuite expliquer à la GameBoy où afficher chacun de ces morceaux. Ce qui est pratique avec cette manière de faire, c'est qu'on peut réutiliser une tuile à plusieurs endroits à la fois, ce qui nous fait économiser de la mémoire.
L'image affichée sur l'écran de la GameBoy est issue de la composition de plusieurs couches :
Les différentes couches d'affichage de la GameBoy
--------------------------------------------------------------------------------
📝️ Note:
--------------------------------------------------------------------------------
NOTE : il est également possible d'afficher des sprites sous la couche background, mais on verra également ça le moment venu. 😉️
--------------------------------------------------------------------------------
La mémoire vidéo (VRAM) de la GameBoy est divisée essentiellement en 3 plages contenant chacune une partie des informations nécessaires à l'affichage :
La partie qui nous intéresse le plus pour le moment est la plage de la mémoire vidéo contenant les données des tuiles (Tiles Data), car c'est avec elle qu'on va travailler le plus directement et qui va nous apporter le plus de contraintes. Voici donc une représentation de cette zone mémoire (chaque case correspond à 16 octets, soit la taille des données d'une tuile de 8×8 pixels) :
Apperçu de la mémoire vidéo (VRAM) de la GameBoy
On peut voir dans le schéma ci-dessus que j'ai divisé cette zone de la mémoire en 3 parties :
On peut donc utiliser au maximum 255 tuiles différents sur chacune des couches, et si on utilise 255 tuiles pour les couches Background et Window, il nous reste seulement 128 tuiles utilisables pour les sprites.
Tant qu'on parle de mémoire, je vous informe qu'il va falloir oublier l'idée d'utiliser le mode texte dans un programme graphique. Fini donc la fonction printf(), les bibliothèques <stdio.h>, <gb/console.h> et compagnie : elles ne feront pas bon ménage avec votre programme.
Pour afficher du texte, les bibliothèques susnommées stockent en effet les caractères affichables sous forme de tuiles dans la mémoire vidéo. Voici une représentation de la mémoire vidéo lorsque ces bibliothèques sont chargées :
Occupation de la mémoire vidéo par la police d'écriture
On se rend vite compte que toute la place est occupée par les caractères et qu'on ne peut plus trop rajouter les images de notre propre jeu. Alors bien sur, il y a plein de caractères qu'on ne va pas utiliser et qu'on pourrait remplacer, mais c'est plus simple dans ce cas de placer seulement ceux dont on a besoin en mémoire et d'y faire appel nous-même plutôt que de jongler avec la bibliothèque <stdio.h> (en plus vous gagnerez pas mal de place dans votre ROM).
Après tout ce que l'on vient de voir, on pourrait se dire que c'est quand même assez compliqué d'afficher une image... mais il ne faut pas se décourager, lisez l'exemple ci-dessous, vous verrez que c'est plutôt simple en réalité. ;)
Pour afficher des tuiles à l'écran, il nous faut commencer par les dessiner. N'importe quel éditeur d'image peut faire l'affaire. Comme j'ai la flemme de dessiner de nouvelles tuiles, je vais reprendre celles que j'avais faites pour mon Snake il y a quelques années :
Tileset issue d'un de mes vieux projets
Il faut ensuite convertir cette image sous forme de code C, dans un format spécifique à la GameBoy... Comme on abordera ce sujet dans le prochain article, je vous donne directement le résultat ci-dessous :
const UINT8 TILESET[] = { // Tile 00: Blank 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Tile 01: Block 0xff, 0x01, 0x81, 0x7f, 0xbd, 0x7f, 0xa5, 0x7b, 0xa5, 0x7b, 0xbd, 0x63, 0x81, 0x7f, 0xff, 0xff, // Tile 02: Snake Body 0x7e, 0x00, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x7e, 0x7e, // Tile 03: Boulder 0x3c, 0x00, 0x54, 0x2a, 0xa3, 0x5f, 0xc1, 0x3f, 0x83, 0x7f, 0xc5, 0x3f, 0x2a, 0x7e, 0x3c, 0x3c, // Tile 04: Cherry 0x04, 0x04, 0x04, 0x04, 0x0a, 0x0a, 0x12, 0x12, 0x66, 0x00, 0x99, 0x77, 0x99, 0x77, 0x66, 0x66, };
Il nous faut maintenant charger ces tuiles dans la mémoire vidéo :
set_bkg_data(0, 5, TILESET);
Après cette étape, notre mémoire vidéo ressemble à ça :
Représentation des tiles dans la mémoire vidéo
Il ne nous reste plus qu'à indiquer à la GameBoy où afficher les tuiles... Si on veut par exemple représenter une scène du célèbre jeu Snake (je prends cet exemple totalement au hasard hein, pas parce que c'est les seules tiles que j'ai sous la main 🤫️), on fait un grand tableau avec les numéros des tuiles à afficher :
Une fois converti sous forme de code, notre tableau devient :
const UINT8 TILEMAP[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 2, 2, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, };
Il ne nous reste plus qu'à copier tout ça dans la mémoire vidéo :
set_bkg_tiles(0, 0, 20, 18, TILEMAP);
Il ne nous reste plus qu'une dernière étape afin de rendre notre image visible à l'écran : il faut dire à la GameBoy qu'elle doit afficher la couche Background (elle est masquée par défaut). Pour ce faire, on appelle la macro suivante :
SHOW_BKG;
Voici donc le code final pour afficher notre magnifique image de serpent croqueur de cerises :
#include <gb/gb.h> const UINT8 TILESET[] = { // Tile 00: Blank 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Tile 01: Block 0xff, 0x01, 0x81, 0x7f, 0xbd, 0x7f, 0xa5, 0x7b, 0xa5, 0x7b, 0xbd, 0x63, 0x81, 0x7f, 0xff, 0xff, // Tile 02: Snake Body 0x7e, 0x00, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x81, 0x7f, 0x7e, 0x7e, // Tile 03: Boulder 0x3c, 0x00, 0x54, 0x2a, 0xa3, 0x5f, 0xc1, 0x3f, 0x83, 0x7f, 0xc5, 0x3f, 0x2a, 0x7e, 0x3c, 0x3c, // Tile 04: Cherry 0x04, 0x04, 0x04, 0x04, 0x0a, 0x0a, 0x12, 0x12, 0x66, 0x00, 0x99, 0x77, 0x99, 0x77, 0x66, 0x66, }; const UINT8 TILEMAP[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 2, 2, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, }; void main(void) { set_bkg_data(0, 5, TILESET); set_bkg_tiles(0, 0, 20, 18, TILEMAP); SHOW_BKG; }
Et voici le résultat une fois ce code compilé et exécuté dans un émulateur :
Capture d'écran du Snake dans un émulateur GameBoy
Vous retrouverez cet exemple sur Github à l'adresse suivante :
https://github.com/flozz/gameboy-examples/tree/master/04-graphics1
Ceci conclut notre première immersion dans le système vidéo de la GameBoy, et je vous retrouve très bientôt pour un prochain article qui détaillera le format d'image utilisé par la GameBoy, et comment convertir ses propres images en tuiles utilisables par vos futurs jeux.
--------------------------------------------------------------------------------