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 :
- 1 —
Hello World(obsolète) 1bis — re-Hello World
2 — Utiliser le gamepad (+ aparté sur les types de variable)
3 — Projet 1 - Tic Tac Toe (+ présentation du mode texte et de "gb/console.h")
4 — Afficher des images (création et affichage de tilemaps)
5 — Créer des tilesets (format d'image de la GameBoy et conversion avec img2gb)
7 — Les sprites
9 — Les palettes
10 — Projet 2 - Breakout • PARTIE 1 • PARTIE 2 • PARTIE 3
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.
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 :
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 !