Tous droits réservés

Les évènements 1

Dernière mise à jour :

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 type

Champ de SDL_Event correspondant

Description

Évènements de l’application

SDL_QUIT

quit

Demande de fermeture du programme

Évènements de la fenêtre

SDL_WINDOWEVENT

window

Changement d’état de la fenêtre

SDL_SYSWMEVENT

syswm

Évènement dépendant du sytème

Évènements du clavier

SDL_KEYDOWN

key

Une touche est pressé

SDL_KEYUP

key

Une touche est relâchée

SDL_TEXTEDITING

edit

Édition de texte

SDL_TEXTINPUT

text

Saisie de texte

Évènements de la souris

SDL_SDL_MOUSEMOTION

motion

Déplacement de la souris

SDL_MOUSEBUTTONDOWN

button

Une touche de la souris est pressée

SDL_MOUSEBUTTONUP

button

Une touche de la souris est relâchée

SDL_MOUSEWHEEL

wheel

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.

1
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.

1
2
3
4
5
6
7
8
9
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.

1
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
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).

1
2
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.

1
2
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.

1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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.

1
2
3
4
5
6
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.

1
2
3
4
5
6
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

1
2
3
4
5
6
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.

1
2
3
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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.

1
2
3
4
5
6
7
8
9
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
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.

1
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.

1
2
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.

1
2
3
4
5
6
7
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.

1
2
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.

1
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.

1
2
3
4
5
6
7
8
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.

1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
#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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.