La gestion des événements est une des fonctionnalités les plus importantes de la SDL.
C'est probablement une des sections les plus passionnantes à découvrir.
C'est à partir de là que vous allez vraiment être capables de contrôler
votre application.
Chacun de vos périphériques (clavier, souris…) peut produire des événements. Nous allons apprendre à intercepter ces événements et à réagir en conséquence. Votre application va donc devenir enfin réellement dynamique !
Concrètement, qu'est-ce qu'un événement ? C'est un « signal » envoyé par un périphérique (ou par le système d'exploitation) à votre application. Voici quelques exemples d'événements courants :
quand l'utilisateur appuie sur une touche du clavier ;
quand il clique avec la souris ;
quand il bouge la souris ;
quand il réduit la fenêtre ;
quand il demande à fermer la fenêtre ;
etc.
Le rôle de ce chapitre sera de vous apprendre à traiter ces événements. Vous serez capables de dire à l'ordinateur « Si l'utilisateur clique à cet endroit, fais ça, sinon fais cela… S'il bouge la souris, fais ceci. S'il appuie sur la touche Q, arrête le programme… », etc.
Le principe des événements
Pour nous habituer aux événements, nous allons apprendre à traiter le plus simple d'entre eux : la demande de fermeture du programme.
C'est un événement qui se produit lorsque l'utilisateur clique sur la croix pour fermer la fenêtre (fig. suivante).
C'est
vraiment l'événement le plus simple. En plus, c'est un événement que
vous avez utilisé jusqu'ici sans vraiment le savoir, car il était situé
dans la fonction pause()
!
En effet, le rôle de la fonction pause
était d'attendre que l'utilisateur demande à fermer le programme. Si on
n'avait pas créé cette fonction, la fenêtre se serait affichée et
fermée en un éclair !
La variable d'événement
Pour traiter des événements, vous aurez besoin de déclarer une variable (juste une seule, rassurez-vous) de type SDL_Event
. Appelez-la comme vous voulez : moi, je vais l'appeler event
, ce qui signifie « événement » en anglais.
SDL_Event event;
Pour nos tests nous allons nous contenter d'un main
très basique qui affiche juste une fenêtre, comme on l'a vu quelques chapitres plus tôt. Voici à quoi doit ressembler votre main
:
int main(int argc, char *argv[])
{
SDL_Surface *ecran = NULL;
SDL_Event event; // Cette variable servira plus tard à gérer les événements
SDL_Init(SDL_INIT_VIDEO);
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
SDL_WM_SetCaption("Gestion des événements en SDL", NULL);
SDL_Quit();
return EXIT_SUCCESS;
}
C'est donc un code très basique, il ne contient qu'une nouveauté : la déclaration de la variable event
dont nous allons bientôt nous servir.
Testez ce code : comme prévu, la fenêtre va s'afficher et se fermer immédiatement après.
La boucle des événements
Lorsqu'on
veut attendre un événement, on fait généralement une boucle. Cette
boucle se répètera tant qu'on n'a pas eu l'événement voulu.
On va avoir besoin d'utiliser un booléen qui indiquera si on doit continuer la boucle ou non.
Créez donc ce booléen que vous appellerez par exemple continuer
:
int continuer = 1;
Ce booléen est mis à 1 au départ car on veut que la boucle se répète TANT QUE la variable continuer
vaut 1 (vrai). Dès qu'elle vaudra 0 (faux), alors on sortira de la boucle et le programme s'arrêtera.
Voici la boucle à créer :
while (continuer)
{
/* Traitement des événements */
}
Voilà : on a pour le moment une boucle infinie qui ne s'arrêtera que si on met la variable continuer
à 0. C'est ce que nous allons écrire à l'intérieur de cette boucle qui est le plus intéressant.
Récupération de l'événement
Maintenant, faisons appel à une fonction de la SDL pour demander si un événement s'est produit.
On dispose de deux fonctions qui font cela, mais d'une manière différente :
SDL_WaitEvent
: elle attend qu'un événement se produise. Cette fonction est dite bloquante car elle suspend l'exécution du programme tant qu'aucun événement ne s'est produit ;SDL_PollEvent
: cette fonction fait la même chose mais n'est pas bloquante. Elle vous dit si un événement s'est produit ou non. Même si aucun événement ne s'est produit, elle rend la main à votre programme de suite.
Ces deux fonctions sont utiles, mais dans des cas différents.
Pour faire simple, si vous utilisez SDL_WaitEvent
votre programme utilisera très peu de processeur car il attendra qu'un événement se produise.
En revanche, si vous utilisez SDL_PollEvent
, votre programme va parcourir votre boucle while
et rappeler SDL_PollEvent
indéfiniment jusqu'à ce qu'un événement se soit produit. À tous les coups, vous utiliserez 100 % du processeur.
Mais alors, il faut tout le temps utiliser SDL_WaitEvent
si cette fonction utilise moins le processeur, non ?
Non, car il y a des cas où SDL_PollEvent
se révèle indispensable. C'est le cas des jeux dans lesquels l'écran se met à jour même quand il n'y a pas d'événement.
Prenons par exemple Tetris : les blocs descendent tout seuls, il n'y a
pas besoin que l'utilisateur crée d'événement pour ça ! Si on avait
utilisé SDL_WaitEvent
, le programme serait resté « bloqué »
dans cette fonction et vous n'auriez pas pu mettre à jour l'écran pour
faire descendre les blocs !
Comment fait SDL_WaitEvent
pour ne pas consommer de processeur ?
Après tout, la fonction est bien obligée de faire une boucle infinie
pour tester tout le temps s'il y a un événement ou non, n'est-ce pas ?
C'est
une question que je me posais il y a encore peu de temps. La réponse
est un petit peu compliquée car ça concerne la façon dont l'OS gère les
processus (les programmes).
Si vous voulez – mais je vous en parle rapidement –, avec SDL_WaitEvent
, le processus de votre programme est mis « en pause ». Votre programme n'est donc plus traité par le processeur.
Il sera « réveillé » par l'OS au moment où il y aura un événement. Du
coup, le processeur se remettra à travailler sur votre programme à ce
moment-là. Cela explique pourquoi votre programme ne consomme pas de
processeur pendant qu'il attend l'événement.
Je
comprends que ce soit un peu abstrait pour vous pour le moment. Et à
dire vrai, vous n'avez pas besoin de comprendre ça maintenant. Vous
assimilerez mieux toutes les différences plus loin en pratiquant.
Pour le moment, nous allons utiliser SDL_WaitEvent
car notre programme reste très simple. Ces deux fonctions s'utilisent de toute façon de la même manière.
Vous devez envoyer à la fonction l'adresse de votre variable event
qui stocke l'événement.
Comme cette variable n'est pas un pointeur (regardez la déclaration à nouveau), nous allons mettre le symbole &
devant le nom de la variable afin de donner l'adresse :
SDL_WaitEvent(&event);
Après appel de cette fonction, la variable event
contient obligatoirement un événement.
Analyse de l'événement
Maintenant, nous disposons d'une variable event
qui contient des informations sur l'événement qui s'est produit.
Il faut regarder la sous-variable event.type
et faire un test sur sa valeur. Généralement on utilise un switch
pour tester l'événement.
Mais comment sait-on quelle valeur correspond à l'événement « Quitter », par exemple ?
La SDL nous fournit des constantes, ce qui simplifie grandement l'écriture du programme. Il en existe beaucoup (autant qu'il y a d'événements possibles). Nous les verrons au fur et à mesure tout au long de ce chapitre.
while (continuer)
{
SDL_WaitEvent(&event); /* Récupération de l'événement dans event */
switch(event.type) /* Test du type d'événement */
{
case SDL_QUIT: /* Si c'est un événement de type "Quitter" */
continuer = 0;
break;
}
}
Voici comment ça fonctionne.
Dès qu'il y a un événement, la fonction
SDL_WaitEvent
renvoie cet événement dansevent
.On analyse le type d'événement grâce à un
switch
. Le type de l'événement se trouve dansevent.type
On teste à l'aide de
case
dans leswitch
le type de l'événement. Pour le moment, on ne teste que l'événementSDL_QUIT
(demande de fermeture du programme), car c'est le seul qui nous intéresse.Si c'est un événement
SDL_QUIT
, c'est que l'utilisateur a demandé à quitter le programme. Dans ce cas on met le booléencontinuer
à 0. Au prochain tour de boucle, la condition sera fausse et donc la boucle s'arrêtera. Le programme s'arrêtera ensuite.Si ce n'est pas un événement
SDL_QUIT
, c'est qu'il s'est passé autre chose : l'utilisateur a appuyé sur une touche, a cliqué ou tout simplement bougé la souris dans la fenêtre. Comme ces autres événements ne nous intéressent pas, on ne les traite pas. On ne fait donc rien : la boucle recommence et on attend à nouveau un événement (on repart à l'étape 1).
Ce que je viens de vous expliquer ici est extrêmement important. Si vous avez compris ce code, vous avez tout compris et le reste du chapitre sera très facile pour vous.
Le code complet
int main(int argc, char *argv[])
{
SDL_Surface *ecran = NULL;
SDL_Event event; /* La variable contenant l'événement */
int continuer = 1; /* Notre booléen pour la boucle */
SDL_Init(SDL_INIT_VIDEO);
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
SDL_WM_SetCaption("Gestion des événements en SDL", NULL);
while (continuer) /* TANT QUE la variable ne vaut pas 0 */
{
SDL_WaitEvent(&event); /* On attend un événement qu'on récupère dans event */
switch(event.type) /* On teste le type d'événement */
{
case SDL_QUIT: /* Si c'est un événement QUITTER */
continuer = 0; /* On met le booléen à 0, donc la boucle va s'arrêter */
break;
}
}
SDL_Quit();
return EXIT_SUCCESS;
}
Voilà le code complet. Il n'y a rien de bien difficile : si vous avez suivi jusqu'ici, ça ne devrait pas vous surprendre.
D'ailleurs, vous remarquerez qu'on n'a fait que reproduire ce que faisait la fonction pause
. Comparez avec le code de la fonction pause
: c'est le même, sauf qu'on a cette fois tout mis dans le main
. Bien entendu, il est préférable de placer ce code dans une fonction à part, comme pause
, car cela allège la fonction main
et la rend plus lisible.
Le clavier
Nous allons maintenant étudier les événements produits par le clavier.
Si vous avez compris le début du chapitre, vous n'aurez aucun problème pour traiter les autres types d'événements. Il n'y a rien de plus facile.
Pourquoi
est-ce si simple ? Parce que maintenant que vous avez compris le
fonctionnement de la boucle infinie, tout ce que vous allez avoir à
faire, c'est d'ajouter d'autres case
dans le switch
pour traiter d'autres types d'événements. Ça ne devrait pas être trop dur.
Les événements du clavier
Il existe deux événements différents qui peuvent être générés par le clavier :
SDL_KEYDOWN
: quand une touche du clavier est enfoncée ;SDL_KEYUP
: quand une touche du clavier est relâchée.
Pourquoi y a-t-il ces deux événements ?
Parce que quand vous appuyez sur une touche, il se passe deux choses : vous enfoncez la touche (SDL_KEYDOWN
), puis vous la relâchez (SDL_KEYUP
). La SDL vous permet de traiter ces deux événements à part, ce qui sera bien pratique, vous verrez.
Pour le moment, nous allons nous contenter de traiter l'événement SDL_KEYDOWN
(appui de la touche) :
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN: /* Si appui sur une touche */
continuer = 0;
break;
}
}
Si on appuie sur une touche, le programme s'arrête. Testez, vous verrez !
Récupérer la touche
Savoir qu'une touche a été enfoncée c'est bien, mais savoir laquelle, c'est quand même mieux !
On peut obtenir la nature de la touche enfoncée grâce à une sous-sous-sous-variable (ouf) qui s'appelle event.key.keysym.sym
.
Cette variable contient la valeur de la touche qui a été enfoncée (elle fonctionne aussi lors d'un relâchement de la touche SDL_KEYUP
).
Il
y a une constante pour chacune des touches du clavier. Vous trouverez
cette liste dans la documentation de la SDL, que vous avez très
probablement téléchargée avec la bibliothèque quand vous avez dû
l'installer.
Si tel n'est pas le cas, je vous recommande fortement
de retourner sur le site de la SDL et d'y télécharger la documentation,
car elle est très utile.
Vous trouverez la liste des touches du clavier dans la section Keysym definitions
. Cette liste est trop longue pour être présentée ici, aussi je vous propose pour cela de consulter la documentation sur le web directement.
Bien entendu, la documentation est en anglais et donc… la liste aussi. Si vous voulez vraiment programmer, il est important d'être capable de lire l'anglais car toutes les documentations sont dans cette langue, et vous ne pouvez pas vous en passer !
Il y a deux tableaux dans cette liste : un grand (au début) et un petit (à la fin). Nous nous intéresserons au grand tableau.
Dans la première colonne vous avez la constante, dans la seconde la
représentation équivalente en ASCII et enfin, dans la troisième colonne,
vous avez une description de la touche.
Notez que certaines touches comme Maj
(ou Shift
) n'ont pas de valeur ASCII correspondante.
Prenons par exemple la touche Echap
(« Escape » en anglais). On peut vérifier si la touche enfoncée est Echap
comme ceci :
switch (event.key.keysym.sym)
{
case SDLK_ESCAPE: /* Appui sur la touche Echap, on arrête le programme */
continuer = 0;
break;
}
Voici une boucle d'événement complète que vous pouvez tester :
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
switch (event.key.keysym.sym)
{
case SDLK_ESCAPE: /* Appui sur la touche Echap, on arrête le programme */
continuer = 0;
break;
}
break;
}
}
Cette fois, le programme s'arrête si on appuie sur Echap
ou si on clique sur la croix de la fenêtre.
Auparavant, je vous avais demandé d'éviter de le faire car on ne savait pas comment arrêter un programme en plein écran (il n'y a pas de croix sur laquelle cliquer pour arrêter !).}
Exercice : diriger Zozor au clavier
Vous êtes maintenant capables de déplacer une image dans la fenêtre à l'aide du clavier !
C'est un exercice très intéressant qui va d'ailleurs nous permettre de
voir comment utiliser le double buffering et la répétition de touches.
De plus, ce que je vais vous apprendre là est la base de tous les jeux réalisés en SDL, donc ce n'est pas un exercice facultatif ! Je vous invite à le lire et à le faire très soigneusement.
Charger l'image
Pour
commencer, nous allons charger une image. On va faire simple : on va
reprendre l'image de Zozor utilisée dans le chapitre précédent.
Créez donc la surface zozor
, chargez l'image et rendez-la transparente (c'était un BMP, je vous le rappelle).
zozor = SDL_LoadBMP("zozor.bmp");
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));
Ensuite, et c'est certainement le plus important, vous devez créer une variable de type SDL_Rect
pour retenir les coordonnées de Zozor :
SDL_Rect positionZozor;
Je vous recommande d'initialiser les coordonnées, en mettant soit x = 0 et y = 0 (position en haut à gauche de la fenêtre), soit en centrant Zozor dans la fenêtre comme vous avez appris à le faire il n'y a pas si longtemps.
/* On centre Zozor à l'écran */
positionZozor.x = ecran->w / 2 - zozor->w / 2;
positionZozor.y = ecran->h / 2 - zozor->h / 2;
Si vous vous êtes bien débrouillés, vous devriez avoir réussi à afficher Zozor au centre de l'écran (fig. suivante).
J'ai choisi de mettre le fond en blanc cette fois (en faisant un SDL_FillRect
sur ecran
), mais ce n'est pas une obligation.
Schéma de la programmation événementielle
Quand
vous codez un programme qui réagit aux événements (comme on va le faire
ici), vous devez suivre la plupart du temps le même « schéma » de code.
Ce schéma est à connaître par cœur. Le voici :
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_TRUC: /* Gestion des événements de type TRUC */
case SDL_BIDULE: /* Gestion des événements de type BIDULE */
}
/* On efface l'écran (ici fond blanc) : */
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
/* On fait tous les SDL_BlitSurface nécessaires pour coller les surfaces à l'écran */
/* On met à jour l'affichage : */
SDL_Flip(ecran);
}
Voilà dans les grandes lignes la forme de la boucle principale d'un programme SDL.
On boucle tant qu'on n'a pas demandé à arrêter le programme.
On attend un événement (
SDL_WaitEvent
) ou bien on vérifie s'il y a un événement mais on n'attend pas qu'il y en ait un (SDL_PollEvent
). Pour le moment on se contente deSDL_WaitEvent
.On fait un (grand)
switch
pour savoir de quel type d'événement il s'agit (événement de typeTRUC
, de typeBIDULE
, comme ça vous chante !). On traite l'événement qu'on a reçu : on effectue certaines actions, certains calculs.Une fois sorti du
switch
, on prépare un nouvel affichage :
première chose à faire : on efface l'écran avec un
SDL_FillRect
. Si on ne le faisait pas, on aurait des « traces » de l'ancien écran qui subsisteraient, et forcément, ce ne serait pas très joli ;ensuite, on fait tous les blits nécessaires pour coller les surfaces sur l'écran ;
enfin, une fois que c'est fait, on met à jour l'affichage aux yeux de l'utilisateur, en appelant la fonction
SDL_Flip(ecran)
.
Traiter l'événement SDL_KEYDOWN
Voyons maintenant comment on va traiter l'événement SDL_KEYDOWN
.
Notre but est de diriger Zozor au clavier avec les flèches
directionnelles. On va donc modifier ses coordonnées à l'écran en
fonction de la flèche sur laquelle on appuie :
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_UP: // Flèche haut
positionZozor.y--;
break;
case SDLK_DOWN: // Flèche bas
positionZozor.y++;
break;
case SDLK_RIGHT: // Flèche droite
positionZozor.x++;
break;
case SDLK_LEFT: // Flèche gauche
positionZozor.x--;
break;
}
break;
}
Comment j'ai trouvé ces constantes ? Dans la doc' !
Je vous ai donné tout à l'heure un lien vers la page de la doc' qui
liste toutes les touches du clavier : c'est là que je me suis servi.
Ce qu'on fait là est très simple :
si on appuie sur la flèche « haut », on diminue l'ordonnée (y) de la position de Zozor d'un pixel pour le faire « monter ». Notez que nous ne sommes pas obligés de le déplacer d'un pixel, on pourrait très bien le déplacer de 10 pixels en 10 pixels ;
si on va vers le bas, on doit au contraire augmenter (incrémenter) l'ordonnée de Zozor (y) ;
si on va vers la droite, on augmente la valeur de l'abscisse (x) ;
si on va vers la gauche, on doit diminuer l'abscisse (x).
Et maintenant ?
En vous aidant du schéma de code donné précédemment, vous devriez être capables de diriger Zozor au clavier !
int main(int argc, char *argv[])
{
SDL_Surface *ecran = NULL, *zozor = NULL;
SDL_Rect positionZozor;
SDL_Event event;
int continuer = 1;
SDL_Init(SDL_INIT_VIDEO);
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
SDL_WM_SetCaption("Gestion des événements en SDL", NULL);
/* Chargement de Zozor */
zozor = SDL_LoadBMP("zozor.bmp");
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));
/* On centre Zozor à l'écran */
positionZozor.x = ecran->w / 2 - zozor->w / 2;
positionZozor.y = ecran->h / 2 - zozor->h / 2;
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_UP: // Flèche haut
positionZozor.y--;
break;
case SDLK_DOWN: // Flèche bas
positionZozor.y++;
break;
case SDLK_RIGHT: // Flèche droite
positionZozor.x++;
break;
case SDLK_LEFT: // Flèche gauche
positionZozor.x--;
break;
}
break;
}
/* On efface l'écran */
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
/* On place Zozor à sa nouvelle position */
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
/* On met à jour l'affichage */
SDL_Flip(ecran);
}
SDL_FreeSurface(zozor);
SDL_Quit();
return EXIT_SUCCESS;
}
Il est primordial de bien comprendre comment est composée la boucle principale du programme. Il faut être capable de la refaire de tête. Relisez le schéma de code que vous avez vu plus haut, au besoin.
Donc
en résumé, on a une grosse boucle appelée « Boucle principale du
programme ». Elle ne s'arrêtera que si on le demande en mettant le
booléen continuer
à 0.
Dans cette boucle, on récupère d'abord un événement à traiter. On fait un switch
pour déterminer de quel type d'événement il s'agit. En fonction de
l'événement, on effectue différentes actions. Ici, je mets à jour les
coordonnées de Zozor pour donner l'impression qu'on le déplace.
Ensuite, après le switch
vous devez mettre à jour votre écran comme suit.
Premièrement, vous effacez l'écran via un
SDL_FillRect
(de la couleur de fond que vous voulez).Ensuite, vous blittez vos surfaces sur l'écran. Ici, je n'ai eu besoin de blitter que Zozor car il n'y a que lui. Vous noterez, et c'est très important, que je blitte Zozor à
positionZozor
! C'est là que la différence se fait : si j'ai mis à jourpositionZozor
auparavant, alors Zozor apparaîtra à un autre endroit et on aura l'impression qu'on l'a déplacé !Enfin, toute dernière chose à faire :
SDL_Flip
. Cela ordonne la mise à jour de l'écran aux yeux de l'utilisateur.
On peut donc déplacer Zozor où l'on veut sur l'écran, maintenant (fig. suivante) !
Quelques optimisations
Répétition des touches
Pour l'instant, notre programme fonctionne mais on ne peut se déplacer que d'un pixel à la fois. Nous sommes obligés d'appuyer à nouveau sur les flèches du clavier si on veut encore se déplacer d'un pixel. Je ne sais pas vous, mais moi ça m'amuse moyennement de m'exciter frénétiquement sur la même touche du clavier juste pour déplacer le personnage de 200 pixels.
Heureusement, il y a SDL_EnableKeyRepeat
!
Cette fonction permet d'activer la répétition des touches. Elle fait en sorte que la SDL régénère un événement de type SDL_KEYDOWN
si une touche est maintenue enfoncée un certain temps.
Cette fonction peut être appelée quand vous voulez, mais je vous conseille de l'appeler de préférence avant la boucle principale du programme. Elle prend deux paramètres :
la durée (en millisecondes) pendant laquelle une touche doit rester enfoncée avant d'activer la répétition des touches ;
le délai (en millisecondes) entre chaque génération d'un événement
SDL_KEYDOWN
une fois que la répétition a été activée.
Le
premier paramètre indique au bout de combien de temps on génère une
répétition la première fois, et le second indique le temps qu'il faut
ensuite pour que l'événement se répète.
Personnellement, pour des raisons de fluidité, je mets la même valeur à ces deux paramètres, le plus souvent.
Essayez avec une répétition de 10 ms :
SDL_EnableKeyRepeat(10, 10);
Maintenant, vous pouvez laisser une touche du clavier enfoncée. Vous allez voir, c'est quand même mieux !
Travailler avec le double buffer
À partir de maintenant, il serait bon d'activer l'option de double buffering de la SDL.
Le double buffering est une technique couramment utilisée dans les jeux. Elle permet d'éviter un scintillement de l'image.
Pourquoi l'image scintillerait-elle ? Parce que quand vous dessinez à
l'écran, l'utilisateur « voit » quand vous dessinez et donc quand
l'écran s'efface. Même si ça va très vite, notre cerveau perçoit un
clignotement et c'est très désagréable.
La technique du double buffering consiste à utiliser deux « écrans » : l'un est réel (celui que l'utilisateur est en train de voir sur son moniteur), l'autre est virtuel (c'est une image que l'ordinateur est en train de construire en mémoire).
Ces deux écrans alternent : l'écran A est affiché pendant que l'autre (l'écran B) en « arrière-plan » prépare l'image suivante (fig. suivante).
Une fois que l'image en arrière-plan (l'écran B) a été dessinée, on intervertit les deux écrans en appelant la fonction SDL_Flip
(fig. suivante).
L'écran A part en arrière-plan préparer l'image suivante, tandis que l'image de l'écran B s'affiche directement et instantanément aux yeux de l'utilisateur. Résultat : aucun scintillement !
Pour réaliser cela, tout ce que vous avez à faire est de charger le mode vidéo en ajoutant le flag SDL_DOUBLEBUF
:
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
Vous n'avez rien d'autre à changer dans votre code.
Vous vous demandez peut-être pourquoi on a déjà utilisé SDL_Flip
auparavant sans le double buffering ?
En fait, cette fonction a deux utilités :
si le double buffering est activé, elle sert à commander « l'échange » des écrans qu'on vient de voir ;
si le double buffering n'est pas activé, elle commande un rafraîchissement manuel de la fenêtre. Cette technique est valable dans le cas d'un programme qui ne bouge pas beaucoup, mais pour la plupart des jeux, je recommande de l'activer.
Dorénavant, j'aurai toujours le double buffering activé dans mes codes source (ça ne coûte pas plus cher et c'est mieux : de quoi se plaint-on ?).
Voici le code source complet qui fait usage du double buffering et de la répétition des touches. Il est très similaire à celui que l'on a vu précédemment, on y a seulement ajouté les nouvelles instructions qu'on vient d'apprendre.
int main(int argc, char *argv[])
{
SDL_Surface *ecran = NULL, *zozor = NULL;
SDL_Rect positionZozor;
SDL_Event event;
int continuer = 1;
SDL_Init(SDL_INIT_VIDEO);
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); /* Double Buffering */
SDL_WM_SetCaption("Gestion des évènements en SDL", NULL);
zozor = SDL_LoadBMP("zozor.bmp");
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));
positionZozor.x = ecran->w / 2 - zozor->w / 2;
positionZozor.y = ecran->h / 2 - zozor->h / 2;
SDL_EnableKeyRepeat(10, 10); /* Activation de la répétition des touches */
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_UP:
positionZozor.y--;
break;
case SDLK_DOWN:
positionZozor.y++;
break;
case SDLK_RIGHT:
positionZozor.x++;
break;
case SDLK_LEFT:
positionZozor.x--;
break;
}
break;
}
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
SDL_Flip(ecran);
}
SDL_FreeSurface(zozor);
SDL_Quit();
return EXIT_SUCCESS;
}
La souris
Vous vous dites peut-être que gérer la souris est plus compliqué que le clavier ?
Que nenni ! C'est même plus simple, vous allez voir !
La souris peut générer trois types d'événements différents.
SDL_MOUSEBUTTONDOWN
: lorsqu'on clique avec la souris. Cela correspond au moment où le bouton de la souris est enfoncé.SDL_MOUSEBUTTONUP
: lorsqu'on relâche le bouton de la souris. Tout cela fonctionne exactement sur le même principe que les touches du clavier : il y a d'abord un appui, puis un relâchement du bouton.SDL_MOUSEMOTION
: lorsqu'on déplace la souris. À chaque fois que la souris bouge dans la fenêtre (ne serait-ce que d'un pixel !), un événementSDL_MOUSEMOTION
est généré !
Nous allons d'abord travailler avec les clics de la souris et plus particulièrement avec SDL_MOUSEBUTTONUP
. On ne travaillera pas avec SDL_MOUSEBUTTONDOWN
ici, mais vous savez de toute manière que c'est exactement pareil sauf
que cela se produit plus tôt, au moment de l'enfoncement du bouton de la
souris.
Nous verrons un peu plus loin comment traiter l'événement SDL_MOUSEMOTION
.
Gérer les clics de la souris
Nous allons donc capturer un événement de type SDL_MOUSEBUTTONUP
(clic de la souris) puis voir quelles informations on peut récupérer.
Comme d'habitude, on va devoir ajouter un case
dans notre switch
de test, alors allons-y gaiement :
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_MOUSEBUTTONUP: /* Clic de la souris */
break;
}
Jusque-là, pas de difficulté majeure.
Quelles informations peut-on récupérer lors d'un clic de la souris ? Il y en a deux :
le bouton de la souris avec lequel on a cliqué (clic gauche ? clic droit ? clic bouton du milieu ?) ;
les coordonnées de la souris au moment du clic (x et y).
Récupérer le bouton de la souris
On va d'abord voir avec quel bouton de la souris on a cliqué.
Pour cela, il faut analyser la sous-variable event.button.button
(non, je ne bégaie pas) et comparer sa valeur avec l'une des 5 constantes suivantes :
SDL_BUTTON_LEFT
: clic avec le bouton gauche de la souris ;SDL_BUTTON_MIDDLE
: clic avec le bouton du milieu de la souris (tout le monde n'en a pas forcément un, c'est en général un clic avec la molette) ;SDL_BUTTON_RIGHT
: clic avec le bouton droit de la souris ;SDL_BUTTON_WHEELUP
: molette de la souris vers le haut ;SDL_BUTTON_WHEELDOWN
: molette de la souris vers le bas.
On va faire un test simple pour vérifier si on a fait un clic droit avec la souris. Si on a fait un clic droit, on arrête le programme (oui, je sais, ce n'est pas très original pour le moment mais ça permet de tester) :
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_MOUSEBUTTONUP:
if (event.button.button == SDL_BUTTON_RIGHT) /* On arrête le programme si on a fait un clic droit */
continuer = 0;
break;
}
Vous pouvez tester, vous verrez que le programme s'arrête si on fait un clic droit.
Récupérer les coordonnées de la souris
Voilà une information très intéressante : les coordonnées de la souris au moment du clic !
On les récupère à l'aide de deux variables (pour l'abscisse et l'ordonnée) : event.button.x
et event.button.y
.
Amusons-nous un petit peu : on va blitter Zozor à l'endroit du clic de la souris.
Compliqué ? Pas du tout ! Essayez de le faire, c'est un jeu d'enfant !
Voici la correction :
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_MOUSEBUTTONUP:
positionZozor.x = event.button.x;
positionZozor.y = event.button.y;
break;
}
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor); /* On place Zozor à sa nouvelle position */
SDL_Flip(ecran);
}
Ça
ressemble à s'y méprendre à ce que je faisais avec les touches du
clavier. Là, c'est même encore plus simple : on met directement la
valeur de x
de la souris dans positionZozor.x
, et de même pour y
.
Ensuite on blitte Zozor à ces coordonnées-là, et voilà le travail (fig. suivante) !
Petit exercice très simple : pour le moment, on déplace Zozor quel que soit le bouton de la souris utilisé pour le clic. Essayez de ne déplacer Zozor que si on fait un clic gauche avec la souris. Si on fait un clic droit, arrêtez le programme.
Gérer le déplacement de la souris
Un déplacement de la souris génère un événement de type SDL_MOUSEMOTION
.
Notez bien qu'on génère autant d'événements que l'on parcourt de pixels
pour se déplacer ! Si on bouge la souris de 100 pixels (ce qui n'est
pas beaucoup), il y aura donc 100 événements générés.
Mais ça ne fait pas beaucoup d'événements à gérer pour notre ordinateur, tout ça ?
Pas du tout : rassurez-vous, il en a vu d'autres !
Bon, que peut-on récupérer d'intéressant ici ?
Les coordonnées de la souris, bien sûr ! On les trouve dans event.motion.x
et event.motion.y
.
On va placer Zozor aux mêmes coordonnées que la souris, là encore. Vous allez voir, c'est rudement efficace et toujours aussi simple !
while (continuer)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_MOUSEMOTION:
positionZozor.x = event.motion.x;
positionZozor.y = event.motion.y;
break;
}
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor); /* On place Zozor à sa nouvelle position */
SDL_Flip(ecran);
}
Bougez votre Zozor à l'écran. Que voyez-vous ?
Il suit naturellement la souris où que vous alliez. C'est beau, c'est rapide, c'est fluide (vive le double buffering).
Quelques autres fonctions avec la souris
Nous allons voir deux fonctions très simples en rapport avec la souris, puisque nous y sommes. Ces fonctions vous seront très probablement utiles bientôt.
Masquer la souris
On peut masquer le curseur de la souris très facilement.
Il suffit d'appeler la fonction SDL_ShowCursor
et de lui envoyer un flag :
SDL_DISABLE
: masque le curseur de la souris ;SDL_ENABLE
: réaffiche le curseur de la souris.
Par exemple :
SDL_ShowCursor(SDL_DISABLE);
Le curseur de la souris restera masqué tant qu'il sera à l'intérieur de la fenêtre.
Masquez de préférence le curseur avant la boucle principale du
programme. Pas la peine en effet de le masquer à chaque tour de boucle,
une seule fois suffit.
Placer la souris à un endroit précis
On peut placer manuellement le curseur de la souris aux coordonnées que l'on veut dans la fenêtre.
On utilise pour cela SDL_WarpMouse
qui prend pour paramètres les coordonnées x et y où le curseur doit être placé.
Par exemple, le code suivant place la souris au centre de l'écran :
SDL_WarpMouse(ecran->w / 2, ecran->h / 2);
Les événements de la fenêtre
La fenêtre elle-même peut générer un certain nombre d'événements :
lorsqu'elle est redimensionnée ;
lorsqu'elle est réduite en barre des tâches ou restaurée ;
lorsqu'elle est active (au premier plan) ou lorsqu'elle n'est plus active ;
lorsque le curseur de la souris se trouve à l'intérieur de la fenêtre ou lorsqu'il en sort.
Commençons par étudier le premier d'entre eux : l'événement généré lors du redimensionnement de la fenêtre.
Redimensionnement de la fenêtre
Par défaut, une fenêtre SDL ne peut pas être redimensionnée par l'utilisateur.
Je vous rappelle que pour changer ça, il faut ajouter le flag SDL_RESIZABLE
dans la fonction SDL_SetVideoMode
:
ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_RESIZABLE);
Une fois que vous avez ajouté ce flag, vous pouvez redimensionner la fenêtre. Lorsque vous faites cela, un événement de type SDL_VIDEORESIZE
est généré.
Vous pouvez récupérer :
la nouvelle largeur dans
event.resize.w
;la nouvelle hauteur dans
event.resize.h
.
On peut utiliser ces informations pour faire en sorte que notre Zozor soit toujours centré dans la fenêtre :
case SDL_VIDEORESIZE:
positionZozor.x = event.resize.w / 2 - zozor->w / 2;
positionZozor.y = event.resize.h / 2 - zozor->h / 2;
break;
Visibilité de la fenêtre
L'événement SDL_ACTIVEEVENT
est généré lorsque la visibilité de la fenêtre change.
Cela peut être dû à de nombreuses choses :
la fenêtre est réduite en barre des tâches ou restaurée ;
le curseur de la souris se trouve à l'intérieur de la fenêtre ou en sort ;
la fenêtre est active (au premier plan) ou n'est plus active.
Étant donné le nombre de raisons qui peuvent avoir provoqué cet événement, il faut impérativement regarder dans des variables pour en savoir plus.
event.active.gain
: indique si l'événement est un gain (1) ou une perte (0). Par exemple, si la fenêtre est passée en arrière-plan c'est une perte (0), si elle est remise au premier plan c'est un gain (1).event.active.state
: c'est une combinaison de flags indiquant le type d'événement qui s'est produit. Voici la liste des flags possibles :SDL_APPMOUSEFOCUS
: le curseur de la souris vient de rentrer ou de sortir de la fenêtre.
Il faut regarder la valeur de event.active.gain
pour savoir si elle est rentrée (gain = 1) ou sortie (gain = 0) de la fenêtre ;
SDL_APPINPUTFOCUS
: l'application vient de recevoir le focus du clavier ou de le perdre. Cela signifie en fait que votre fenêtre vient d'être mise au premier plan ou en arrière-plan.
Encore une fois, il faut regarder la valeur de event.active.gain
pour savoir si la fenêtre a été mise au premier plan (gain = 1) ou en arrière-plan (gain = 0) ;
SDL_APPACTIVE
: l'applicaton a été icônifiée, c'est-à-dire réduite dans la barre des tâches (gain = 0), ou remise dans son état normal (gain = 1).
Vous suivez toujours ? Il faut bien comparer les valeurs des deux sous-variables gain
et state
pour savoir exactement ce qui s'est produit.
Tester la valeur d'une combinaison de flags
event.active.state
est une combinaison de flags. Cela signifie que dans un événement, il
peut se produire deux choses à la fois (par exemple, si on réduit la
fenêtre dans la barre des tâches, on perd aussi le focus du clavier et
de la souris).
Il va donc falloir faire un test un peu plus compliqué qu'un simple…
if (event.active.state == SDL_APPACTIVE)
Pourquoi est-ce plus compliqué ?
Parce
que c'est une combinaison de bits. Je ne vais pas vous faire un cours
sur les opérations logiques bit à bit ici, ça serait un peu trop pour ce
cours et vous n'avez pas nécessairement besoin d'en connaître
davantage.
Je vais vous proposer un code prêt à l'emploi qu'il faut
utiliser pour tester si un flag est présent dans une variable sans
rentrer dans les détails.
Pour tester par exemple s'il y a eu un changement de focus de la souris, on doit écrire :
if ((event.active.state & SDL_APPMOUSEFOCUS) == SDL_APPMOUSEFOCUS)
Il n'y a pas d'erreur. Attention, c'est précis : il faut un seul &
et deux =
, et il faut bien utiliser les parenthèses comme je l'ai fait.
Cela fonctionne de la même manière pour les autres événements. Par exemple :
if ((event.active.state & SDL_APPACTIVE) == SDL_APPACTIVE)
Tester l'état et le gain à la fois
Dans la pratique, vous voudrez sûrement tester l'état et le gain à la fois. Vous pourrez ainsi savoir exactement ce qui s'est passé.
Supposons que vous ayez un jeu qui fait faire beaucoup de calculs à l'ordinateur. Vous voulez que le jeu se mette en pause automatiquement lorsque la fenêtre est réduite, et qu'il se relance lorsque la fenêtre est restaurée. Cela évite que le jeu continue pendant que le joueur n'est plus actif et cela évite aussi au processeur de faire trop de calculs par la même occasion.
Le code ci-dessous met en pause le jeu en activant un booléen pause
à 1. Il remet en marche le jeu en désactivant le booléen à 0.
if ((event.active.state & SDL_APPACTIVE) == SDL_APPACTIVE)
{
if (event.active.gain == 0) /* La fenêtre a été réduite */
pause = 1;
else if (event.active.gain == 1) /* La fenêtre a été restaurée */
pause = 0;
}
Je vous laisse faire d'autres tests pour les autres cas (par exemple, vérifier si le curseur de la souris est à l'intérieur ou à l'extérieur de la fenêtre). Vous pouvez — pour vous entraîner — faire bouger Zozor vers la droite lorsque la souris rentre dans la fenêtre, et le faire bouger vers la gauche lorsqu'elle en sort.
En résumé
Les événements sont des signaux que vous envoie la SDL pour vous informer d'une action de la part de l'utilisateur : appui sur une touche, mouvement ou clic de la souris, fermeture de la fenêtre, etc.
Les événements sont récupérés dans une variable de type
SDL_Event
avec la fonctionSDL_WaitEvent
(fonction bloquante mais facile à gérer) ou avec la fonctionSDL_PollEvent
(fonction non bloquante mais plus complexe à manipuler).Il faut analyser la sous-variable
event.type
pour connaître le type d'événement qui s'est produit. On le fait en général dans unswitch
.Une fois le type d'événement déterminé, il est le plus souvent nécessaire d'analyser l'événement dans le détail. Par exemple, lorsqu'une touche du clavier a été enfoncée (
SDL_KEYDOWN
) il faut analyserevent.key.keysym.sym
pour connaître la touche en question.Le double buffering est une technique qui consiste à charger l'image suivante en tâche de fond et à l'afficher seulement une fois qu'elle est prête. Cela permet d'éviter des scintillements désagréables à l'écran.