Développement GameBoy #2 : Utiliser le gamepad

Après un premier article expliquant comment écrire, compiler et exécuter un programme pour la GameBoy, je reviens avec ce second article qui traitera de l'interaction avec le joueur à travers le gamepad de la console.

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 dispose de 8 boutons pouvant être utilisés par notre programme pour recevoir des entrées de l'utilisateur :

  • Un D-PAD (ou croix directionnelle dans la langue de Francis Cabrel) composé de 4 boutons haut, bas, gauche, droite. Il sert la plupart du temps à se déplacer (déplacer le personnage ou un objet), et à naviguer dans les menus.
  • Un bouton A, plutôt utilisé pour les actions primaires (sauter, accélérer, valider dans un menu).
  • Un bouton B, souvent attribué aux actions secondaires (tirer, freiner, annuler / retourner en arrière dans un menu).
  • Un bouton START, généralement utilisé pour mettre le jeu en pause ou pour afficher un menu.
  • Un bouton SELECT, qui sert, quand il est utilisé, à effectuer des actions peu courantes, car il est peu accessible dans le feu de l'action.
GameBoy Gamepad

Pour utiliser le gamepad, GBDK nous fournit 3 fonctions via la bibliothèque <gb/gb.h> :

  • waitpad
  • waitpadup
  • joypad

ainsi qu'un masque pour chaque bouton du gamepad :

  • J_UP : bouton haut du D-Pad,
  • J_DOWN : bouton bas du D-Pad,
  • J_LEFT : bouton gauche du D-Pad,
  • J_RIGHT : bouton droite du D-Pad,
  • J_A : bouton A,
  • J_B : bouton B,
  • J_START : bouton START,
  • J_SELECT : bouton SELECT.

Petit aparté sur les types

Avant d'aller plus loin sur l'utilisation du GamePad, on va faire un petit point sur les types de données. Les développeurs de GBDK recommandent l'utilisation des types personnalisés déclarés dans la bibliothèque <types.h> à la place des types natifs, car ils sont plus explicites quant à la taille des données qu'ils peuvent contenir (on oublie vite que sur une GameBoy, un int a une taille de seulement 8 bits).

Je vous donne ci-dessous un tableau faisant la correspondance entre les types recommandés, les types natifs équivalents et les valeurs qu'ils peuvent contenir.

Type recommandé Types natifs correspondants Taille Valeur minimale Valeur maximale
INT8 char, int 8 bits (1 octet) -128 127
UINT8 unsigned char, unsigned int 8 bits (1 octet) 0 255
INT16 long 16 bits (2 octets) -32 768 32 767
UINT16 unsigned long 16 bits (2 octets) 0 65 535
INT32 long long 32 bits (4 octets) -2 147 483 648 2 147 483 647
UINT32 unsigned long long 32 bits (4 octets) 0 4 294 967 296
  float 32 bits (4 octets)    
  double 32 bits (4 octets)    
  pointer 16 bits (2 octets)    

Je profite également de cette section pour ajouter quelques recommandations liées à l'utilisation des types :

  • La GameBoy possédant un processeur 8 bits, utilisez autant que possible des nombres 8 bits (INT8, UINT8), les calculs seront beaucoup plus rapides.
  • Il est également préférable d'utiliser des types non-signés (UINT8, UINT16, UINT32), c'est plus efficace lors des comparaisons entre deux valeurs.

Pour ceux que cela intéresse, vous trouverez plus de recommandations dans la documentation de GBDK (en anglais).

La fonction waitpad()

La fonction waitpad() bloque l'exécution du programme jusqu'à ce que la (ou les) touche(s) demandée(s) soit pressée. Elle prend en paramètre le masque des touches qui nous intéressent et retourne la ou les touches effectivement appuyées.

UINT8 waitpad(UINT8 mask)

Dans l'exemple ci-dessous, on attend que le joueur presse la touche START, on passe donc le masque J_START en paramètre de la fonction :

#include <stdio.h>
#include <gb/gb.h>

void main(void) {
    printf("Press START!\n");
    waitpad(J_START);
    printf("> Good!");
}

Si on veut autoriser le joueur à presser plusieurs touches, il faut alors fusionner les masques des touches souhaitées via l'opération binaire « ou » (|). Par exemple pour autoriser l'appui sur les touches A et B, on peut écrire le code suivant :

#include <stdio.h>
#include <gb/gb.h>

void main(void) {
    printf("Press A or B!\n");
    waitpad(J_A | J_B);
    printf("> Good!");
}

Note

NOTE : On est évidemment pas limité à seulement deux touches, on peut en écouter plus, il suffit de fusionner autant de masques de touche que nécessaire (par exemple J_A | J_B | J_START | J_SELECT).

Le problème quand on écoute plusieurs touches, c'est qu'on ne peut pas savoir immédiatement laquelle a été pressée, puisqu'on passe à la suite du programme dès que l'une (ou plusieurs) des touches a été pressée. Heureusement, la fonction waitpad() nous retourne quelles touches ont été pressées.

Ce retour prend la forme d'un nombre représentant l'ensemble des touches pressées. S'il n'était possible de presser qu'une seule touche à la fois, on pourrait comparer directement ce nombre au masque de la touche (touche == J_START par exemple). Le problème, c'est que dans la pratique, l'utilisateur peut appuyer simultanément sur autant de touches qu'il le veut, ce qui nous empêche de simplement effectuer cette comparaison.

Pour savoir si une touche a effectivement été pressée, il nous faut appliquer son masque sur la valeur retournée par waitpad() via l'opérateur binaire « et » (&), et en fonction du résultat, on saura si oui ou non la touche a été appuyée.

Par exemple, si on désire savoir si la touche START a été pressée, il faut appliquer le masque J_START au nombre retourné par waitpad(). Si le résultat de l'opération touche & J_START est différent de 0, c'est que la touche a été pressée. Si au contraire le résultat est égal à 0, le bouton n'a pas été pressé.

Voici un petit exemple pour clarifier tout ça :

#include <stdio.h>
#include <gb/gb.h>

void main(void) {
    UINT8 key;
    printf("Press LEFT or RIGHT!\n");
    key = waitpad(J_LEFT | J_RIGHT);
    if (key & J_LEFT) {
        printf("> You pressed LEFT");
    } else if (key & J_RIGHT) {
        printf("> You pressed RIGHT");
    }
}

La fonction waitpadup()

La fonction waitpadup() bloque le programme jusqu'à ce que toutes les touches du gamepad aient été relâchées.

void waitpadup(void)

Dit comme ça, cette fonction peut sembler un peu inutile, mais elle est pourtant très importante lorsque l'on utilise waitpad().

En effet, si vous appelez deux fois la fonction waitpad() à la suite, il y a de fortes chances que les deux « passent » d'un coup (il n'y aura pas de pause entre les deux appels).

Cela s'explique par le fait que la fonction waitpad() redonne immédiatement la main au programme si le bouton demandé était déjà pressé lorsqu'elle a été appelée... ce qui arrivera si le joueur n'a pas eu le temps d'enlever son gros doigt du bouton entre les deux appels (c'est pas très rapide un humain comparé à un microprocesseur).

Le programme suivant illustre le problème exposé ci-dessus et montre comment utiliser waitpadup() pour le résoudre :

#include <stdio.h>
#include <gb/gb.h>

void main(void) {
     // Ici, un seul appui sur "A" passera les deux appels à waitpad()
     printf("Press A!\n");
     waitpad(J_A);
     printf("> 1st\n");
     waitpad(J_A);
     printf("> 2nd\n");

     // Attends que tous les boutons soient relachés
     waitpadup();

     printf("Press A one more time!\n");
     waitpad(J_A);
     printf("> 3rd");
}

La fonction joypad()

Contrairement aux deux précédentes fonctions, joypad() ne bloque pas le programme en attendant que le joueur appuie sur une touche : elle retourne simplement les boutons qui sont actuellement pressés et rend immédiatement la main.

UINT8 joypad(void)

L'exemple ci-dessous contient une boucle infinie qui lit continuellement les touches et affiche en temps réel toutes celles qui sont pressées :

#include <stdio.h>
#include <gb/gb.h>

void main(void) {
    UINT8 prev_keys = 0;
    UINT8 keys = 0;

    printf("Press what you want\n\n");

    // Boucle infinie pour lire les touches en continu
    while (1) {
        // On lit les touches actuellement appuyées
        keys = joypad();

        // Si rien n'a changé par rapport au tour de boucle précédent, on
        // passe directement au tour de boucle suivant (on évite d'afficher
        // la même chose en boucle, on affiche un message seulement lorsque
        // quelque chose change)
        if (keys == prev_keys) {
            continue;
        }

        // On affiche toutes les touches actuellement pressées...
        if (keys > 0) {
            if (keys & J_UP) printf("UP ");
            if (keys & J_DOWN) printf("DOWN ");
            if (keys & J_LEFT) printf("LEFT ");
            if (keys & J_RIGHT) printf("RIGHT ");
            if (keys & J_SELECT) printf("SELECT ");
            if (keys & J_START) printf("START ");
            if (keys & J_A) printf("A ");
            if (keys & J_B) printf("B ");
            printf("\n");

        // ... ou "-" si aucune touche n'est pressée.
        } else {
            printf("-\n");
        }

        // On mémorise les touches appuyées
        prev_keys = keys;
    }
}

Quand utiliser quelle fonction ?

La question que l'on pourrait se poser, c'est dans quel cas il convient d'utiliser le couple waitpad() / waitpadup() et dans quel cas il est préférable de faire appel à joypad(). La réponse à cette question est assez simple :

Les fonctions waitpad() et waitpadup() sont très pratiques à utiliser lorsque l'interactivité est limitée, par exemple sur un start screen, dans un menu ou dans un puzzle game très simple. Elles sont par contre inutilisables dans un jeu de plateforme ou même dans un jeu comme Tetris : étant donné que ces fonctions bloquent l'exécution du programme, il n'est pas possible de faire quoi que ce soit d'autre pendant ce temps (comme faire descendre le tétromino dans le cas de Tetris).

La fonction joypad() est quant à elle toute indiquée pour être utilisée dans la boucle principale d'un jeu dès lors qu'il doit se passer autre chose qu'uniquement attendre des actions de l'utilisateur.

Programme récapitulatif

J'ai publié sur Github un projet qui regroupe les exemples de l'article. Vous le trouverez à l'adresse suivante :

GameBoy Gamepad program

C'est tout pour cette fois-ci, je vous retrouve dans 15 jours avec un nouvel article qui montrera concrètement comment créer un jeu avec ce que nous venons juste d'apprendre. En attendant, n'hésitez pas à m'envoyer un message sur Twitter, sur Github, ou via le formulaire de contact du site si vous avez la moindre remarque ou question !