Développement GameBoy #9 : Les palettes

Et voici enfin le dernier article sur la partie graphique de la console. On va parler cette fois des palettes de couleurs. Dans les précédents articles j'ai pas mal éludé ce sujet histoire de pouvoir le traiter convenablement un article dédié.

On pourrait penser au premier abord qu'il est inutile de pouvoir modifier les palettes de couleurs sur une GameBoy, par ce que de toute façon l'écran de la console ne peut afficher que 4 niveaux de gris. Mais voilà, c'est possible et en plus c'est utile : ça permet de faire des effets, comme des fondues ou des flashs lumineux (orage,...). Et c'est encore plus utile pour les sprites qui ne peuvent afficher que 3 couleurs (étant donné que la première couleur de la palette est transparente).

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 :

La GameBoy fournit trois palettes de couleurs :

  • une utilisée par les couches Background et Window, que l'on nomme BGP (Background Palette),
  • et deux utilisées par les sprites : OBP0 et OBP1 (Object Palette 0 / 1).

On peut bien évidemment modifier les couleurs de chacune des palettes à notre convenance, sinon ça ne servirait à rien d'en parler, mais dans la limite des 4 niveaux de gris supportés par l'écran de la GameBoy.

La palette BGP permet donc de définir les 4 couleurs utilisées sur les couches Background et Window, et les palettes OBP0 et OBP1 permettent de définir les 3 couleurs utilisables par un sprite (la couleur numéro 0 est transparente et les 3 autres sont visibles). Chaque sprite choisit individuellement laquelle des deux palettes il utilise.

Palettes de la GameBoy

Palettes de la GameBoy représentées avec les couleurs par défaut

Modifier les palettes

Pour modifier une palette, il suffit de modifier le nombre stocké dans le registre correspondant à la palette que l'on souhaite changer. En C, cela revient simplement à assigner une valeur à une variable spécifique.

Je vous mets tout de suite un exemple pour que vous puissiez vous rendre compte à quel point c'est simple :

BGP_REG = 0xE4;

Le code ci-dessus redéfinit la palette par défaut (blanc, gris clair, gris foncé et noir) des couches Background et Window.

Maintenant que vous avez vu à quel point c'est simple de modifier les palettes, voici les variables correspondant aux différentes palettes :

  • BGP : BGP_REG,
  • OBP0 : OBP0_REG,
  • OBP1 : OBP1_REG.

Rien de bien compliqué donc, il suffit de rajouter le suffixe _REG au nom de la palette.

Les palettes que l'on assigne à ces registres sont des nombres codés sur 8 bits. Chacune des 4 couleurs composant une palette est donc codée sur 2 bits (2 × 4 = 8).

Dans notre entier de 8 bits, la couleur numéro 0 est encodée sur les 2 bits de poids faible, la couleur numéro 1 est stockée sur les 2 bits suivants, la couleur numéro 2 encore sur les 2 bits suivants, et enfin la couleur numéro 3 est stockée sur les 2 bits de poids fort.

Et pour finir, voici la liste des couleurs disponibles :

Couleur Numéro (décimal) Numéro (binaire)
p0 Blanc 0 00
p1 Gris clair 1 01
p2 Gris foncé 2 10
p3 Noir 3 11

Et voici une petite image pour résumer tout ça, histoire que ça soit clair :

Encodage des couleurs dans les palettes de la GameBoy

Une petite macro pour simplifier tout ça

Étant donné que c'est un peu chiant à la longue de calculer sa palette à la main, et que de toute façon c'est pas très lisible dans le code, j'ai écrit la macro suivante pour simplifier les choses :

#define WHITE  0
#define SILVER 1
#define GRAY   2
#define BLACK  3

#define PALETTE(c0, c1, c2, c3) c0 | c1 << 2 | c2 << 4 | c3 << 6

Avec cette macro, au lieu de calculer sa palette et d'écrire le code suivant :

BGP_REG = 0xE4;

on peut écrire directement :

BGP_REG = PALETTE(WHITE, SILVER, GRAY, BLACK);

Je sais pas ce que vous en pensez, mais pour moi c'est plus lisible. 🙂️

Exemple

Et voici donc un petit exemple tout simple pour finir.

#include <gb/gb.h>

#include "tileset.h"
#include "tilemap.h"

#define WHITE  0
#define SILVER 1
#define GRAY   2
#define BLACK  3
#define PALETTE(c0, c1, c2, c3) c0 | c1 << 2 | c2 << 4 | c3 << 6

void wait_frames(INT8 count) {
    while (count) {
        count -= 1;
        wait_vbl_done();
    }
}

void main(void) {
    set_bkg_data(0, TILESET_TILE_COUNT, TILESET);
    set_bkg_tiles(0, 0, TILEMAP_WIDTH, TILEMAP_HEIGHT, TILEMAP);
    SHOW_BKG;

    // Black screen (initial state)
    BGP_REG = PALETTE(BLACK, BLACK, BLACK, BLACK);
    wait_frames(60);  // ~ 1s

    while (1) {
        // Fade-in
        BGP_REG = PALETTE(BLACK, BLACK, BLACK, BLACK);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(GRAY, BLACK, BLACK, BLACK);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(SILVER, GRAY, BLACK, BLACK);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(WHITE, SILVER, GRAY, BLACK);

        wait_frames(60);  // ~ 1s

        // Invert colors
        BGP_REG = PALETTE(BLACK, GRAY, SILVER, WHITE);

        wait_frames(60);  // ~ 1s

        // Fade-out (inverted color)
        BGP_REG = PALETTE(BLACK, GRAY, SILVER, WHITE);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(BLACK, BLACK, GRAY, SILVER);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(BLACK, BLACK, BLACK, GRAY);
        wait_frames(5);   // ~ 0.08s
        BGP_REG = PALETTE(BLACK, BLACK, BLACK, BLACK);
        wait_frames(5);   // ~ 0.08s

        wait_frames(60);  // ~ 1s
    }
}
Capture vidéo de l'exemple

Note

Comme d'habitude, vous retrouverez le code source complet de l'exemple sur Github :

Comme vous avez pu le constater, la manipulation des palettes est extrêmement simple, mais permet néanmoins d'implémenter un certain nombre d'effets intéressants dans nos jeux.