Gestion des entrées
Tutoriel SDL: Gestion des entrées
Paru dans Linux Mag 66, novembre 2004
Notre premier article constituait une introduction encore très incomplète à SDL. Ce mois-ci, nous allons la compléter en étudiant la gestion des entrées qui nous permettra de réaliser des programmes plus réactifs.
Le plus gros défaut de notre programme précédent est qu'il est strictement linéaire: il dessine quelques bricoles, attend 5 secondes, et se termine. Pour donner un peu plus de goût à nos futurs programmes ludiques, il est nécessaire de disposer d'une bonne interaction avec l'utilisateur.
Les entrées avec SDL
La gestion des entrées dans SDL est, comme le reste, d'assez bas niveau. Contrairement à des APIs comme GTK ou Qt, les entrées ne sont pas gérées par un mécanisme de callback où une fonction est automatiquement appelée lorsqu'un événement survient. Ici, c'est au programmeur de prendre l'initiative de regarder si quelque chose s'est passé et de réagir en conséquence. Cela se fera pour notre cas par un appel régulier à une fonction de gestion des entrées dans notre boucle de jeu.
L'utilisation des entrées nécessite qu'un mode vidéo soit activé. Il est donc inutile de s'en préoccuper tant que SDL_SetVideoMode n'a pas été appelé.
SDL stocke les évènements arrivants dans une file d'attente. La fonction SDL_PollEvent permet de retirer l'élément en tête de file:
int SDL_PollEvent(SDL_Event * event);
Elle retourne 1 si il y avait des évènements en attente, 0 si la file d'attente était vide. Dans notre fonction de gestion des entrées, nous allons l'appeler tant qu'elle n'est pas vide pour traiter les évènements qui sont survenus depuis notre dernier passage. Il existe plusieurs autres fonctions liées à la gestion de la file des évènements, notamment pour la regarder sans la vider ou ajouter arbitrairement un événement. N'étant pas primordiales dans le cadre de la programmation ludique, nous les ignorerons lors de nos articles.
Dans le cas où SDL_PollEvent retire un événement, ce dernier est placé dans la structure SDL_Event donnée en argument. SDL_PollEvent va retourner absolument tous les évènements susceptibles d'arriver: appui d'une touche clavier, mouvement de la souris, mais aussi redimensionnement de la fenêtre SDL ou réception du focus. Tous ces évènements sont stockables dans la structure SDL_Event qui n'est rien d'autre qu'une union de tous les évènements gérés par SDL. Pour savoir à quel type d'évènement on a affaire, on regardera son champ type. Voici quelques-unes de ses valeurs qui nous intéressent dans un premier temps:
- SDL_KEYDOWN
- appui d'une touche clavier
- SDL_KEYUP
- relâchement d'une touche clavier
- SDL_MOUSEMOTION
- mouvement de la souris
- SDL_MOUSEBUTTONDOWN
- appui d'un bouton de la souris
- SDL_MOUSEBUTTONUP
- relâchement d'un bouton de la souris
En fonction du type d'évènement, on accèdera au membre du SDL_Event correspondant au membre d'union qui correspond. Pour un évènement clavier, ce sera key, pour un mouvement de souris motion, ou pour un click de souris button.
Chacun de ces membres a lui-même sa propre structure, que nous allons découvrir superficiellement. Pour plus de détails, et pour connaître les évènements gérés par SDL mais non-liés aux entrées, vous pouvez vous reporter à sa documentation.
Évènements clavier
Comme dit précédemment, les informations associées à un événement clavier sont accessibles via le membre key du SDL_Event correspondant. Il est lui-même d'un type structure (SDL_KeyboardEvent) et son seul membre intéressant (outre type) est keysym. C'est en effet lui qui va nous renseigner sur quelle touche a été actionnée ou relâchée. Il s'agit encore d'une structure (SDL_Keysym) dont voici les membres les plus utiles:
- sym
- nous donne le code de la touche concernée. Il peut être testé avec les macros de type SDLK_NOMDETOUCHE pour connaître précisément quelle touche a été enfoncée ou relâchée. La liste complète des noms de touche est disponible dans le fichier SDL_keysym.h du répertoire d'include de SDL, ou dans la documentation.
- mod
- nous informe sur l'état des modifieurs (touches CTRL, ALT, etc.). Son intérêt étant limité dans notre cadre, nous ne ferons que le mentionner.
- unicode
- fournit une information d'un peu plus haut niveau et nous donne, si le mode unicode de SDL a été activé, le caractère unicode qui a été émis par l'appui de cette touche. L'information est de plus haut niveau car le caractère retourné tient compte de l'état des modifieurs (majuscule si SHIFT est enfoncé, par exemple) et de la localisation du clavier. Il peut être utile si vous avez à lire du texte via le clavier. Pour activer le mode unicode (désactivé par défaut à cause du traitement supplémentaire qu'il induit), il suffit d'appeler SDL_EnableUNICODE(1).
Gestion de la souris
Les évènements souris sont gérés par les structures SDL_MouseMotionEvent (nom de membre: motion) pour les déplacements et SDL_MouseButtonEvent (nom de membre: button) pour les clics de souris. Concernant SDL_MouseMotionEvent, ses membres intéressants sont:
- x et y
- qui donnent les coordonnées absolues de la souris dans le repère graphique correspondant à la zone d'écran.
- xrel et yrel
- qui donnent le déplacement du curseur de souris, par rapport à sa position initiale.
Pour la gestion des boutons de la souris et la structure SDL_MouseButtonEvent:
Le membre button indique quel bouton a été cliqué ou relâché. Les macros SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE, SDL_BUTTON_RIGHT, SDL_BUTTON_WHEELUP et SDL_BUTTON_WHEELDOWN) peuvent être utilisées pour tester les trois principaux boutons ainsi que la molette. Les éventuels boutons additionnels portent des numéros supérieurs à 5.
On retrouve aussi les membres x et y qui jouent un rôle similaire à ceux de SDL_MouseMotionEvent et permettent de connaître quelle zone d'écran a été cliquée.
Comme nous venons de le voir, la gestion des deux périphériques d'entrée principaux est relativement aisée. Cependant, nombreux sont les types de jeux dont le gameplay est multiplié s'ils sont joués avec un joystick ou un joypad. Comme il se doit, SDL nous offre une gestion très complète de ces périphériques.
Gestion du joystick
La gestion du joystick, incontournable pour beaucoup de jeux, est plus complexe. En effet, si les claviers et souris sont relativement standardisés, les joysticks sont eux très variés. Les différences incluent notamment le nombre et le type des axes, le nombre de boutons, la présence ou non d'un POV ou d'un trackball, etc.
Commençons par standardiser le langage: un axe sera un élément de type manche à balai (ou axes à potentiomètre), un chapeau pourra être soit le « hat » que l'on trouve en haut d'un joystick et qui sert à gérer le point de vue dans les simulateurs de vol, soit le pad d'un joypad (ou tout type d'axe à 3 positions: gauche, centré ou droite). Les boutons comprennent tout ce qui est pressable, et les trackballs sont les petites boules orientables.
Pour activer la gestion des joysticks dans SDL, il faudra ajouter le flag SDL_INIT_JOYSTICK lors de notre appel à SDL_Init, récupérer le nombre de joysticks connectés au système avec SDL_NumJoysticks et les ouvrir avec SDL_JoystickOpen (faute de quoi ils n'émettront pas d'évènements). Cette dernière fonction prend l'index du joystick à ouvrir (qui doit être inférieur à la valeur retournée par SDL_NumJoysticks) et retourne un pointeur vers un SDL_Joystick qui pourra être utilisé pour obtenir plus d'informations sur les capacités du périphérique. Ces informations s'obtiennent grâce à une série de fonctions dédiées:
- SDL_JoystickName
- prend un index de joystick et retourne sa chaîne d'identifiant,
- SDL_JoystickNumAxes, SDL_JoystickNumButtons, SDL_JoyStickNumBalls et SDL_JoyStickNumHats
- prennent quant à elles le pointeur retourné par SDL_JoystickOpen et retournent respectivement le nombre d'axes, de boutons, de trackballs et de chapeaux disponibles sur le joystick.
Une dernière fonction d'intérêt est SDL_JoystickClose qui permet de faire « taire » un joystick.
Une fois nos joysticks ouverts, ils enverront des évènements au même titre que les autres périphériques d'entrée. Un joystick est capable de générer quatre types d'évènements, selon que l'on a touché à ses axes, ses chapeaux, ses boutons ou ses trackballs.
Un mouvement d'axe sera représenté par un événement de type SDL_JOYAXISMOTION, accessible via le membre jaxis de SDL_Event. Ses membres sont:
- which
- le numéro du joystick qui a émis l'évènement (ce membre se retrouvera dans tous les évènements liés aux joysticks).
- axis
- le numéro de l'axe ayant bougé.
- value
- la position de l'axe, comprise entre -32767 (complètement à gauche/en haut) et 32767 (complètement à droite/en bas).
Un chapeau émettra quant à lui un évèmenent de type SDL_JOYHATMOTION, accessible par le membre jhat. Outre le membre which indiquant le joystick émetteur, on trouvera:
- hat
- qui indique le numéro du chapeau concerné par l'évènement.
- value
- qui comme pour les évènements liés aux axes donnera la position du chapeau. Le nombre de valeurs possibles étant limité, les valeurs se testeront avec les macros SDL_HAT_CENTERED, SDL_HAT_UP, SDL_HAT_RIGHT, SDL_HAT_DOWN, SDL_HAT_LEFT, ces macros étant composables par ou logique pour en obtenir d'autres: SDL_HAT_RIGHTUP, SDL_HAT_RIGHTDOWN, SDL_HAT_LEFTUP, SDL_HAT_LEFTDOWN. En toute logique, SDL_HAT_RIGHTUP sera égal à (SDL_HAT_RIGHT | SDL_HAT_UP).
Les trackballs émettront des évènements SDL_JOYBALLMOTION lisibles via jball, très proches d'un mouvement de souris:
- ball
- indique le numéro du trackball émetteur.
- xrel et yrel
- donnent les mouvements du trackball par rapport à leur dernière position. Notez que, contrairement aux évènements souris, on ne dispose pas de la position absolue.
Enfin, les évènements boutons se gèrent pratiquement comme des appuis sur des touches clavier. Deux types d'évènements leur sont associés, SDL_JOYBUTTONDOWN pour un appui et SDL_JOYBUTTONUP pour un relâchement, et les informations utiles sont accessibles via le membre jbutton de SDL_Event:
- which
- toujours présent.
- button
- indique le numéro du bouton appuyé ou relâché.
Le programme de ce mois-ci
Notre programme d'exemple met toutes ces notions en application. Il analyse tous les évènements d'entrée venant du clavier, de la souris ou des joysticks. Pour chacun de ces évènements, il fait un rapport détaillé sur la sortie standard. De plus, les touches directionnelles du clavier, les mouvements de la souris, ainsi que le déplacement des axes, chapeaux et trackballs des joysticks connectés permettent de diriger le petit sprite affiché à l'écran. La touche Escape permet de quitter le programme. Il se compile et se lance de la même manière que l'exemple du mois dernier. Remarquez les différentes façons de mettre à jour la position de notre sprite (soit par sa vélocité, soit directement par sa position absolue selon le type d'évènement). Comme d'habitude, le source complet ainsi que l'image utilisée sont disponibles ici.
#include <stdio.h>
#include "SDL.h"
/* Variable permettant de quitter la boucle de jeu */
unsigned char letsexit = 0;
/* Vélocité du sprite */
Uint16 xvel = 0, yvel = 0;
/* Position du sprite */
SDL_Rect spritepos;
/* Gestion des évènements d'entrée */
void process_events()
{
SDL_Event event;
/* Un évènement attend d'être traité? */
while (SDL_PollEvent(&event))
{
/* Si oui, quel type? */
switch (event.type)
{
/* Appui sur une touche */
case SDL_KEYDOWN:
printf("Touche %d enfoncée (caractère produit: %c)\n",
event.key.keysym.sym,
event.key.keysym.unicode);
switch (event.key.keysym.sym)
{
case SDLK_ESCAPE:
letsexit = 1;
break;
case SDLK_LEFT:
xvel = -1;
break;
case SDLK_RIGHT:
xvel = 1;
break;
case SDLK_UP:
yvel = -1;
break;
case SDLK_DOWN:
yvel = 1;
break;
default:
break;
}
break;
/* Relâchement d'une touche */
case SDL_KEYUP:
printf("Touche %d relâchée\n", event.key.keysym.sym);
switch (event.key.keysym.sym)
{
case SDLK_LEFT:
case SDLK_RIGHT:
xvel =0;
break;
case SDLK_UP:
case SDLK_DOWN:
yvel = 0;
break;
default:
break;
}
break;
/* Déplacement souris */
case SDL_MOUSEMOTION:
spritepos.x += event.motion.xrel;
spritepos.y += event.motion.yrel;
printf("Position souris: (d)\n", event.motion.x,
event.motion.y);
break;
/* Enfoncement bouton souris */
case SDL_MOUSEBUTTONDOWN:
printf("Click bouton souris %d\n", event.button.button);
break;
/* Relâchement bouton souris */
case SDL_MOUSEBUTTONUP:
printf("Relâchement bouton souris %d\n", event.button.button);
break;
/* Déplacement axe joystick */
case SDL_JOYAXISMOTION:
printf("Axe d positionné à %d\n",
event.jaxis.axis, event.jaxis.which, event.jaxis.value);
/* Les axes pairs sont verticaux, les impairs horizontaux.
Les valeurs de jaxis.value étant très grandes, on compense
en faisant un décalage à droite (division par 2^13) */
if (event.jaxis.axis % 2)
yvel = event.jaxis.value >> 13;
else
xvel = event.jaxis.value >> 13;
break;
/* Déplacement chapeau joystick */
case SDL_JOYHATMOTION:
printf("Chapeau d en position ",
event.jhat.hat, event.jhat.which);
switch (event.jhat.value)
{
case SDL_HAT_CENTERED:
xvel = 0; yvel = 0;
printf("centrée\n");
break;
case SDL_HAT_UP:
xvel = 0; yvel = -1;
printf("haute\n");
break;
case SDL_HAT_RIGHT:
xvel = 1; yvel = 0;
printf("droite\n");
break;
case SDL_HAT_DOWN:
xvel = 0; yvel = 1;
printf("basse\n");
break;
case SDL_HAT_LEFT:
xvel = -1; yvel = 0;
printf("gauche\n");
break;
case SDL_HAT_RIGHTUP:
xvel = 1; yvel = -1;
printf("haute/droite\n");
break;
case SDL_HAT_RIGHTDOWN:
xvel = 1; yvel = 1;
printf("basse/droite\n");
break;
case SDL_HAT_LEFTUP:
xvel = -1; yvel = -1;
printf("haute/gauche\n");
break;
case SDL_HAT_LEFTDOWN:
xvel = -1; yvel = 1;
printf("basse/gauche\n");
break;
}
break;
/* Déplacement trackball joystick */
case SDL_JOYBALLMOTION:
spritepos.x += event.jball.xrel;
spritepos.y += event.jball.yrel;
printf("Trackball d a bougé de (d)\n",
event.jball.ball, event.jball.which,
event.jball.xrel, event.jball.yrel);
break;
/* Appui bouton joystick */
case SDL_JOYBUTTONDOWN:
printf("Bouton d enfoncé\n",
event.jbutton.button, event.jbutton.which);
break;
/* Relâchement bouton joystick */
case SDL_JOYBUTTONUP:
printf("Bouton d relâché\n",
event.jbutton.button, event.jbutton.which);
break;
default:
break;
}
}
}
#define SDL_VIDEO_FLAGS (SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)
int main(int argc, char * argv[])
{
SDL_Surface * screen;
SDL_Surface * sprite, * tmp;
int nbjoysticks;
int i;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) == -1)
{
fprintf(stderr, "Erreur lors de l'initialisation de SDL: %s\n",
SDL_GetError());
return 1;
}
screen = SDL_SetVideoMode(640, 480, 24, SDL_VIDEO_FLAGS);
printf("Mode vidéo: dx%d\n", screen->w, screen->h,
screen->format->BitsPerPixel);
/* Activation du support UNICODE */
SDL_EnableUNICODE(1);
nbjoysticks = SDL_NumJoysticks();
printf("Nombres de joysticks attachés: %d\n\n", nbjoysticks);
for (i = 0; i < nbjoysticks; i++)
{
SDL_Joystick * joy = SDL_JoystickOpen(i);
printf("Joystick s\n", i, SDL_JoystickName(i));
printf("Axes: %d\n", SDL_JoystickNumAxes(joy));
printf("Boutons: %d\n", SDL_JoystickNumButtons(joy));
printf("Trackballs: %d\n", SDL_JoystickNumBalls(joy));
printf("Chapeaux: %d\n\n", SDL_JoystickNumHats(joy));
}
/* Chargement du sprite */
tmp = SDL_LoadBMP("image.bmp");
sprite = SDL_DisplayFormat(tmp);
SDL_FreeSurface(tmp);
SDL_SetColorKey(sprite, SDL_SRCCOLORKEY, SDL_MapRGB(sprite->format, 0xff, 0x00, 0xff));
/* Position initiale du sprite */
spritepos.x = (screen->w - sprite->w) / 2;
spritepos.y = (screen->h - sprite->h) / 2;
spritepos.w = 0;
spritepos.h = 0;
/* Boucle de jeu */
while (!letsexit)
{
/* Mise à jour de l'état du jeu à partir des
évènements */
process_events();
/* Mise à jour de la position du sprite à partir
de sa vélocité */
spritepos.x += xvel;
spritepos.y += yvel;
/* Test de débordement d'écran */
if (spritepos.x < 0) spritepos.x = 0;
if (spritepos.x > screen->w - sprite->w) spritepos.x = screen->w - sprite->w;
if (spritepos.y < 0) spritepos.y = 0;
if (spritepos.y > screen->h - sprite->h) spritepos.y = screen->h - sprite->h;
/* Mise à jour de l'écran */
/* Remplissage de l'écran en noir */
SDL_FillRect(screen, NULL, 0);
/* Affichage du sprite puis mise à jour du
buffer d'affichage */
SDL_BlitSurface(sprite, NULL, screen, &spritepos);
SDL_Flip(screen);
}
SDL_Quit();
return 0;
}