Apprenez à programmer en C !
Last updated on Thursday, September 25, 2014
  • 4 semaines
  • Moyen

Free online content available in this course.

Paperback available in this course

eBook available in this course.

Certificate of achievement available at the end this course

Got it!

La gestion des événements

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

La croix permettant de fermer la fenêtre

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.

  1. Dès qu'il y a un événement, la fonction SDL_WaitEvent renvoie cet événement dans event.

  2. On analyse le type d'événement grâce à un switch. Le type de l'événement se trouve dans event.type

  3. On teste à l'aide de case dans le switch le type de l'événement. Pour le moment, on ne teste que l'événement SDL_QUIT (demande de fermeture du programme), car c'est le seul qui nous intéresse.<liste>

  4. 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éen continuer à 0. Au prochain tour de boucle, la condition sera fausse et donc la boucle s'arrêtera. Le programme s'arrêtera ensuite.

  5. 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).

Zozor centré dans la fenêtre

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.

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

    2. On fait un (grand) switch pour savoir de quel type d'événement il s'agit (événement de type TRUC, de type BIDULE, comme ça vous chante !). On traite l'événement qu'on a reçu : on effectue certaines actions, certains calculs.

    3. Une fois sorti du switch, on prépare un nouvel affichage :

<liste type="1">

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

  2. ensuite, on fait tous les blits nécessaires pour coller les surfaces sur l'écran ;

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

  1. Premièrement, vous effacez l'écran via un SDL_FillRect (de la couleur de fond que vous voulez).

  2. 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 à jour positionZozor auparavant, alors Zozor apparaîtra à un autre endroit et on aura l'impression qu'on l'a déplacé !

  3. 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) !

Zozor en balade

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

En double buffering, l'image suivante est préparée en tâche de fond

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

SDL_Flip intervertit les écrans pour afficher la nouvelle image

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énement SDL_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) !

Zozor apparaît à l'endroit du clic

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 :<liste>

    • 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 fonction SDL_WaitEvent (fonction bloquante mais facile à gérer) ou avec la fonction SDL_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 un switch.

  • 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 analyser event.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.

Example of certificate of achievement
Example of certificate of achievement