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

Après une première partie dédiée à la création et à l'affichage des éléments graphiques, et une seconde partie dédiée aux déplacements et aux collisions, on va enfin s'attaquer au cœur du gameplay : casser des briques !

Je vais donc vous montrer cette fois-ci comment casser des briques... mais également comment rendre le jeu vraiment jouable, en permettant au joueur de perdre s'il rate la balle ou de gagner s'il casse toutes les briques. Je vais également vous donner quelques pistes sur les améliorations qu'il faudrait apporter pour que le jeu soit réellement fun à jouer.

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 :

Casser des briques, enfin !

On va enfin pouvoir casser des briques... c'est cool... mais en fait vous allez vite être déçus, car en fait, c'est extrêmement simple : lorsque l'on vérifie si la balle rentre en collision avec l'environnement, il suffit de regarder au passage s'il ne s'agirait pas d'une tuile représentant une brique, et si c'est le cas, on la supprime. Bon il y a quand même une petite subtilité : les briques sont composées de deux tuiles, donc on se retrouve dans deux cas :

  • si on tape dans la partie gauche de la brique, il faut supprimer cette tuile, et celle immédiatement à sa droite,
  • et si on tape dans la partie droite de la brique, dans ce cas il faut la supprimer avec la tuile immédiatement à sa gauche.

Eh oui, il n'y a pas plus de difficulté que ça. Je vais donc créer une petite fonction qui supprime une brique si on lui donne les coordonnées de la tuile la plus à gauche :

#define TILE_EMPTY      128

void remove_brick(UINT8 x, UINT8 y) {
    UINT8 cells[2] = {TILE_EMPTY, TILE_EMPTY};
    set_bkg_tiles(x, y, 2, 1, cells);
}

et je vais ajouter le code suivant dans la fonction check_ball_collide() :

#define TILE_BRICK_L   136
#define TILE_BRICK_R   137

UINT8 check_ball_collide(INT8 delta_x, INT8 delta_y) {

    // ...

    // remove bricks
    switch (next_cell[0]) {
        case TILE_BRICK_L:
            remove_brick(ball_next_cell_x, ball_next_cell_y);
            break;
        case TILE_BRICK_R:
            remove_brick(ball_next_cell_x - 1, ball_next_cell_y);
            break;
    }

    // ...

}

Note

NOTE : ce n'est pas forcément très « propre » (d'un point de vue organisation du code) de supprimer les briques au milieu du code d'une fonction censée seulement vérifier les collisions, mais le but ici est d'arriver rapidement à un résultat. Il faudra améliorer ça plus tard si on dépasse le stade du PoC. 😉️

Si on exécute le programme, cette fois-ci ça ressemble vraiment à un casse-briques parfaitement jouable !

Capture vidéo du cassage de brique

Game Over

Un jeu où on ne peut pas perdre n'est pas amusant, je vais donc implémenter le game over pas plus tard que tout de suite 😛️. Pour perdre la partie, la condition est simple : il suffit que la balle touche le bas de l'écran. On peut donc écrire la fonction suivante :

UINT8 check_gameover() {
    return BALL_Y + BALL_WIDTH >= 18 * 8 + 16;
    // 18 * 8 -> hauteur de l'écran en pixels
    // 16 -> offset de la position des sprites sur l'axe y
}

Et maintenant, je n'ai plus qu'un petit test à rajouter dans ma boucle principale :

void main() {

    // ...

    while (1) {
        // ...

        if (check_gameover()) {
              break;
        }

        // ...
    }
}

Bon normalement il faudrait afficher un joli écran de game over et tout, mais pour le moment on va faire au plus simple, sinon cet article va finir par être beaucoup trop long. On va donc pour le moment simplement couper la boucle, ce qui aura pour effet de terminer le jeu (par contre il faudra redémarrer la console pour rejouer).

Je ne vous mets pas de GIF cette fois-ci, parce que visuellement la fin de la partie se matérialise juste par un écran figé.

Gagner la partie

Et maintenant qu'on peut perdre, il faudrait aussi pouvoir gagner... Ça ne devrait pas être trop dur à faire : il suffit de compter le nombre de briques cassées pour savoir quand le joueur a terminé le niveau.

Je vais donc créer une variable globale qui va stocker le nombre de briques qu'il reste :

UINT8 REMAINING_BRICKS = 39;  // il y a 39 briques dans le level01

puis je modifie la fonction remove_brick() pour qu'elle décompte les briques cassées :

void remove_brick(UINT8 x, UINT8 y) {
    // ...
    REMAINING_BRICKS -= 1;
}

et enfin, dans la fonction main(), je remplace la boucle infinie par une boucle qui tourne tant qu'il reste des briques :

void main() {

    // ...

    while (REMAINING_BRICKS) {
        // ...
    }
}

Et voilà, le jeu s'arrêtera de lui-même lorsqu'il ne restera plus de brique à casser !

Comme pour la section précédente, je ne vous mets pas de GIF ici non plus, car cette fois encore, le jeu va simplement se figer lorsque la partie sera terminée.

Il reste tant à faire...

On est à présent en possession d'un prototype de casse-briques qui fonctionne... et je vais m'arrêter là pour cet article car ça va commencer à devenir trop long sinon ! Mais pour bien faire les choses, il y aurait encore pas mal de boulot.

Déjà il faudrait commencer par afficher un écran de félicitations lorsque le joueur gagne et un écran de game over lorsqu'il perd. Et tant qu'on y est, on pourrait rajouter un écran de titre. Il faudrait bien sûr également permettre au joueur de relancer une partie sans redémarrer la console.

Enfin, il faudrait améliorer le gameplay :

  • Actuellement la balle se déplace toujours selon le même angle... il faudrait faire varier cet angle lorsque le joueur tape la balle tout en bougeant la raquette ou s'il rattrape la balle avec l'extrémité de la raquette (sinon toutes les parties se déroulent exactement de la même manière).
  • On pourrait aussi laisser le joueur démarrer là où il le souhaite, en collant la balle à la raquette en début de partie et en lançant la balle lorsqu'il presse le bouton A.
  • Idéalement, on devrait également ajouter un système de vies pour ne pas tout perdre en une seule balle.
  • On pourrait aussi ajouter des briques différentes (qui ne se cassent pas en un seul coup par exemple), et pourquoi pas, ajouter des bonus à rattraper.
  • Et enfin, il faudrait évidemment ajouter plein de niveaux... sinon ça va être très répétitif !

Je vous laisse donc le soin de poursuivre le développement du jeu, si vous en avez l'envie bien sûr 😉️. Et si vous le faites, n'hésitez pas à poster un lien vers votre projet dans les commentaires ou à m'envoyer un mail ! Qui sait, si je reçois quelques propositions sympas je pourrais peut-être vous les présenter ici-même ! 😁️

Note

Comme à chaque fin d'article de cette série, vous trouverez les sources complètes du projet sur Github :

À bientôt pour de nouvelles aventures vidéo-ludiques ! 😃️