Développement GameBoy #11 : Gérer et afficher du texte

Ça fait déjà un an que je n'ai pas sorti un article sur la GameBoy. Je m'étais en effet un peu éloigné du développement rétro pour me concentrer sur d'autres projets, mais ça y est, je vais enfin reprendre cette série d'articles (ne vous attendez cependant pas à des publications régulières) !

Aujourd'hui on va traiter un sujet qui m'a beaucoup, beaucoup, beaucoup été demandé : la gestion du texte ! On va donc voir dans cet article comment créer une font, comment la charger en mémoire, et comment l'utiliser pour afficher du texte sur une GameBoy. 😁️

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 :

stdio.h et printf() ne sont pas vos amis !

Comme je l'avais expliqué dans un précédent article, bien que la bibliothèque "stdio.h" contienne tout ce qu'il faut pour afficher du texte sans effort, elle n'est en pratique pas utilisable dans un jeu.

Cette bibliothèque va en effet remplir la VRAM avec des tas de caractères qui ne nous seront pas utiles, et il ne nous restera plus du tout de place pour les tuiles de notre jeu. On va donc devoir réimplémenter par nous-mêmes un système de gestion du texte, et limiter le nombre de caractères disponibles.

Capture d'écran de la VRAM

Contenu de la VRAM lorsque la bibliothèque stdio.h est chargée

Créer une police d'écriture

Tout comme les autres éléments que l'on souhaite afficher à l'écran, une police d'écriture (ou font en anglais) est composée de tuiles. On va donc commencer par dessiner une image contenant tous les caractères dont on a besoin. Chaque caractère doit tenir dans une tuile de 8×8 px :

Apperçu de la police d'écriture

Comme vous pouvez le voir, j'ai inclus les lettres de A à Z, mais seulement la version en capitale : on se passera des minuscules pour économiser de la place. J'ai aussi inclus les chiffres de 0 à 9 et les ponctuation les plus courantes. Et bien sûr, on n'oublie pas de laisser une tuile blanche que l'on utilisera pour les espaces !

Les plus attentifs d'entre vous auront remarqué que j'ai également ajouté un carré gris et une espèce de double ligne. Le carré gris est un « TOFU », on l'utilisera pour représenter les caractères qui ne sont pas présents dans notre tileset histoire de bien les voir lors du développement. La double ligne quant à elle nous servira peut-être plus tard comme bordure pour une boîte de message.

Exemple d'utiliation du texte

Exemple d'utilisation de notre font dans un jeu

Une fois que notre font est prête, on l'enregistre dans un fichier, que je nommerai "font.png" pour ma part, et il nous faudra ensuite la convertir en tuile pour la GameBoy. Je vais pour cela utiliser le logiciel img2gb et effectuer la conversion à l'aide de la commande suivante :

img2gb tileset \
    --output-c-file=src/font.tileset.c \
    --output-header-file=src/font.tileset.h \
    --name=FONT_TILESET \
    font.png

Je ne vous détaille pas plus cette commande, j'en ai déjà parlé dans le 5ème article de cette série.

Gestion des caractères sur nos ordinateurs : la table ASCII

Que vous utilisiez un PC, une PS4 ou une GameBoy, les caractères, tels que ceux qui composent cet article, ne sont au final que des nombres. Il existe de grands tableaux attribuant un numéro à chaque caractère existant et permettant à la machine de savoir quoi afficher à l'écran quand elle trouve ce nombre en mémoire.

L'un de ces tableaux, qui reste l'un des plus utilisés encore aujourd'hui, s'appelle la table ASCII. Je vous colle les 127 premiers caractères de ce tableau ci-dessous :

|   0  NULL  |   20  DC4   |   40  (     |   60  <     |   80  P     |  100  d     |  120  x     |
|   1  SOH   |   21  NAK   |   41  )     |   61  =     |   81  Q     |  101  e     |  121  y     |
|   2  STX   |   22  SYN   |   42  *     |   62  >     |   82  R     |  102  f     |  122  z     |
|   3  ETX   |   23  ETB   |   43  +     |   63  ?     |   83  S     |  103  g     |  123  {     |
|   4  EOT   |   24  CAN   |   44  ,     |   64  @     |   84  T     |  104  h     |  124  |     |
|   5  ENQ   |   25  EM    |   45  -     |   65  A     |   85  U     |  105  i     |  125  }     |
|   6  ACK   |   26  SUB   |   46  .     |   66  B     |   86  V     |  106  j     |  126  ~     |
|   7  BEL   |   27  ESC   |   47  /     |   67  C     |   87  W     |  107  k     |  127  DEL   |
|   8  BS    |   28  FS    |   48  0     |   68  D     |   88  X     |  108  l     |             |
|   9  HT    |   29  GS    |   49  1     |   69  E     |   89  Y     |  109  m     |             |
|  10  LF    |   30  RS    |   50  2     |   70  F     |   90  Z     |  110  n     |             |
|  11  VT    |   31  US    |   51  3     |   71  G     |   91  [     |  111  o     |             |
|  12  FF    |   32        |   52  4     |   72  H     |   92  \     |  112  p     |             |
|  13  CR    |   33  !     |   53  5     |   73  I     |   93  ]     |  113  q     |             |
|  14  SO    |   34  "     |   54  6     |   74  J     |   94  ^     |  114  r     |             |
|  15  SI    |   35  #     |   55  7     |   75  K     |   95  _     |  115  s     |             |
|  16  DLE   |   36  $     |   56  8     |   76  L     |   96  `     |  116  t     |             |
|  17  DC1   |   37  %     |   57  9     |   77  M     |   97  a     |  117  u     |             |
|  18  DC2   |   38  &     |   58  :     |   78  N     |   98  b     |  118  v     |             |
|  19  DC3   |   39  '     |   59  ;     |   79  O     |   99  c     |  119  w     |             |

On peut par exemple voir dans ce tableau que la lettre "A" capitale est numérotée 65, que le chiffre "5" a pour numéro 53 ou encore que le caractère numéro 32 correspond à l'espace.

Pourquoi est-ce que je vous parle de tout ça me demanderez-vous ? Eh bien par ce qu'on va se servir de cette table ASCII pour afficher notre texte. Si on prend le code C suivant :

printf("Hello");

La chaîne de caractère "Hello" se traduit par la séquence de nombres suivante dans la mémoire de l'ordinateur :

72, 101, 108, 108, 111, 0

C'est cette séquence qu'il va nous falloir traiter pour reconstituer notre texte à l'écran de la GameBoy.

Note

NOTE : Ne faites pas attention au 0 à la fin du tableau de nombre ci-dessus pour l'instant, on en reparlera le moment venu. 😉️

Charger la police de caractère

Pour commencer, il faut que l'on charge la font que l'on a créée en mémoire. Je vais pour cela écrire une fonction que je vais nommer "text_load_font()". Voici le contenu d'un fichier C qui s'occupe de charger la police :

#include <gb/gb.h>
#include "./font.tileset.h"

#define TEXT_FONT_OFFSET 0xD0

void text_load_font() {
    set_bkg_data(TEXT_FONT_OFFSET, FONT_TILESET_TILE_COUNT, FONT_TILESET);
}

void main(void) {
    SHOW_BKG;
    text_load_font();
}

Le code ci-dessus charge les tuiles contenant nos caractères à partir de l'adresse D0 de la mémoire vidéo : j'aime bien en effet caler ma font à la fin de la plage des tuiles pour ne pas être embêté plus tard lorsque je chargerai les autres tuiles de mon jeu. Voici à présent le contenu de la mémoire vidéo si on lance ce programme dans un émulateur :

Capture d'écran de la VRAM

On peut remarquer que le premier caractère, le "A", est bien chargé à l'adresse D0 et que notre font personnalisée prend bien moins de place en mémoire que celle de la bibliothèque "stdio.h" ! 😁️

Afficher un caractère

On arrive enfin au cœur du sujet : l'affichage à proprement parler d'un caractère !

Note

Rappel de C :

Avant de se lancer dans le code, petit rappel : en C, on utilise des simple-quotes pour représenter un caractère :

unsigned char mon_caractere = 'A';

Les double-quotes sont utilisées quant à elles pour représenter des chaînes de caractères (des tableaux de plusieurs caractères) :

unsigned char ma_chaine[] = "Hello";

Commençons par définir quelques constantes pour rendre notre code plus lisible par la suite :

#define TEXT_FONT_OFFSET 0xD0

#define _TEXT_CHAR_A              TEXT_FONT_OFFSET
#define _TEXT_CHAR_0              TEXT_FONT_OFFSET + 26
#define _TEXT_CHAR_COLON          TEXT_FONT_OFFSET + 26 + 10 + 4
#define _TEXT_CHAR_RPARENTHESES   TEXT_FONT_OFFSET + 26 + 10 + 8
#define _TEXT_CHAR_TOFU           TEXT_FONT_OFFSET + 26 + 10 + 9
#define _TEXT_CHAR_SPACE          TEXT_FONT_OFFSET + 26 + 10 + 11

Note

NOTE : Je ne gèrerais pas tous les caractères spéciaux dans cet article pour ne pas rallonger inutilement les exemples de code, mais une version plus complète sera disponible sur Github (lien en fin d'article). 😉️

À présent, on peut écrire une fonction pour convertir nos caractères en tuile et les afficher sur la couche Background de la GameBoy :

void text_print_char_bkg(UINT8 x, UINT8 y, unsigned char chr) {

    // On définit la variable contenant l'adresse de la tuile
    // représentant le caractère à afficher
    //
    // On l'initialise avec le TOFU, qui sera donc affiché
    // dans le cas où le caractère demandé n'est pas géré
    UINT8 tile = _TEXT_CHAR_TOFU;

    // Ici on gère les lettres en minuscule.
    //
    // Il ne faut pas s'étonner si on fait des calculs avec des
    // caractères, pour l'ordinateur il ne s'agit que de nombres
    // compris entre 0 et 255 :)
    if (chr >= 'a' && chr <= 'z') {
        tile = _TEXT_CHAR_A + chr - 'a';

        // Si chr = 'c' le calcul est le suivant :
        //
        // 0xD0 + 'c' - 'a'
        // = 208 + 99 - 97
        // = 210 (0xD2)
        //
        // La tuile représentant la lettre 'C' est donc celle
        // se trouvant à l'adresse 210 (0xD2) de la mémoire vidéo

    // On gère également les lettres en capitale
    } else if (chr >= 'A' && chr <= 'Z') {
        tile = _TEXT_CHAR_A + chr - 'A';

    // Et bien sûr les chiffres
    } else if (chr >= '0' && chr <= '9') {
        tile = _TEXT_CHAR_0 + chr - '0';

    // Ensuite on s'attaque au cas des caractères "spéciaux"
    } else {
        switch (chr) {
            case ':':
                tile = _TEXT_CHAR_COLON;
                break;
            case ')':
                tile = _TEXT_CHAR_RPARENTHESES;
                break;
            case ' ':
                tile = _TEXT_CHAR_SPACE;
                break;
        }
    }

    // Et enfin, on affiche le caractère à l'endroit désiré
    // sur la couche Background
    set_bkg_tiles(x, y, 1, 1, &tile);
}

On peut à présent tester tout ça en modifiant la fonction main() de la façon suivante :

void main(void) {
    SHOW_BKG;
    text_load_font();

    text_print_char_bkg(2, 2, 'A');
    text_print_char_bkg(3, 2, 'b');
    text_print_char_bkg(2, 3, '0');
    text_print_char_bkg(3, 3, '1');
    text_print_char_bkg(2, 4, ':');
    text_print_char_bkg(3, 4, ')');
    text_print_char_bkg(2, 5, '*');  // <- caractère non géré
}

Voici un aperçu de ce que ça donne si on exécute le code ci-dessus :

Capture d'écran du résultat

Afficher une chaîne de caractère

On sait maintenant afficher un caractère, mais ça risque d'être un peu répétitif d'afficher un texte complet caractère par caractère... On va donc écrire une fonction qui fera ça pour nous.

Note

Rappel de C :

Encore une fois, avant de se lancer dans le code, on va faire un petit rappel sur ce qu'est une chaîne de caractère (ou string en anglais) en C.

Une string est simplement un tableau de caractère (unsigned char) se terminant par un caractère nul (\0). Ainsi, comme on l'a vu plus tôt, si l'on écrit la chaîne "hello", le compilateur génèrera le tableau suivant :

72, 101, 108, 108, 111, 0

Ce zéro terminal est essentiel : les fonctions qui manipulent des chaînes de caractères ne connaissent pas leurs longueurs à l'avance ; elles ont donc besoin d'un repère pour savoir où elles se terminent.

Donc si on déclare une chaîne comme un tableau ou qu'on la manipule nous-mêmes, il ne faudra surtout pas oublier de rajouter le caractère nul à la fin, sinon gare à la catastrophe :

unsigned char ma_chaine[] = {'H', 'e', 'l', 'l', 'o', '\0'};

Maintenant qu'on est au clair avec le concept de chaîne de caractère, voici une fonction permettant d'en afficher une sur notre couche Background. Cette fonction boucle jusqu'à trouver le caractère nul (\0) qui délimite la fin de la chaîne, et gère les retours à la ligne manuels (en utilisant le caractère "\n" dans la chaîne) :

void text_print_string_bkg(UINT8 x, UINT8 y, unsigned char *string) {

    // On déclare des variables pour savoir où se trouve le "curseur" du
    // texte (pour éviter d'afficher toutes les lettres au même endroit)
    UINT8 offset_x = 0;
    UINT8 offset_y = 0;

    // On boucle tant que le premier caractère de la string n'est pas \0
    while (string[0]) {

        // On gère les retours à la ligne
        if (string[0] == '\n') {
            offset_x = 0;    // revient à 0 puisqu'on est sur une nouvelle ligne
            offset_y += 1;   // par contre on décale d'une ligne sur y

        } else {
            // On affiche le caractère à la bonne position
            text_print_char_bkg(x + offset_x, y + offset_y, (unsigned char) string[0]);

            // On décale le "curseur" du texte sur x pour le prochain caractère
            offset_x += 1;
        }

        // On décale l'adresse mémoire du tableau de 1 octet. De cette manière
        // le caractère [0] de la chaîne devient le caractère suivant.
        string += 1;

        // NOTE :
        // Ici on fait un truc qui est assez dangereux en C : des math sur des pointeurs.
        // Il faut être très attentif à ce que l'on fait lorsque l'on s'amuse à modifier
        // l'adresse mémoire contenue dans un pointeur sinon on court à la catastrophe...
        // mais bon là on est sur une GameBoy, donc au pire on fera planter le programme. ;)
    }
}

Si on veut tester cette fonction, on peut l'appeler depuis notre fonction main() :

void main() {
    // ...

    text_print_string_bkg(2, 7, "Hello\nWorld!");
    //                                ↑↑ retour à la ligne
}

Et voici le résultat une fois exécuté dans notre émulateur :

Capture d'écran du résultat

Le mot de la fin

Au final, gérer du texte est un poil fastidieux, mais pas vraiment compliqué. J'espère en tout cas avoir pu vous éclairer sur ce sujet. En complément de cet article, je vous invite à visionner les deux épisodes suivants de l'émission Hello World à laquelle je participe actuellement, qui traitent justement de la gestion du texte sur GameBoy :

Hello World 46 sur Youtube Hello World 47 sur Youtube

Et comme de coutume, l'exemple de cet article est disponible sur Github :

À bientôt pour de nouveaux articles ! 😋️

'\0'