Développement GameBoy #5 : Créer des tilesets

Dans le précédent article on a abordé le système vidéo de la GameBoy de manière générale, cette fois-ci l'article sera plus ciblé : on va voir comment créer des tuiles utilisables par nos applications.

On va commencer par regarder comment doivent être encodées les images pour pouvoir être copiées dans la mémoire vidéo. On verra ensuite comment convertir les images avec un petit outil nommé img2gb.

Note

NOTE : Cet article fait partie d'une série de 6 articles (d'autres sont en cours de rédaction) dont vous trouverez la liste ci-dessous :

  1. Hello World
  2. Utiliser le gamepad
  3. Projet 1 - Tic Tac Toe
  4. Afficher des images
  5. Créer des tilesets
  6. La couche « Background »

Encodage des images

Dans la dernière partie du précédent article, on avait vu rapidement comment afficher des tuiles à l'écran de la GameBoy... tellement rapidement que je n'avais pas pris le temps d'expliquer comment on passe d'une image comme celle-ci :

Tileset issue du jeu zZNAKE (zoom x10)

à un tableau d'octets comme celui-là :

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,
};

On va donc regarder étape par étape comment encoder chaque pixel d'une image pour qu'elle puisse être utilisée dans nos applications. Étant donné que ce sera toujours la même chose pour chaque tuile, on va se concentrer sur une seule. On va donc prendre la tuile représentant des cerises comme exemple (parce que je l'aime bien et que c'est moi qui décide :p) :

La fameuse tuile représentant des cerises

La première chose à faire, c'est de faire correspondre chaque pixel à l'une des couleurs de la palette de la GameBoy (on verra quand on parlera des sprites qu'il y a en fait plusieurs palettes, mais on ne va pas s'en préoccuper pour le moment), sachant qu'à chaque couleur correspond un numéro :

Conversion des couleurs en numéro issue de la palette

Ensuite, il faut convertir les numéros des couleurs en binaire (2 bits par pixels) :

Conversion des numéro de couleur en binaire

Enfin, il faut regrouper les bits obtenus en octets, c'est la partie la plus compliquée car l'encodage n'est pas forcément des plus intuitifs :

  • Chaque ligne (de 8px), est encodée sur 2 octets (16 bits).
  • Pour encoder le premier pixel, on place le bit de poids faible de notre couleur dans le bit de poids fort du premier octet, et le bit de poids fort de la couleur dans le bit de poids fort du second octet.
  • Pour le second pixel, on place le bit de poids faible de la couleur dans le second bit de poids fort du premier octet, et le bit de poids fort de la couleur dans le second de bit de poids fort du second octet.
  • Et ainsi de suite jusqu'à la fin de la ligne.
  • Et on recommence la même opération pour chaque ligne jusqu'à la fin de la tuile. Au final on se retrouvera avec 16 octets (soit 128 bits) représentant notre image.

Si vous arrivez jusqu'ici en ayant tout compris du premier coup, bravo, sinon c'est normal... c'est pas quelque chose d'évident à expliquer à l'écrit... c'est pourquoi j'ai fait un « joli » dessin qui devrait beaucoup faciliter la compréhension :

Regroupement des bits en octets

Au final on a plus qu'à convertir nos octets en C, en écriture hexadécimale car c'est quand même plus court. Notre image de cerises devient donc :

const UINT8 cherry = {
    0x04, 0x04, 0x04, 0x04, 0x0a, 0x0a, 0x12, 0x12,
    0x66, 0x00, 0x99, 0x77, 0x99, 0x77, 0x66, 0x66,
};

Comme vous pouvez vous en rendre compte, ce n'est pas si compliqué, mais si on devait faire ça à la main ce serait très répétitif et ça prendrait énormément de temps... C'est pourquoi on va voir comment automatiser la conversion dans la suite de cet article.

Convertir des images avec img2gb

img2gb est un petit logiciel en Python que j'ai développé pour automatiser la conversion d'une image en tileset utilisable sur GameBoy. Concrètement, ça prend en entrée une image dans un format courant (PNG, JPEG,...) et en sortie ça génère des fichiers .c / .h.

Le logiciel devrait fonctionner sur n'importe quel OS où un environement Python (2.7 ou 3.x) est disponible et sa seule dépendance est Pillow (une bibliothèque de manipulation d'image). On va donc commencer par l'installer afin de pouvoir l'utiliser (il vous faudra probablement des droits root sous Linux) :

pip install img2gb

Note

NOTE pour les utilisateurs de Windows : Comme Python n'est pas installé de base sur cet OS, je vous ai compilé tout ça dans un executable pour vous éviter d'avoir trop de trucs à installer et à configurer. Vous pouvez télécharger la dernière version de img2gb.exe sur la page de release du projet sur Github.

Pas la peine d'essayer de double-cliquer dessus, il n'y a pas d'installer ni d'interface graphique pour le moment : il s'utilise en ligne de commande (exemple : img2gb.exe -h pour afficher l'aide).

Une fois installé (ou copié dans votre répertoire courant pour le .exe), la commande img2gb devrait être disponible... et c'est ce que l'on va essayer tout de suite. :)

Pour notre exemple, on va convertir l'image suivante et écrire un peu de code pour l'afficher sur une GameBoy :

Image à convertir

On va supposer que notre répertoire de travail est le suivant :

+-- test-project/
    |
    +-- src/
    |   |
    |   +-- main.c
    |
    +-- img.png

Pour commencer, on va utiliser img2gb pour convertir img.png en C :

img2gb --deduplicate --map --header-file=src/tileset.h img.png src/tileset.c

Prenons le temps de détailler un peu les arguments de la commande :

  • --deduplicate : Évite d'inclure plusieurs fois des tuiles identiques. Étant donné que notre image est composée de 360 tiles et qu'on ne peut en stocker que 255 en mémoire, on a de toute façon pas le choix, pour cet exemple il va falloir optimiser en dédupliquant les tuiles.
  • --map : Génère la tilemap correspondant à l'image. Ceci est utile seulement si on souhaite réafficher l'image telle quelle sur l'écran de la GameBoy, sinon on n'en a pas besoin.
  • --header-file=src/tileset.h : Génère le fichier d'entête (.h) dans src/tileset.h. Si on ne passe pas cette option, seul le fichier .c sera généré.
  • img.png : L'image à convertir.
  • src/tileset.c : La destination du fichier .c à générer.

Une fois la commande exécutée, notre répertoire de travail devrait ressembler à ça :

+-- test-project/
    |
    +-- src/
    |   |
    |   +-- main.c
    |   |
    |   +-- tileset.c
    |   |
    |   +-- tileset.h
    |
    +-- img.png

On va jeter un petit coup d'œil au fichier src/tileset.h :

// This file was generated by img2gb, DO NOT EDIT

#ifndef _TILESET_H
#define _TILESET_H

extern const UINT8 TILESET[];
#define TILESET_TILE_COUNT 97

extern const UINT8 TILESET_MAP[];
#define TILESET_MAP_WIDTH 20
#define TILESET_MAP_HEIGHT 18

#endif

Ici, on peut voir qu'une variable TILESET est disponible, c'est elle qui contient toutes les tuiles extraites de l'image.

On voit également qu'une macro TILESET_TILE_COUNT a été générée et qu'elle contient le nombre de tuiles (on a donc 97 tuiles différentes pour afficher notre image).

Viennent ensuite la variable TILESET_MAP et les macros TILESET_MAP_WIDTH et TILESET_MAP_HEIGHT qui ont été générées par l'option --map passée à la ligne de commande. Elles contiennent respectivement, la tilemap permettant de recomposer notre image, la largeur de ladite tilemap et sa hauteur.

Maintenant qu'on a converti notre image en C, et qu'on a toutes les informations nécessaires, on peut écrire un petit programme pour l'afficher sur l'écran d'une GameBoy. Voici le contenu du fichier main.c :

#include <gb/gb.h>

#include "tileset.h"

void main(void) {
    set_bkg_data(0, TILESET_TILE_COUNT, TILESET);
    set_bkg_tiles(0, 0, TILESET_MAP_WIDTH, TILESET_MAP_HEIGHT, TILESET_MAP);
    SHOW_BKG;
}

Il ne nous reste plus qu'à compiler tout ça :

lcc src/*.c src/*.h -o program.gb

Note

NOTE : si vous avez des soucis avec la compilation de l'exemple, je vous invite à relire les instructions du premier article. ;)

... et à lancer la ROM générée avec l'émulateur de votre choix. Si tout s'est bien passé, vous devriez voir un résultat similaire à celui-ci :

Capture d'écran du résultat

Vous retrouverez bien sur le code complet de l'exemple ci-dessus sur Github :

À présent qu'on sait comment convertir des images en tiles utilisables sur la GameBoy, il ne nous reste plus qu'à découvrir toutes les façons de s'en servir dans un jeu. On abordera donc les différentes couches (Background, Window et Sprites) dans les prochains articles afin de pouvoir commencer à développer de vrais jeux. :)