[Tutoriel] La gestion du clavier en C
Posté le 17/02/2016 14:11
Pour des questions de pratique ou d'habitude, beaucoup d'entre nous utilisent des fonctions comme IsKeyDown() pour gérer leurs entrées claviers. C'est mal, et voici pourquoi.
Le problème
Le principe d'un programme disposant d'une interface graphique est simple : il travaille quand l'utilisateur lui donne du boulot et le reste du temps il attend patiemment de recevoir des ordres (travailleur acharné, je vous dis), et se met donc en pause pour un temps indéterminé. En clair, il dort.
Seulement voilà, les bons jeux en temps réel donnent au programme du travail à faire régulièrement (comme faire bouger les adversaires sur une map ou faire avancer des animations) et cet ordre ne se donne pas au clavier.
Et là, l'erreur de logique apparaît : au lieu de réveiller le programme régulièrement pour lui rappeler qu'il a du travail, on va plutôt l'empêcher de dormir. « De toute façon il fonctionne tout aussi bien donc autant le maltraiter, ça ne fait de mal à personne », me direz-vous.
Sauf que ça fait du mal à la machine, qui d'une part n'apprécie pas de ne jamais pouvoir se reposer (effet similaire à de l'overlock), ce qui use les piles, et la rend peu réceptive aux signaux qu'on lui envoie puisqu'elle est occupée à faire autre chose.
Alors n'en déplaise aux concepteurs de bibliothèques (je pense à
input mais cela n'a rien contre Ninestars), les méthodes de gestion du clavier qui ne mettent pas le programme en pause (puisque finalement le clavier c'est l'interface de l'utilisateur pour donner des ordres) ne sont pas propres.
« On a setFps() » me répondrez-vous, mais setFps() utilise Sleep() (de manière un peu barbare d'ailleurs) et Sleep() est une fonction qui ne prend même pas la peine d'endormir le processeur (désassemblez le syscall 0x420, vous verrez, y'en a pour 20 lignes d'assembleur et c'est une horreur), donc c'est du pareil au même.
La solution
Assez parlé, la méthode correcte est donc, si l'on reprend notre analogie, de laisser dormir notre processeur, en lui programmant tout de même un réveil pour qu'il n'oublie pas de faire son boulot. Ça tombe bien, ce réveil s'appelle un timer et c'est probablement la fonctionnalité la moins utilisée de fxlib.
#include "fxlib.h"
#include "timer.h"
void SetTimer(int timer_id, int elapse, void (*callback)(void));
void KillTimer(int timer_id);
Ces fonctions sont très simples d'utilisation. Le timer_id s'écrit ID_USER_TIMER
X, avec X variant de 1 à 5. Si vous avez la flemme vous pouvez aussi écrire de 1 à 5, ça fonctionne aussi.
La durée s'exprime en millisecondes mais le résultat est arrondi à un multiple de 25 ms (probablement tronqué en fait). Vous parlez donc en millisecondes mais vous indiquez un multiple de 25.
Si vous n'êtes pas familier avec la notation du troisième paramètre, il s'agit d'un pointeur sur une fonction de la forme « void fonction(void) ». En gros c'est la fonction quoi.
Une fois SetTimer() appelée, le timer se lance et chaque fois que la durée indiquée s'écoule, la fonction que vous avez passée en troisième argument est appelée (et ce quoi que le programme soit en train de faire à ce moment-là). Notez que le timer se recharge automatiquement.
Lorsque vous voulez arrêter le timer, vous faites appel à KillTimer(). N'oubliez pas d'arrêter le timer quand vous quittez votre fonction, vous n'avez définitivement pas envie que vos ennemis continuent à bouger sur l'écran alors que vous êtes revenu au menu principal.
Maintenant que vous avez de quoi réveiller votre processeur, vous allez pouvoir lui octroyer du temps de sommeil avec la fameuse fonction GetKey().
int GetKey(unsigned int *key);
GetKey() endort le processeur jusqu'à ce qu'une pression sur une touche le réveille, excepté qu'aujourd'hui vous avez aussi prévu un timer pour le réveiller. L'argument est l'adresse d'une variable dans laquelle le code de la touche sera stocké en suivant la norme de l'en-tête keybios.h, et la valeur de retour n'intéresse jamais personne. Votre processeur retrouve enfin le sommeil, et le sourire.
Un exemple
Voici un exemple simple permettant d'afficher des animations à deux frames d'une seconde. C'est volontairement exhaustif. Il n'y a pas particulièrement de commentaires à faire, je pense que c'est assez simple. Si vous avez une question, laissez un commentaire.
#include "fxlib.h"
#include "timer.h"
static void draw(void);
static void callback(void);
static int animation_state = 0;
int main(void)
{
unsigned int key;
// On dessine une premiere fois le contenu de l'ecran au frame 0
animation_state = 0;
draw();
// On met en place le timer pour une seconde de delai
SetTimer(ID_USER_TIMER1, 1000, callback);
while(1)
{
// On laisse dormir le processeur \o/
GetKey(&key);
if(key == KEY_CTRL_EXIT) break;
}
// On arrete le timer avant de quitter la fonction
KillTimer(ID_USER_TIMER1);
}
static void draw(void)
{
Bdisp_AllClr_VRAM();
if(animation_state == 0)
{
// Dessiner au frame 0
}
else
{
// Dessiner au frame 1
}
Bdisp_PutDisp_DD();
}
static void callback(void)
{
// On passe de 0 a 1, ou de 1 a 0
animation_state = !animation_state;
// Et bien sur on redessine !
draw();
}
Le problème du retour au menu
Vous êtes sans doute déjà fou de joie de ces nouvelles connaissances et vous vous apprêtez probablement à les mettre en œuvre avec un enthousiasme renouvelé, et vous vous rendez soudain compte que GetKey() possède quelques fonctionnalités de plus que ses cousines de la famille de IsKeyDown() :
- La capacité de revenir au menu quand on appuie sur MENU
- La modification du contraste avec SHIFT et REPLAY
Le premier vous intéresse peut-être, mais il y a un souci : en effet, lorsque vous revenez au menu, les timers ne sont pas arrêtés. Donc une seconde après votre retour au menu, votre fonction callback() est appelée de nouveau et redessine la map de votre jeu alors que l'utilisateur se trouve bien dans le menu principal de la calculatrice.
Pour cela, il existe une solution (un workaround, en fait) qui consiste à passer par un peu d'assembleur (c'est du syscall en fait, mais les syscalls C sont très peu pratiques à utiliser) mais qui nécessite de vous convertir aux matrix codes.
Les matrix codes, c'est une autre façon de noter les codes des touches. C'est plus intuitif que la liste classique de KEY_CTRL_EXE, KEY_CTRL_EXIT et tout le reste. Observez le tableau suivant :
F1 F2 F3 F4 F5 F6 09
SHIFT OPTN VARS MENU ← ↑ 08
ALPHA ^2 ^ EXIT ↓ → 07
XTT log ln sin cos tan 06
ab/c F↔D ( ) , → 05
7 8 9 DEL 04
4 5 6 x div 03
1 2 3 + - 02
0 . EXP (-) EXE 01
AC 00
06 05 04 03 02 01 00
Vous voulez le code de la touche MENU ? Aucun problème, repérez la colonne... 3, et la ligne... 8. Vous ajoutez 1 (oui, désolé), ça fait 4/9, et vous avez tout de suite le code : 0x0409. EXE est en position 2/1, son code sera donc 0x0302. ALPHA donnera 0x0708. Facile, non ? Libre à vous de refaire des macros ensuite, par exemple :
#define KEY_UP 0x0209
#define KEY_RIGHT 0x0208
#define KEY_DOWN 0x0308
#define KEY_LEFT 0x0309
#define KEY_SHIFT 0x0709
#define KEY_ALPHA 0x0708
#define KEY_EXE 0x0302
#define KEY_EXIT 0x0408
#define KEY_MENU 0x0409
Pour régler le problème du menu, il vous suffit (si vous travaillez avec le fx-9860G SDK, les linuxiens trouveront tous seuls) d'inclure le fichier getkey.src, en fichier joint au tutoriel, à votre projet, et d'utiliser la fonction getkey() qui s'y trouve au lieu de GetKey(). N'oubliez pas de modifier l'extension pour un .src, le site n'accepte pas ce format.
unsigned int key = getkey();
Et là, miracle, la fonction ne vous renvoie plus au menu lorsque vous appuyez sur la touche mais vous renvoie le code de KEY_MENU défini plus haut (0x0409). Notez que cette fonction renvoie systématiquement les matrix codes, donc la touche EXE ne donne plus KEY_CTRL_EXE (30001) mais KEY_EXE (0x0302).
Et alors vous pouvez stopper vous-mêmes vos timers, lancer le menu principal en utilisant la deuxième fonction fournie dans le fichier, et redémarrer vos timers dès que l'utilisateur retourne dans votre application.
key = getkey();
if(key == KEY_MENU)
{
KillTimer(ID_USER_TIMER1);
system_menu();
draw();
SetTimer(ID_USER_TIMER1, 1000, callback);
}
Notez qu'il est intéressant de redessiner le contenu de votre écran pour éviter à l'utilisateur de contempler le menu principal encore une seconde avant que votre timer n'arrive à expiration et ne le fasse lui-même.
Je n'ai pas encore de solution viable pour le problème du contraste (SHIFT + REPLAY), n'oubliez pas lorsque vous testez sur émulateur qu'après SHIFT les directions gauche et droite ne répondent plus, ce n'est pas un bug de votre programme.
En conclusion
Si vous avez lu jusqu'ici, vous n'avez plus le droit d'utiliser IsKeyDown(). Rappelez-vous, il n'y a quasiment aucune circonstance atténuante qui justifie que vous maltraitiez votre processeur.
Je terminerai sur une petite citation de Kirafi :
Kirafi a écrit :Bon c'est bien beau tout ça
, mais maintenant faut balancer le tuto sur le Combo Getkey/Timer pour que Phénix arrête de vomir à chaque fois que quelqu'un sort un Add-In
...
Je compte sur vous...
Fichier joint
Citer : Posté le 31/10/2020 09:49 | #
Et que risque-t-il de se passer si, dans la fonction qu'exécute le timer, on KillTimer ?
Exemple :
void verif(void)
{
if(a==0) KillTimer(1);
affiche(map);
}
Là, il se passe quoi si a vaut 0 ?
→ Ça beugue
→ Ça stoppe le timer normalement et la map ne se dessine pas
→ Ça stoppe le timer mais la fonction se termine et dessine la map
Citer : Posté le 31/10/2020 09:54 | #
→ Ça stoppe le timer mais la fonction se termine et dessine la map
À confirmer, mais c'est le plus logique
Citer : Posté le 31/10/2020 09:54 | #
Troisième option. Le code en cours d'exécution ne va pas s'arrêter comme ça.
Citer : Posté le 31/10/2020 09:57 | #
Super !
Merci !
Ajouté le 13/02/2021 à 10:25 :
Toute petite modification pour atteindre la perfection : "vous avec aussi prévu un timer pour le réveiller", à peu près au milieu
Citer : Posté le 13/02/2021 10:35 | #
C'est fait merci, j'ai corrigé deux autres fautes au passage.
Citer : Posté le 13/02/2021 10:38 | #
De rien