Développement GameBoy #10 : Projet 2 - Breakout (PARTIE 1)

Maintenant que j'en ai terminé avec les articles sur la partie graphique de la GameBoy (6 articles tout de même !), il est temps de réaliser un petit projet pour synthétiser tout ça. J'ai choisi de faire un casse-briques, premièrement parce que ça va nous permettre de réutiliser un maximum de choses vues dans les articles précédents, mais surtout parce que j'adore les casse-briques 😜️.

Dans cet article je ne vais pas autant rentrer dans les détails du code que dans celui du premier projet : c'était beaucoup trop long (d'ailleurs je vais couper cet article en plusieurs parties pour que ça ne soit pas trop lourd) ! Je vais donc surtout vous décrire la démarche et les étapes par lesquelles je suis passé pour arriver au résultat final, en détaillant les points qui me semblent importants (mais il y aura quand même un peu de code hein 😉️).

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 les nouveaux articles paraissent à un rythme d'environ un par mois.

Articles de la série :

Dessin des éléments graphiques

Pour commencer le projet, on va dessiner les différents éléments graphiques dont on aura besoin. À ce stade du projet, ces éléments peuvent n'être que des placeholders, c'est-à-dire qu'ils n'ont pas forcément besoin d'être jolis du moment qu'ils font le taf. Ceci dit, ce n'est pas interdit de faire des trucs un peu plus recherchés si on a des idées hein 😊️.

Avant de se lancer dans le dessin, listons les éléments dont on aura besoin pour notre jeu. Pour faire un bon casse-briques il nous faut :

  • des briques,
  • une balle pour casser les briques susnommées,
  • une raquette pour rattraper la balle,
  • et des bordures pour délimiter le terrain de jeu et empêcher la balle de sortir.

Voici un petit schéma de tout ça pour se faire une idée :

Schémas d'un jeu de casse-briques

Rien de bien compliqué donc, il suffit de prendre n'importe quel logiciel de dessin avec lequel on est à l'aise, et dessiner les différentes tuiles qui composent les éléments du jeu. Pour ma part j'ai utilisé GIMP, dans lequel j'ai créé une image en mode couleurs indexées (pour me limiter à 4 couleurs) et dans lequel j'ai affiché une grille de 8×8 px (puisque les tuiles de la GameBoy font cette dimension-là).

Capture d'écran du dessin des éléments graphiques dans GIMP

Dessin des tuiles de GIMP

Une fois que j'ai été à peu près content de moi, j'ai réorganisé les tuiles pour que ce soit plus facile de les utiliser dans le programme par la suite. J'ai donc placé la tuile vide en premier, suivie de toutes les tuiles des bordures, puis les tuiles qui composent les briques et enfin les tuiles que j'utiliserai pour la raquette et la balle.

J'en ai également profité pour placer la balle en haut à gauche de sa tuile et la raquette en haut : ça sera plus pratique pour les calculs plus tard.

Tileset réordonné

Tileset après avoir réordonné les tuiles

Composition du premier (et unique) niveau

Une fois que j'ai terminé les différentes tuiles dont j'ai besoin, j'ai créé la tilemap représentant le premier niveau du jeu. J'ai pour ça sorti l'éditeur de cartes Tiled afin de composer le niveau.

On commence par lui définir les différents paramètres, comme largeur et hauteur de la map (dans mon cas 20×18 tuiles comme c'est la taille de l'écran de la GameBoy), et la dimension des tuiles (ici 8×8 px). Et après il n'y a plus qu'à agencer les tuiles sur la carte.

Capture d'écran du niveau 1 en cours d'édition dans Tiled

Création de la map du niveau dans Tiled

Une fois arrivé au résultat voulu, il ne reste plus qu'à exporter la map sous forme d'image PNG et on est prêt pour la suite !

Tilemap exportée par Tiled

Tilemap exportée sous forme d'image depuis Tiled

Note

NOTE : On notera que la balle et la raquette sont absentes de l'image ci-dessus. C'est normal : ce que l'on vient d'exporter est ce que j'appèlerai « l'environnement » dans le code et sera affiché sur la couche Background car ce sont des éléments qui resteront statiques. La raquette et la balle sont des éléments mobiles et seront affichées en utilisant des Sprites.

On se sort les doigts du code !

Il est maintenant grand temps de commencer à coder. Plutôt que de me retaper tous les fichiers à créer à la main, j'ai fait mon fainéant et repris l'exemple « Hello World », comme ça j'ai un projet tout prêt avec tout ce qu'il faut pour commencer, notamment les scripts de compilation (Makefile et Make.bat).

J'en profite pour rajouter un dossier assets/ dans lequel je place les images et les fichiers Tiled que j'ai créés.

Aperçu des fichiers du projet

Aperçu des fichiers du projet

Construction des assets

Maintenant qu'on a préparé le terrain, on va importer les assets (le tileset et la tilemap) dans le projet. Rien de bien compliqué, je vais utiliser img2gb, comme je l'avais déjà expliqué dans un précédent article.

Construction des assets du projet

Construction des assets du projet

Petite subtilité toutefois, je génère la tilemap avec un décalage (offset) de 128. Pourquoi, me direz-vous ? Eh bien rappelez-vous de l'organisation de la mémoire vidéo vue dans l'article numéro 4 de la série :

Organisation de la mémoire vidéo

Une partie de la mémoire vidéo est accessible à la fois à la couche Background (et Window) et aux Sprites. Étant donné que j'ai placé toutes mes tuiles dans une même image et que je n'ai pas envie de m'embêter, je vais les charger dans cette zone partagée. Il faudra juste tenir compte du fait que ma première tuile aura le numéro 128 et pas 0.

Chargement des tuiles dans la mémoire vidéo

Une fois les assets converties, il ne reste plus qu'à les charger dans la mémoire vidéo de la console, ce qui se fait assez simplement, en important le fichier src/levels.tileset.h dans le fichier src/main.c et en utilisant la fonction set_bkg_data() comme vu dans l'article « Afficher des images » :

#include <gb/gb.h>

#include "./levels.tileset.h"

void main(void) {
    set_bkg_data(128, LEVELS_TILESET_TILE_COUNT, LEVELS_TILESET);
    //           ^^^  ici on n'oublie pas de mettre l'offset évoqué précédemment ;)
}

Si on lance le jeu à ce stade, il n'y aura rien à voir à l'écran de la GameBoy, mais on peut utiliser le VRAM Viewer de l'émulateur BGB pour s'assurer que tout fonctionne comme prévu :

Capture d'écran du visualisateur de mémoire vidéo de l'émulateur BGB

Affichage de « l'environnement »

Les tuiles sont à présent chargées, on va enfin pouvoir afficher des trucs ! 😁️

Je vais commencer par « l'environnement », c'est-à-dire les briques et la bordure qui délimitent le niveau. Je vais pour ce faire utiliser la couche Background qui est tout à fait adaptée pour ça.

J'importe donc la tilemap que j'avais composée plus tôt, et je la charge à l'aide de la fonction set_bkg_tiles(). Il ne faut bien sûr pas oublier de rendre la couche Background visible, sinon on ne verra rien !

#include <gb/gb.h>

#include "./levels.tileset.h"

void main(void) {
    set_bkg_data(128, LEVELS_TILESET_TILE_COUNT, LEVELS_TILESET);
    set_bkg_tiles(0, 0, LEVEL01_TILEMAP_WIDTH, LEVEL01_TILEMAP_HEIGHT, LEVEL01_TILEMAP);
    SHOW_BKG;
}

Cette fois-ci, si on lance le jeu dans un émulateur, on peut voir apparaitre le niveau à l'écran :

Capture d'écran du jeu

Afficher les sprites

Maintenant que le niveau est affiché à l'écran, je vais m'atteler à afficher les éléments interactifs, à savoir la raquette et la balle. Pour cela je vais avoir besoin de 4 sprites :

  • 1 pour la balle (j'utiliserai le sprite 0),
  • 3 pour raquette, étant donné qu'elle est composée de 3 tuiles (j'utiliserai les sprites 1, 2 et 3)

On peut donc assigner les tuiles aux différents sprites à l'aide du code suivant :

set_sprite_tile(0, 128 + 15);  // balle
set_sprite_tile(1, 128 + 12);  // raquette (partie gauche)
set_sprite_tile(2, 128 + 13);  // raquette (partie centrale)
set_sprite_tile(3, 128 + 14);  // raquette (partie droite)

À ceci, il faut ajouter un peu de code pour positionner les sprites :

set_sprite_tile(0, 128 + 15);  // balle
move_sprite(0, 50, 120);

set_sprite_tile(1, 128 + 12);  // raquette (partie gauche)
move_sprite(1, 76, 152);

set_sprite_tile(2, 128 + 13);  // raquette (partie centrale)
move_sprite(2, 76 + 8, 152);

set_sprite_tile(3, 128 + 14);  // raquette (partie droite)
move_sprite(3, 76 + 16, 152);

Et bien sur, on n'oublie pas de rendre les sprites visibles. Au final on obtient le code suivant :

#include <gb/gb.h>

#include "./levels.tileset.h"
#include "./level01.tilemap.h"

void main(void) {
    set_bkg_data(128, LEVELS_TILESET_TILE_COUNT, LEVELS_TILESET);
    set_bkg_tiles(0, 0, LEVEL01_TILEMAP_WIDTH, LEVEL01_TILEMAP_HEIGHT, LEVEL01_TILEMAP);
    SHOW_BKG;

    // Balle
    set_sprite_tile(0, 128 + 15);
    move_sprite(0, 50, 120);

    // Raquette
    set_sprite_tile(1, 128 + 12);
    move_sprite(1, 76, 152);

    set_sprite_tile(2, 128 + 13);
    move_sprite(2, 76 + 8, 152);

    set_sprite_tile(3, 128 + 14);
    move_sprite(3, 76 + 16, 152);

    SHOW_SPRITES;
}

Pour plus d'explications sur le code ci-dessus, je vous laisse jeter un œil à l'article dédié aux Sprites. 😉️

On peut à présent lancer le jeu et constater que la raquette et la balle sont bien affichées. On peut aussi en profiter pour faire un tour dans l'onglet « OAM » du VRAM Viewer de BGB afin de constater que tout se présente bien comme prévu :

Capture d'écran de l'onglet OAM du VRAM Viewer de l'émulateur BGB

To be continued...

C'est terminé pour cette première partie. On a donc créé et affiché tous les éléments graphiques à l'écran. Pour ceux que ça intéresse, je vous mets ci-dessous le code source en l'état actuel avec la ROM.

Dans la seconde partie de cet article (qui devrait sortir d'ici une semaine ou une semaine et demie), on verra comment gérer le déplacement de la raquette, de la balle et comment implémenter les collisions de la balle avec l'environnement et la raquette.