Maintenant que nous avons joué un peu (beaucoup) avec l’affichage, il est temps de voir comment faire en sorte que notre programme réagisse aux actions du joueur comme l’appui sur une touche du clavier, le déplacement de la souris, etc.
Gérer les évènements
Une file d’évènements
Nous appelons évènement toute action extérieure à notre programme et qui peut avoir un effet sur lui. L’appui sur une touche du clavier, le déplacement de la souris, le redimensionnement d’une fenêtre, et même une demande de fermeture du programme sont des évènements.
Durant toute la durée de vie de notre application, elle reçoit de nombreux évènements. Ceux-ci sont placés dans une file et attendent d’être traités par notre programme. Nous pouvons ensuite interroger la SDL pour savoir s’il y a eu un évènement. À ce moment, elle nous donne des informations sur l’évènement qui n’a pas été traité. Cela signifie notamment qu’il faudra réinterroger la SDL à chaque fois pour savoir s’il y a eu un nouvel évènement.
Bien sûr, il peut y avoir plusieurs évènements en attente. Dans ce cas, c’est l’évènement le plus vieux qui sera lu, conformément au système de file : le premier arrivé est également le premier à être traité. On peut donc voir cela comme n’importe quelle file d’attente dans la vie courante (avec les tricheurs en moins). On peut alors imaginer quelque chose de ce genre (ici, on suppose qu’on a prévu de fermer la fenêtre quand il y a une demande de fermeture du programme) :
- début de notre programme ;
- appui sur la touche A,
file = [appui A]
; - déplacement de la souris,
file = [appui A, déplacement souris]
; - lecture de la file, on apprend qu’il y a eu appui sur A,
file = [déplacement souris]
; - clic droit,
file = [déplacement souris, clic droit]
; - fermeture fenêtre,
file = [déplacement souris, clic droit, fermeture fenêtre]
; - lecture de la file,
file = [clic droit, fermeture fenêtre]
; - déplacement souris,
file = [clic droit, fermeture fenêtre, déplacement souris]
; - lecture de la file,
file = [fermeture fenêtre, déplacement souris]
; - clic gauche,
file = [fermeture fenêtre, déplacement souris, clic gauche]
; - lecture de la file,
file = [déplacement souris, clic gauche]
; - on a lu un évènement « fermeture fenêtre », on ferme le programme.
Ce petit exemple nous apprend beaucoup de choses à propos de la gestion des évènements que nous aurons à adopter. Lire un évènement à la fois est une très mauvaise idée, car pendant qu’on en lit un plusieurs peuvent être en train de se produire. Cela signifie qu’il faudra soit s’arranger pour lire un évènement dès qu’il survient, soit les lire tous quand on décide de lire.
La structure SDL_Event
La structure SDL_Event
est la structure primordiale pour la gestion des évènements. Lorsque nous demandons à la SDL si un évènement a eu lieu, elle remplit cette structure avec les données de l’évènement correspondant (s’il y en a un). Elle contient alors toutes les informations sur le type d’évènements qui a eu lieu (par exemple, quelle touche a été pressée, à quelle position la souris a été déplacée, etc.) et nous est donc essentielle.
La structure SDL_Event
possède un champ type
qui prend pour valeur le type d’évènement lu, et ses autres champs (un par type d’évènement) sont des structures qui contiennent des informations précises sur l’évènement qui a eu lieu. Les valeurs pouvant être prises par ce champ sont celles de l’énumération SDL_EventType
. Voici un tableau récapitulant les données les plus importantes.
Type d’évènements | Valeur du champ | Champ de | Description |
---|---|---|---|
Évènements de l’application |
|
| Demande de fermeture du programme |
Évènements de la fenêtre |
|
| Changement d’état de la fenêtre |
|
| Évènement dépendant du sytème | |
Évènements du clavier |
|
| Une touche est pressé |
|
| Une touche est relâchée | |
|
| Édition de texte | |
|
| Saisie de texte | |
Évènements de la souris |
|
| Déplacement de la souris |
|
| Une touche de la souris est pressée | |
|
| Une touche de la souris est relâchée | |
|
| La molette est utilisée |
Il y a quelques autres types d’évènements, comme les évènements relatifs aux mobiles (Android et iOS), ou encore ceux relatifs aux écrans tactiles ou aux manettes et autres contrôleurs.
Récupérer les évènements
Pour lire un évènement de la file, la SDL nous propose plusieurs fonctions. Chacune récupère l’évènement le plus ancien, mais elles ont un comportement différent.
SDL_WaitEvent
La première fonction que nous verrons est la fonction SDL_WaitEvent
. Son prototype est le suivant.
int SDL_WaitEvent(SDL_Event* event)
Elle prend en paramètre un pointeur sur un SDL_Event
qu’elle remplira avec les informations sur l’évènement. Elle retourne 0 en cas d’erreur et 1 en cas de succès (ce qui est contraire à beaucoup de fonctions de la SDL).
La fonction SDL_WaitEvent
est une fonction bloquante. Quand on l’utilise, notre programme reste bloqué au niveau de cette fonction jusqu’à ce qu’un évènement ait lieu. Cela signifie que s’il n’y a aucun évènement dans la file, la fonction va attendre jusqu’à ce qu’un évènement arrive. Et dès qu’un évènement est arrivé, la fonction remplit le SDL_Event
et termine. Faisons un code qui attend une demande de fermeture de la fenêtre pour quitter le programme.
SDL_Event event;
SDL_bool quit = SDL_FALSE;
while(!quit)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
}
/* On ferme notre fenêtre et on quitte le programme */
SDL_PollEvent
La fonction SDL_PollEvent
est l’équivalent non bloquant de SDL_WaitEvent
. Cela signifie que s’il n’y a aucun évènement dans la file, le programme va juste continuer sa route.
int SDL_PollEvent(SDL_Event* event)
Tout comme SDL_WaitEvent
, elle prend en paramètre un pointeur sur un SDL_Event
qu’elle remplira avec les informations sur l’évènement. Cependant, sa valeur de retour n’est pas liée à la présence d’erreur, mais à la présence d’évènements. Si elle a lu un évènement (donc si la file n’était pas vide), elle retourne 1, sinon elle retourne 0.
Nous avons précédemment dit qu’« il faudra soit s’arranger pour lire un évènement dès qu’il survient, soit les lire tous quand on décide de lire ». La fonction SDL_WaitEvent
correspond vraisemblablement au premier cas, la fonction SDL_PollEvent
fait plus partie du second cas. Pour lire tous les évènements dans la file, nous allons appeler SDL_PollEvent
tant que la file n’est pas vide, c’est-à-dire tant que le retour de SDL_PollEvent
est différent de 0. Faisons avec SDL_PollEvent
le même exemple que nous avons fait avec SDL_WaitEvent
.
SDL_Event event;
SDL_bool quit = SDL_FALSE;
while(!quit)
{
while(SDL_PollEvent(&event))
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
}
Nous avons juste placé le SDL_PollEvent
dans une boucle. Le problème de cette méthode est que la boucle tourne indéfiniment tant qu’il n’y a pas d’évènements rencontrés, ce qui n’est pas souhaitable niveau performance. Pour régler ce problème, nous allons effectuer une petite pause à chaque tour de boucle, à l’aide de SDL_Delay
, ce qui nous mène au code suivant.
SDL_Event event;
SDL_bool quit = SDL_FALSE;
while(!quit)
{
while(SDL_PollEvent(&event))
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
SDL_Delay(20);
}
Maintenant, un petit exercice : simulons le comportement de la fonction SDL_WaitEvent
en utilisant la fonction SDL_PollEvent
.
Le principe est assez simple, il suffit de rappeler SDL_PollEvent
tant qu’il n’y a pas d’évènements, c’est-à-dire tant que la file est vide et donc tant que la valeur retournée est 0. Dans notre fonction, nous n’aurons alors que cette ligne (et on peut y rajouter une petite pause).
while(!SDL_PollEvent(event))
SDL_Delay(20);
Ici, la seule chose que l’on pourrait nous reprocher est de ne pas gérer les erreurs. De plus, la fonction que nous codons ici n’est pas tout à fait similaire à SDL_WaitEvent
car cette dernière implique que le programme ne soit plus exécuté tant qu’un évènement n’est pas rencontré.
SDL_WaitEventTimeout
Finalement, la SDL propose une dernière fonction, la fonction SDL_WaitEventTimeout
.
int SDL_WaitEventTimeout(SDL_Event* event,
int timeout)
Elle prend en paramètre un pointeur sur SDL_Event
, mais aussi un entier. Cet entier correspond à un nombre de millisecondes. La fonction SDL_WaitEventTimeout
attend un évènement pendant ce nombre de millisecondes. S’il y a un évènement pendant le délai imparti, elle retourne 1, sinon, s’il n’y en a pas eu ou s’il y a eu une erreur, elle retourne 0.
Comment est remplie la file d’évènements ?
La file d’évènements ne se remplit pas toute seule. À chaque appel de SDL_WaitEvent
, de SDL_WaitEventTimeOut
et de SDL_PollEvent
, la fonction SDL_PumpEvents
est appelée.
void SDL_PumpEvents(void)
Elle ne prend aucun argument et ne renvoie rien. Elle se contente d’analyser les périphériques afin de récupérer les nouveaux évènements et de les mettre dans la file.
Nous n’avons pas à l’appeler explicitement puisque les fonctions SDL_WaitEvent
, SDL_PollEVent
et SDL_WaitEventTimeout
y font appel. Cependant, nous verrons plus tard comment elle peut être utilisée.
Comment bien gérer ses évènements
Bien gérer ses évènements n’est pas vraiment compliqué. Il suffit de suivre quelques règles simples. Pour commencer, regardons cette citation de la documentation de SDL_PumpEvents
.
WARNING: This should only be run in the thread that initialized the video subsystem, and for extra safety, you should consider only doing those things on the main thread in any case.
SDL_PumpEvents
doit être appelée uniquement dans le thread initiateur du mode vidéo et il est même conseillé pour plus de sécurité de l’appeler uniquement dans le thread principal.
De plus, il est conseillé de gérer ses évènements (donc de faire appel à SDL_WaitEvent
par exemple) à un seul endroit du code.
/* Bon code */
while(continuer)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
continuer = SDL_FALSE;
if(event.type == SDL_KEYDOWN)
printf("appui sur une touche");
}
/* Mauvais code */
while(continuer)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
continuer = SDL_FALSE;
SDL_WaitEvent(&event);
if(event.type == SDL_KEYDOWN)
printf("appui sur une touche");
}
Appeler une fonction de gestion des évènements une seule fois dans la boucle suffit. En fait, notre programme sera généralement de cette forme.
Boucle principale du programme
Gestion_Évènements
Analyse_Évènements
Actions
Fin Boucle
Quitter
Finalement, pour bien gérer ses évènements, il faut bien choisir la fonction à utiliser. D’un côté, nous avons la fonction bloquante SDL_WaitEvent
, d’un autre côté nous avons la fonction non bloquante SDL_PollEvent
et au milieu des deux nous avons l’hybride SDL_WaitEventTimeOut
.
Nous allons privilégier SDL_PollEvent
qui n’est pas bloquante et utiliser SDL_WaitEvent
dans le seul cas où, sans intervention de l’utilisateur, il n’y rien à faire. SDL_WaitEventTimeout
est un peu plus rarement utilisée.
Analyser les évènements
La fenêtre
Lorsque la SDL détecte un évènement de type SDL_WINDOWEVENT
, nous pouvons récupérer les informations sur l’évènement dans le champ window
du SDL_Event
correspondant. Il s’agit d’une structure, SDL_WindowEvent
. Ses différents champs sont les suivants.
Uint32 type;
Uint32 timestamp;
Uint32 windowID;
Uint8 event;
Sint32 data1;
Sint32 data2;
Ce qui nous intéresse le plus est son champ event
qui correspond au type d’évènement détecté. Il peut avoir plusieurs valeurs qui correspondent aux valeurs de l’énumération SDL_WindowEventID
. On a alors par exemple ces différentes valeurs possibles :
SDL_WINDOWEVENT_SHOWN
si la fenêtre a été rendue visible ;SDL_WINDOWEVENT_MOVED
si la fenêtre a été déplacée ;SDL_WINDOWEVENT_RESIZED
si la fenêtre a été redimensionnée ;SDL_WINDOWEVENT_MINIMIZED
si la fenêtre a été minimisée ;SDL_WINDOWEVENT_RESTORED
si la fenêtre a été restaurée ;SDL_WINDOWEVENT_ENTER
si le focus de la souris est sur la fenêtre ;SDL_WINDOWEVENT_LEAVE
si le focus de la souris n’est plus sur la fenêtre ;SDL_WINDOWEVENT_CLOSE
si le gestionnaire de fenêtre demande la fermeture de la fenêtre.
Et d’autres que nous pouvons voir sur la page de documentation de SDL_WindowEventID
. Par exemple, on peut savoir si la fenêtre a été redimensionnée avec ce code.
while(!quit)
{
SDL_WaitEvent(&event)
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
else if(event.type == SDL_WINDOWEVENT)
if(event.window.event == SDL_WINDOWEVENT_RESIZED)
printf("Fenêtre redimensionnée\n"); /* Fenêtre redimensionnée */
SDL_Delay(20);
}
Le champ windowID
est utile dans le cas où l’on a plusieurs fenêtres et nous permet de savoir par quelle fenêtre l’évènement a été reçu.
Nous pouvons comparer sa valeur à celle retournée par la fonction SDL_GetWindowID
qui prend en paramètre une fenêtre et retourne son identifiant. Ainsi, si event.window.windowID == SDL_GetWIndowID(window1)
, alors c’est la fenêtre window1
qui a été redimensionnée par exemple.
Nous pouvons également agir autrement en utilisant la fonction SDL_GetWindowFromID
qui prend en paramètre un identifiant et renvoie la fenêtre qui a cet identifiant. Ainsi, SDL_GetWindowFromID(event.window.windowID)
renvoie un pointeur sur SDL_Window
qui correspond à la fenêtre ayant reçu l’évènement.
Les champs data1
et data2
sont utiles dans le cas d’un redimensionnement ou d’un déplacement. Dans le cas du redimensionnement, data1
et data2
valent respectivement la nouvelle largeur et la nouvelle hauteur de la fenêtre. Dans le cas du déplacement, data1
et data2
correspondent aux nouvelles positions x
et y
de la fenêtre.
Le champ type
contient le type d’évènements et vaut donc SDL_WINDOWEVENT
.
Pourquoi un évènement SDL_WINDOWEVENT_CLOSE
alors qu’il y a déjà l’évènement SDL_QUIT
?
Cette question est légitime, mais il ne faut pas oublier que nous pouvons ouvrir plusieurs fenêtres, la fermeture d’une fenêtre ne signifie donc pas forcément que l’on quitte le programme. Quand plusieurs fenêtres sont utilisées, cliquer sur la croix de l’une d’entre elles n’entraînera pas l’évènement SDL_QUIT
.
Le clavier
Lorsque c’est un évènement de type SDL_KEYDOWN
ou SDL_KEYUP
, les informations sur l’évènement sont dans le champ key
de SDL_EVENT
. Il s’agit cette fois de la structure SDL_KeyboardEvent
dont les champs sont les suivants.
Uint32 type
Uint32 timestamp;
Uint32 windowID;
Uint8 state;
Uint8 repeat;
SDL_Keysym keysym;
On retrouve, comme pour les événements de type SDL_WINDOWEVENT
, le champ windowID
qui correspond à l’identifiant de la fenêtre sur laquelle l’évènement a été détecté et le champ type
qui vaut alors SDL_KEYDOWN
ou SDL_KEYUP
. Le champ state
contient l’état de la touche et vaut SDL_PRESSED
si la touche est pressée et SDL_RELEASED
si elle est relâchée. Le champ repeat
est différent de 0 s’il y a eu répétition de l’appui.
Le champ qui nous intéresse le plus est keysym
. Il s’agit d’une structure SDL_Keysym
. C’est dans ses champs que l’on retrouvera des informations sur la touche pressée ou libérée. Ses champs sont les suivants.
SDL_Scancode scancode;
SDL_Keycode sym;
Uint16 mod;
Les champs scancode
et sym
correspondent tous les deux à la touche pressée (ou relâchée), mais le premier correspond au code de la touche physique alors que le second correspond au code de la touche virtuelle. Ils prennent comme valeur celles des énumérations SDL_Scancode
et SDL_Keycode
. Par exemple, pour savoir si on a appuyé sur la touche physique A, on vérifiera la condition event.key.keysym.scancode == SDL_SCANCODE_A
et pour la touche virtuelle on fera le test event.key.keysym.sym == SDLK_A
.
Mais quelle est la différence entre touche physique et touche virtuelle ?
En fait, la touche physique ne dépend pas du système et est la même sur tous les ordinateurs. Ainsi, SDL_SCANCODE_A
correspond à l’appui sur la touche A d’un clavier Qwerty, mais à la touche Q d’un clavier Azerty et à la touche A d’un clavier Bépo car ce qui compte ici, c’est la place physique de la touche sur le clavier. Au contraire, la touche virtuelle dépend du système et non du type de clavier, SDLK_a
correspond donc toujours à l’appui sur la touche A. Pour bien voir cette différence, essayons ce code :
while(!quit)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
else if(event.type == SDL_KEYDOWN)
{
if(event.key.keysym.scancode == SDL_SCANCODE_A)
printf("scancode A\n");
if(event.key.keysym.sym == SDLK_a)
printf("keysym A\n");
}
SDL_Delay(20);
}
En testant ce code sur un clavier Azerty, il nous faudra appuyer sur A pour que le message « keysym A » s’affiche, mais il nous faudra appuyer sur Q pour que « scancode A » s’affiche.
Le champ mod
de SDL_Keysym
nous permet de savoir si l’utilisateur a appuyé sur une (ou plusieurs) touche(s) spéciale(s) (Alt, Ctrl, Shift, etc.). Sa valeur correspond au OU logique de valeurs de l’énumération SDL_Keymod
. Par exemple, pour savoir si on a appuyé sur Ctrl, on vérifiera si event.key.keysym.mod & KMOD_CTRL != 0
.
La souris
Pour la souris, il y a, comme nous l’avons vu, trois types d’évènements. Les évènements du type déplacement de la souris, les appuis sur les touches et le déplacement de la molette.
Boutons de la souris
Lorsqu’il y a un évènement SDL_MOUSEBUTTONDOWN
ou un évènement SDL_MOUSEBUTTONUP
, les informations sur l’évènement sont placées dans le champ button
de SDL_Event
qui est une structure SDL_MouseButtonEvent
. Ses champs sont les suivants.
Uint32 type;
Uint32 timestamp;
Uint32 windowID;
Uint32 which;
Uint8 button;
Uint8 state;
Uint8 clicks
Sint32 x;
Sint32 y;
On a comme d’habitude l’identifiant de la fenêtre dans le champ windowID
et le type d’évènement dans le champ type
(il vaut alors SDL_MOUSEBUTTONDOWN
ou SDL_MOUSEBUTTONUP
). Le champ which
contient l’identifiant de la souris. Il peut servir dans le cas d’un appareil tactile (s’il y a appui sur l’écran, which
vaudra SDL_TOUCH_MOUSEID
). Le champ state
correspond comme pour le clavier à l’état de la touche qui a déclenché l’évènement. Il vaut SDL_PRESSED
si le bouton est pressé et SDL_RELEASED
s’il est relâché.
Les champs x
et y
correspondent à la position de la souris (par rapport à la fenêtre) au moment de l’évènement.
Le champ button
contient les informations sur la touche pressée et peut prendre plusieurs valeurs suivant la touche pressée. En particulier, on a les valeurs suivantes :
SDL_BUTTON_LEFT
si c’est un clic gauche ;SDL_BUTTON_RIGHT
si c’est un clic droit ;SDL_BUTTON_MIDDLE
s’il y a appui sur la molette.
Finalement, le champ clicks
peut s’avérer intéressant car il nous permet de savoir combien de clic il y a eu. Il vaut 1 dans le cas d’un clic simple, 2 dans le cas d’un double clic, etc.
Par exemple, on testera s’il y a eu double clic gauche avec ce code.
while(!quit)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
else if(event.type == SDL_MOUSEBUTTONUP)
{
if(event.button.button == SDL_BUTTON_LEFT && event.button.clicks >= 2)
printf("Au moins un double clic gauche\n");
}
SDL_Delay(20);
}
Déplacement de la souris
Dans le cas d’un évènement du type SDL_MOUSEMOTION
, les données sont stockées dans le champ motion
de SDL_Event
qui est une structure SDL_MousMotionEvent
qui a les champs suivants.
Uint32 type;
Uint32 timestamp;
Uint32 windowID;
Uint32 which;
Uint32 state;
Sint32 x;
Sint32 y;
Sint32 xrel;
Sint32 yrel;
Les champs windowID
, which
, x
et y
fonctionnent de la même manière que dans la structure SDL_MousButtonEvent
. Et comme d’habitude, type
contient le type d’évènement (ici SDL_MOUSEMOTION
).
Le champ state
nous permet de savoir sur quelles touches de la souris l’utilisateur appuyait pendant le déplacement de la souris. C’est une combinaison de différents masques qu’on trouve dans la documentation. Par exemple, on peut tester si event.motion.state & SDL_BUTTON_LMASK
pour savoir s’il y a appui sur la touche gauche pendant le déplacement.
Les champs xrel
et yrel
caractérisent le mieux le déplacement puisqu’ils nous donnent respectivement les déplacements relatifs sur les axes des x
et des y
. Par exemple, si on s’est déplacé de 1 pixel vers le haut et de 2 pixels vers la droite, alors xrel
vaudra 2 et yrel
vaudra -1.
Avec le code qui suit, on affiche la position de la souris et les déplacements relatifs à chaque fois qu’il y a déplacement et appui sur la touche gauche de la souris.
while(!quit)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
else if(event.type == SDL_MOUSEMOTION && (event.motion.state & SDL_BUTTON_LEFT))
printf("%d %d - %d %d\n", event.motion.x, event.motion.y, event.motion.xrel, event.motion.yrel);
SDL_Delay(20);
}
Utilisation de la molette
Et finalement, lorsqu’un évènement du type SDL_MOUSEWHEEL
est détecté, les informations sont placées dans le champ wheel
de SDL_Event
. Il s’agit d’une structure SDL_MouseWheelEvent
. Voici ses champs.
Uint32 type;
Uint32 timestamp;
Uint32 windowID;
Uint32 which;
Sint32 x;
Sint32 y;
Uint32 direction;
On retrouve les champs which
, type
(qui vaut cette fois SDL_MOUSEWHEEL
), et windowID
. Le champ y
caractérise le défilement vertical. Cette valeur est positive s’il y a eu défilement vers le haut et négative si le défilement était vers le bas. x
caractérise le déplacement horizontal de la molette (encore faut-il que la molette en soit capable) et est positif si le déplacement s’est fait vers la droite et négatif sinon.
Ainsi, avec ce code, on affiche la valeur de déplacement à chaque déplacement de la molette.
while(!quit)
{
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
quit = SDL_TRUE;
else if(event.type == SDL_MOUSEWHEEL)
printf("%d - %d - %d\n", event.wheel.x, event.wheel.y, event.wheel.timestamp);
SDL_Delay(20);
}
En testant ce code, on s’aperçoit qu’il est en fait très compliqué, mais vraiment très compliqué d’avoir une valeur de déplacement supérieure à 1 (ou inférieure à -1).
Nous n’avons jamais parlé du champ timestamp
qui est présent dans toutes les structures que nous avons vues ici. Ce champ a pour valeur le nombre de millisecondes écoulées entre l’initialisation de la SDL et l’évènement que l’on est en train d’analyser. Puisque nous parlons de timestamp, profitons-en pour présenter la fonction SDL_GetTicks
qui permet d’obtenir le temps en millisecondes depuis l’initialisation de la SDL. Son prototype est le suivant.
Uint32 SDL_GetTicks(void)
Le statut des périphériques
Dans certains cas, ce qui nous intéresse est de savoir l’état de chaque touche (clavier et souris) à n’importe quel moment. Donc en gros, on veut juste pouvoir savoir si telle ou telle touche est pressée ou pas à n’importe quel moment. La SDL propose un mécanisme pour cela en nous proposant des fonctions pour récupérer l’état des touches.
Ces fonctions, contrairement à celles que nous avons précédemment vues, n’appellent pas SDL_PumpEvents
. C’est donc à nous de faire cet appel, et ensuite, nous appellerons nos fonctions.
La souris
Pour récupérer l’état de la souris, il nous faut utiliser la fonction SDL_GetMouseState
. Voici son prototype.
Uint32 SDL_GetMouseState(int* x,
int* y)
Elle prend en paramètre deux pointeurs sur int
qui seront remplis avec les positions de la souris. Elle renvoie un Uint32
qui nous permettra de savoir quelles touches sont pressées en utilisant des flags. On l’utilise comme ceci.
int x, y;
Uint32 boutons;
SDL_PumpEvents();
boutons = SDL_GetMouseState(&x,&y);
if(boutons & SDL_BUTTON(SDL_BUTTON_LEFT))
printf("Clic droit à la positions %d - %d", x, y);
SDL_BUTTON
est une macro qu’il faudra utiliser pour tester si une touche est pressée.
Si la position de la souris ne nous intéresse pas, nous pouvons lui passer NULL
en paramètre.
Déplacement de la souris
Si nous voulons récupérer le déplacement de la souris, nous pouvons utiliser la fonction SDL_GetRelativeMouseState
, dont le prototype est le suivant.
Uint32 SDL_GetRelativeMouseState(int* x,
int* y)
Elle a le même prototype que SDL_GetMouseState
et s’utilise de la même manière. La seule différence est que x
et y
ne vaudront pas la position de la souris par rapport à la fenêtre, mais sa position par rapport à sa dernière position, c’est-à-dire par rapport à la position enregistrée lors du dernier appel à SDL_GetRelativeMouseState
. C’est donc bien le déplacement que l’on obtient.
Le clavier
La fonction pour obtenir l’état du clavier est étonnamment la fonction SDL_GetKeyboardState
.
const Uint8* SDL_GetKeyboardState(int* numkeys)
Elle prend en paramètre un pointeur sur int
et retourne un pointeur sur Uint8
. Le pointeur qu’elle renvoie pointe sur la première case d’un tableau dont les éléments sont les états des différentes touches du clavier (1 si la touche est pressée et 0 sinon). Il s’agit d’un tableau géré par la SDL. Il est valide pendant toute l’exécution du programme et nous n’avons pas besoin de le libérer manuellement. Pour savoir quelle case du tableau correspond à quelle touche, il nous faut utiliser les valeurs de l’énumération SDL_Scancode
. Par exemple, tab[SDL_SCANCODE_RETURN] == 1
signifie que la touche Entrée est pressée.
La variable sur laquelle pointe numkeys
sera modifiée et vaudra la longueur du tableau. Comme nous n’avons généralement pas besoin de connaître la longueur de ce tableau, nous passons généralement NULL
en argument. On peut alors écrire ce code qui boucle tant qu’on n’appuie pas sur Échap ou sur Entrée.
Uint8 *clavier;
while(!quit)
{
SDL_PumpEvents();
clavier = SDL_GetKeyboardState(NULL);
if(clavier[SDL_SCANCODE_ESCAPE] || clavier[SDL_SCANCODE_RETURN])
quit = SDL_TRUE;
}
Les touches spéciales du clavier
Pour les touches spéciales du clavier, il nous faut encore utiliser une autre fonction. Il s’agit de la fonction SDL_GetModState
dont le prototype est le suivant.
SDL_Keymod SDL_GetModState(void)
Elle ne prend aucun paramètre et renvoie une combinaison de OU logique des touches spéciales pressées. Pour tester si une touche est pressée, on va alors utiliser le ET logique.
SDL_Keymod keymod;
Uint8 *clavier;
while(!quit)
{
SDL_PumpEvents();
clavier = SDL_GetKeyboardState(NULL);
keymod = SDL_GetModeState();
if(keymod & KMOD_CTRL)
printf("Appui sur Ctrl\n");
if(clavier[SDL_SCANCODE_ESCAPE])
quit = SDL_TRUE;
}
Une structure pour la gestion des évènements
Cela peut être encore mieux si nous utilisions notre propre structure pour gérer tout ça. Nous pourrions alors placer une touche à zéro à la main (imaginons un jeu où l’on voudrait que l’utilisateur ne puisse pas maintenir la touche de tir enfoncée, mais doive la relâcher avant de ré-appuyer pour tirer à nouveau). Pour la mettre à jour, nous n’utiliserons pas les fonctions d’acquisition d’état mais une boucle avec un SDL_PollEvent
qui nous permettra d’y mettre également l’évènement SDL_QUIT
. D’ailleurs, on va commencer par gérer cet évènement et le clavier.
struct Input
{
SDL_bool key[SDL_NUM_SCANCODES];
SDL_bool quit;
int x, y, xrel, yrel;
int xwheel, ywheel;
SDL_bool mouse[6];
};
La constante SDL_NUM_SCANCODES
est une constante qui est plus grande que la plus grande des valeurs de l’énumération SDL_Scancode
. Nous sommes alors sûrs que notre tableau est suffisamment grand pour contenir toutes les touches du clavier. x
et y
représentent la position de la souris, xrel
et yrel
le déplacement de la souris, et xwheel
et ywheel
le déplacement de la molette. Pour savoir quelle valeur prendre pour le tableau mouse
, nous pouvons regarder les déclarations des différentes valeurs des boutons de la souris.
#define SDL_BUTTON_LEFT 1
#define SDL_BUTTON_MIDDLE 2
#define SDL_BUTTON_RIGHT 3
#define SDL_BUTTON_X1 4
#define SDL_BUTTON_X2 5
Dès lors, il nous suffirait d’un tableau de 5
éléments, mais il nous faudrait décaler les indices de 1. Nous allons plutôt déclarer un tableau de six éléments (de plus, le jour où nous voudrons gérer les appareils tactiles, nous pourrons utiliser la première case pour SDL_TOUCH_MOUSEID
).
Cela donne lieu à cette fonction de mise à jour de notre structure.
void updateEvent(struct Input *input)
{
SDL_Event event;
while(SDL_PollEvent(&event))
{
if(event.type == SDL_QUIT)
input->quit = SDL_TRUE;
else if(event.type == SDL_KEYDOWN)
input->key[event.key.keysym.scancode] = SDL_TRUE;
else if(event.type == SDL_KEYUP)
input->key[event.key.keysym.scancode] = SDL_FALSE;
else if(event.type == SDL_MOUSEMOTION)
{
input->x = event.motion.x;
input->y = event.motion.y;
input->xrel = event.motion.xrel;
input->yrel = event.motion.yrel;
}
else if(event.type == SDL_MOUSEWHEEL)
{
input->xwheel = event.wheel.x;
input->ywheel = event.wheel.y;
}
else if(event.type == SDL_MOUSEBUTTONDOWN)
input->mouse[event.button.button] = SDL_TRUE;
else if(event.type == SDL_MOUSEBUTTONUP)
input->mouse[event.button.button] = SDL_FALSE;
}
}
Et là, nous pouvons écrire ce genre de code.
while(!in.quit)
{
updateEvent(&in);
if(in.key[SDL_SCANCODE_A])
{
printf("Appui sur la touche A");
in.key[SDL_SCANCODE_A] = SDL_FALSE;
}
if(in.key[SDL_SCANCODE_C] && in.key[SDL_SCANCODE_D])
printf("Appui sur les touches C et D");
SDL_Delay(20);
}
Ici, en restant appuyé sur C et D, le second message n’arrêtera pas de s’afficher, mais nous sommes obligés de relâcher la touche A, puis de ré-appuyer pour ré-afficher le premier message.
Nous pourrions rajouter à notre structure d’autres fonctionnalités telles que la gestion de certains des évènements de la fenêtre ou la gestion des touches spéciales du clavier.
Notre structure est très intéressante. Elle nous permet de séparer la gestion des évènements du reste du programme. On ne met à jour notre structure qu’au début de notre boucle. On se contente ensuite de vérifier sur quelle touche il y a appui.
Notons qu’avant d’utiliser la structure, il faut l’initialiser en la remplissant de SDL_FALSE
pour que l’on ne puisse pas détecter d’évènements alors qu’en fait il n’y en a pas. Pour cela, nous pouvons par exemple utiliser la fonction memset
.
Maintenant que nous avons vu comment gérer les événements, nous pouvons enfin faire des programmes interactifs.