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 7 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 »
  7. Les sprites

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 et en tilemap utilisables 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 arriver à nos fins, on va avoir besoin de deux étapes :

  1. Générer un tileset (c'est-à-dire découper l'image en tuiles de 8×8 pixels, converties dans le bon format, qui seront placées dans la mémoire vidéo de la GameBoy).
  2. Générer une tilemap qui indiquera à la GameBoy où placer chacune des tuiles générées à l'étape précédente.

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

img2gb tileset \
    --output-c-file=src/tileset.c \
    --output-header-file=src/tileset.h \
    --output-image=src/tileset.png \
    --deduplicate \
    ./img.png

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

  • tileset : indique à img2gb que l'on souhaite générer un tileset.
  • --output-c-file=src/tileset.c : La destination du fichier .c à générer.
  • --output-header-file=src/tileset.h : Génère le fichier d'entête (.h) dans src/tileset.h.
  • --output-image=src/tileset.png : Génère l'image du tileset dans src/tileset.png. Cette image à deux intérêts :
    • elle nous permet de voir le résultat de ce qui sera réellement placé dans la mémoire de la GameBoy,
    • elle nous sera nécessaire pour générer la tilemap à l'étape suivante.
  • --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.
  • img.png : L'image à convertir.

En suite, il nous faut générer la tilemap qui nous permettra de reconstituer l'image :

img2gb tilemap \
    --output-c-file=src/tilemap.c \
    --output-header-file=src/tilemap.h \
    src/tileset.png \
    ./img.png

Encore une fois, détaillons les arguments :

  • tilemap : indique à img2gb que l'on va cette fois générer une tilemap.
  • --output-c-file=src/tilemap.c : La destination du fichier .c à générer.
  • --output-header-file=src/tilemap.c : La destination du fichier .h à générer.
  • src/tileset.png : Le chemin de l'image représentant le tileset contant toutes les tuiles utilisées dans l'image (c'est l'image générée par l'option --output-image à l'étape précédente).
  • img.png : L'image à mapper.

Une fois ces commandes exécutées, notre répertoire de travail devrait ressembler à ça :

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

On va jeter un petit coup d'œil aux fichiers .h générés :

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


#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).

src/tilemap.h :

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

#ifndef _TILEMAP_H
#define _TILEMAP_H

extern const UINT8 TILEMAP[];
#define TILEMAP_WIDTH 20
#define TILEMAP_HEIGHT 18


#endif

Dans ce fichier, on trouve la variable TILEMAP et les macros TILEMAP_WIDTH et TILEMAP_HEIGHT. 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"
#include "tilemap.h"

void main(void) {
    set_bkg_data(0, TILESET_TILE_COUNT, TILESET);
    set_bkg_tiles(0, 0, TILEMAP_WIDTH, TILEMAP_HEIGHT, TILEMAP);
    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. :)

Note

Edit 04/02/2018 : J'ai mis à jour la section « Convertir des images avec img2gb » afin que les instructions qui s'y trouvent fonctionnent avec img2gb 1.0 (la branche 0.x étant obsolète aujourd'hui).