• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 13/03/2017

Les évènements avec la SDL 2.0

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Depuis le début du tutoriel, les évènements sont gérés automatiquement et sans contrôle sur le clavier et la souris. Nous pourrions continuer à coder ainsi, sans se préoccuper de rien mais on va profiter de ce petit chapitre pour créer une classe à part entière qui s'occupera de gérer tout ça pour nous. Avec elle, nous pourrons retrouver facilement les évènements qui nous intéressent comme les touches ou les boutons de souris qui sont utilisés, ...

Bien entendu, notre code devra respecter la règle de l'encapsulation. Il faudra donc faire attention à protéger les attributs pour ne pas accéder directement aux évènements.

Différences entre la SDL 1.2 et 2.0

Nous avons déjà eu l’occasion de constater les différences qu'ils existaient entre l'ancienne et la nouvelle version de la SDL, notamment lors de la création de fenêtre. Nous allons voir maintenant celles qui concernent les évènements car ils sont indispensables au développement d'un jeu vidéo. D'ailleurs, on les a déjà utilisés lorsque nous voulions savoir si une fenêtre devait se fermer ou pas.

En effet, à chaque tour de la boucle principale, on vérifie si l'évènement SDL_WINDOWEVENT_CLOSE est déclenché, ce qui nous permet de continuer ou d’arrêter la boucle :

// Boucle principale

bool terminer(false);

while(!terminer)
{
    // Gestion des évènements

    SDL_PollEvent(&m_evenements);

    if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;
}

Ce bout de code est pratique mais il ne nous permet pas de gérer la pression des touches. On pourrait par exemple appuyer sur la touche ECHAP pour terminer la boucle, ou utiliser un de souris.

Si vous vous rappelez du chapitre sur les évènements de la SDL 1.2 dans le cours de M@téo, vous vous souvenez surement de la façon de gérer les touches du clavier. Par exemple, pour gérer la pression des touches T ou ECHAP il fallait faire ceci :

// Structure

SDL_Event evenements;


// Récupération d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.sym)
        {
            case SDLK_T:
                ....
            break;

            case SDLK_ESCAPE:
                ....
            break;
        }

    break;
}

Voilà comment on gérait les évènements avec la SDL 1.2, et la bonne nouvelle c'est qu'avec la version 2.0 ça se passe de la même façon. :p

Seulement deux choses vont être modifiées :

  • Premièrement, on ne vérifie plus le champ sym, mais le champ scancode. Ce qui donne au final : m_evenements.key.keysym.scancode.

  • Ensuite, le nom des touches ne commencent plus par SDLK_* mais par SDL_SCANCODE_*. Ce qui nous donne ici : SDL_SCANCODE_T et SDL_SCANCODE_ESCAPE.

Si on modifie le code précédent on trouve :

// Structure

SDL_Event evenements;


// Récupération d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.scancode)
        {
            case SDL_SCANCODE_T:
                ....
            break;

            case SDL_SCANCODE_ESCAPE:
                ....
            break;
        }

    break;
}

Aucun gros changement. ;)

Je ne peux vous parler plus explicitement des scancodes mais sachez simplement que mis à part le nom des constantes, le code restera le même pour nous. Si on veut aller plus loin au niveau du fonctionnement de la SDL on remarque que les scancodes sont une modification majeur de la librairie car on change de norme pour identifier les touches en interne. Mais bon dans le fond on s'en moque un peu. :p

Concernant la souris, il n'y a rien à signaler. Il n'y a pas de grands changements par rapport à la SDL 1.2. La gestion se fera de la même façon pour nous.

La classe Input

La classe Input

Maintenant que nous avons vu les différences qu'il existait entre les deux versions de la SDL, nous allons passer à l'implémentation d'une classe qui gèrera tous les évènements toute seule. :)

Ça veut dire qu'on va remplacer la structure SDL_Event par une classe ?

Alors oui et non, car la structure SDL_Event existera toujours quelque part. Mais au lieu de l'utiliser dans la classe SceneOpenGL, on l’utilisera dans une classe à part. Grâce à ça, les évènements seront protégés par la règle de l'encapsulation et seront aussi plus simples à utiliser. Seul l'appel à une méthode nous permettra de savoir si telle ou telle touche est enfoncée. :)

On appellera cette classe : la classe Input.

En informatique, les inputs sont des systèmes qui permettent d'apporter à l'ordinateur des actions venant de l'utilisateur. Elles peuvent prendre la forme d'un mouvement de souris, d'une pression sur une touche du clavier, d'un geste de doigt sur un écran tactile, ... D'où le nom Input pour la classe qui gèrera tous ces évènements. :)

Voici sa déclaration :

#ifndef DEF_INPUT
#define DEF_INPUT

// Include

#include <SDL2/SDL.h>


// Classe

class Input
{
    public:

    Input();
    ~Input();


    private:
};

#endif
Les attributs

La classe Input possèdera plusieurs attributs :

  • Une structure SDL_Event : pour récupérer les évènements SDL

  • Un tableau de booléensm_touches[] : regroupant toutes les touches du clavier

  • Un tableau de booléensm_boutonsSouris[] : regroupant tous les boutons de la souris

  • Deux entiers (int) : représentant la position (x, y) du pointeur de la souris

  • Deux entiers : représentant la position relative (x, y) du pointeur

  • Un booléenm_terminer : qui permettra se savoir quand la fenêtre devra être fermer

On va directement s’intéresser aux tableaux de booléens. Chaque case de ces tableaux pourra prendre deux valeurs : si une touche, ou un bouton, est enfoncé(e) elle prendra la valeur true, dans le cas contraire ce sera la valeur false. Au début du programme, toutes les cases sont initialisées à false, vu que l'on appuie sur aucune des touches.

Pour définir la taille de ces tableaux on pourrait croire qu'il faille utiliser l'allocation dynamique - vu qu'il existe plusieurs types de claviers - mais pas du tout. En effet, la SDL gère toute seule les différents claviers. Elle est capable de nous fournir une seule et unique constante (SDL_NUM_SCANCODES) représentant le nombre maximal de touches d'un clavier, et par conséquent la taille du tableau m_touches[]. Grâce à elle, on pourra gérer toutes les touches automatiquement. ;)

Pour la souris, ce sera un peu différent. Il n'existe pas de constante que l'on peut donner au tableau. En revanche, on peut savoir que la SDL ne peut gérer que 7 boutons pour la souris. On donnera donc une taille de 7 cases au tableau m_boutonsSouris[].

Si on regroupe les attributs on a :

#ifndef DEF_INPUT
#define DEF_INPUT

// Include

#include <SDL2/SDL.h>


// Classe

class Input
{
    public:

    Input();
    ~Input();


    private:

    SDL_Event m_evenements;
    bool m_touches[SDL_NUM_SCANCODES];
    bool m_boutonsSouris[8];

    int m_x;
    int m_y;
    int m_xRel;
    int m_yRel;

    bool m_terminer;
};

#endif

Avec ces attributs, on pourra récupérer tous les évènements dont on aura besoin. On verra en dernière partie de ce chapitre quelques méthodes qui nous permettront d'accéder à ces attributs.

Le constructeur et le destructeur

Maintenant qu'on a défini la classe Input, on doit s'occuper d'initialiser ses attributs avec le constructeur. Dans cette classe, on aura besoin que d'un constructeur : la constructeur par défaut.

Input();

Il s'occupera de mettre tous les attributs soit à 0 soit à false.

Son implémentation commence par l'initialisation de tous les attributs (sauf les tableaux) :

Input::Input() : m_x(0), m_y(0), m_xRel(0), m_yRel(0), m_terminer(false)
{

}

Pour initialiser les tableaux de bool, il faudra juste faire une boucle pour affecter la valeur false aux différentes cases :

Input::Input() : m_x(0), m_y(0), m_xRel(0), m_yRel(0), m_terminer(false)
{
    // Initialisation du tableau m_touches[]

    for(int i(0); i < SDL_NUM_SCANCODES; i++)
        m_touches[i] = false;


    // Initialisation du tableau m_boutonsSouris[]

    for(int i(0); i < 8; i++)
        m_boutonsSouris[i] = false;
}

Voilà pour le constructeur. :)

On profite également de cette partie pour créer le destructeur. Même si on ne met rien dedans, on l'implémente quand même. C'est une bonne habitude à prendre. ;)

Input::~Input()
{

}
La boucle d'évènements

Pour la partie qui va suivre, je vais vous demander toute votre attention. Nous allons voir la notion la plus importante du chapitre : la boucle d'évènements.

Euh c'est quoi cette boucle ? Ça a un rapport avec les évènements SDL ?

Oui tout à fait. Il s'agit d'une boucle qui récupère tous les évènements à un moment donné, quelque soit leur nature (touche de clavier, mouvement de souris, ...).

Si je vous parle de cette boucle, c'est parce qu'à l'heure actuelle nous avons un gros problème : nous ne sommes pas capables de gérer plusieurs évènements à la fois. Je vais vous donner un exemple pour que vous compreniez bien le problème.

Dans votre vie, vous avez probablement déjà joué à un jeu-vidéo, comme Mario par exemple. Les jeux Mario sont en général des jeux de plateformes où notre petit plombier doit sauter et avancer sur des plateformes, tuyaux et autres éléments de l'environnement.

La plupart du temps, sans même vous en rendre compte, vous appuyez sur plusieurs touches (ou boutons) en même temps. Par exemple, un personnage saute et avance en même temps, il se passe deux actions : sauter et avancer. On peut même rajouter des actions à ça : tirer, sprinter, ... Dans ce cas, le programme doit gérer 4 évènements en même temps.

A l'heure actuelle, notre problème c'est qu'avec la SDL nous ne pouvons gérer qu'un seul et unique évènement à la fois. Si nous programmions un Mario avec la SDL, le personnage ne pourrait même pas avancer et sauter en même, ce qui est un peu contraignant ... :p

Le problème vient de la SDL alors ? Elle est un peu nulle cette librairie :(

Non pas du tout, car ça ne vient pas vraiment d'elle mais de notre code. Vous vous souvenez du switch au début du chapitre ? Celui-ci :

// Structure

SDL_Event evenements;


// Attente d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.scancode)
        {
            case SDL_SCANCODE_T:
                ....
            break;

            case SDL_SCANCODE_ESCAPE:
                ....
            break;
        }

    break;
}

Avec ce code, on ne peut gérer qu'un seul évènement par tour de boucle OpenGL.

Pour régler ce problème, on va se servir d'une petite astuce de la fonction SDL_PollEvent(). En effet, si on s’intéresse à son prototype on peut remarquer une chose :

int SDL_PollEvent(SDL_Event *event);

La fonction SDL_PollEvent() renvoie une valeur (un int).

On ne s'est jamais servi de cette valeur (on ne savait même pas qu'elle existait d'ailleurs) et pourtant c'est elle qui va régler notre problème. Cet integer retourné peut prendre deux valeurs :

  • Soit 1 : ce qui veut dire qu'il reste encore des évènements à capturer dans la file d'attente

  • Soit 0 : ce qui veut dire qu'il n'y en a plus

Pour capturer tous les évènements, il suffit de piéger la fonction SDL_PollEvent() dans une boucle. Tant qu'il reste quelque chose à récupérer dans la file d'attente, la fonction retourne 1, donc on continue de récupérer les évènements jusqu'à qu'il n'y en ait plus.
Grâce à cette astuce, on sera capable de gérer plusieurs touches/boutons en même temps. Le petit Mario de tout à l'heure pourra donc avancer, sauter, sprinter, tirer des boules de feu ... et tout ça en même temps. ;)
D'où la boucle d'évènements dont je vous ai parlée tout à l'heure. :p

Au niveau du code, il suffit d'enfermer la fonction SDL_PollEvent() dans une boucle while. Tant que la fonction retourne 1 on continue la boucle :

// Structure

SDL_Event evenements;


// Boucle d'évènements

while(SDL_PollEvent(&evenements) == 1)
{
    // Switch sur le type d'évènement

    switch(evenements.type)
    {
        case SDL_KEYDOWN:
        
            // Gestion des touches

            switch(evenements.key.keysym.scancode)
            {
                case SDL_SCANCODE_T:
                    ....
                break;

                case SDL_SCANCODE_ESCAPE:
                    ....
                break;
            }

        break;
    }
}

On peut même simplifier le while en enlevant la condition ' == 1 ' :

// Boucle d'évènements

while(SDL_PollEvent(&evenements))
{
    ...
}

Vous savez maintenant ce qu'est la boucle d'évènements. C'est une boucle qui permet de capturer toutes les actions qui concernent le clavier, la souris, ... en même temps.

Nous allons maintenant revenir à la classe Input pour intégrer cette boucle dans une méthode. :)

La méthode updateEvenements()

La méthode qui va implémenter cette boucle s'appellera updateEvenements(). Voici son prototype :

void updateEvenements();

Commençons cette méthode en codant la fameuse boucle while qui prendra en "condition" la valeur retournée par la fonction SDL_PollEvent().

D'ailleurs, on donnera à cette dernière l'adresse de l'attribut m_evenements - car oui la structure SDL_Event existe toujours quelque part, elle n'a pas disparue. :p

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {

    }
}

La suite du code ne changera pas beaucoup par rapport à la SDL 1.2. On met en place un gros switch qui va tester la valeur du champ m_evenements.type . On commencera par gérer les conditions relatives au clavier.

Deux cas doivent être gérés par le clavier :

  • Lorsqu'une touche est enfoncée, l'évènement SDL_KEYDOWN est déclenché (même chose qu'avec la SDL 1.2).

  • Lorsqu'une touche est relâchée, l'évènement SDL_KEYUP est déclenché (même chose aussi)

Dans les deux cas, on actualisera l'état de la touche correspondante dans le tableau m_touches[]. D'ailleurs, on utilisera le champ m_evenements.key.keysym.scancode qui nous servira d'indice pour retrouver la bonne case :

// Actualisation de l'état de la touche

m_touches[m_evenements.key.keysym.scancode] = true;

Le gros avantage du tableau de booléens c'est que, quelque soit la touche enfoncée, nous n'avons besoin que d'une seule ligne de code pour actualiser son état. Que ce soit la touche A, B, C, ESPACE, ... une seule ligne suffit. :D

En définitif, pour gérer les touches du clavier nous devons :

  • Tester le champ m_evenements.type pour savoir si une touche a été enfoncée ou relâchée

  • Actualiser la touche dans le tableau de booléens avec une valeur true ou un false

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            default:
            break;
        }
    }
}

Grâce à ce code, toutes les touches du clavier peuvent être mises à jour en même temps (enfin, en une boucle :p ).

Gestion de la souris

Nous avons déjà fait la plus grosse part du travail, on sait désormais gérer toutes les touches du clavier. Nous allons maintenant passer à la gestion de la souris.

La bonne nouvelle, c'est que les boutons de la souris se gèrent de la même façon que les touches du clavier. Il suffit de rajouter deux cases au switch : un pour les boutons qui seront pressés (ce case utilisera la constante SDL_MOUSEBUTTONDOWN) et un autre pour les boutons qui seront relâchés (il utilisera la constante SDL_MOUSEBUTTONUP).

Pour récupérer l'indice du bouton dans le tableau m_boutonsSouris[], nous n'utiliserons pas le champ m_evenements.key.keysym.scancode, car ce champ ne concerne uniquement que le clavier. A la place, nous utiliserons le champ evenements.button.button qui lui est réservé à la souris.

On ajoute donc les deux cases suivants au switch :

// Cas de pression sur un bouton de la souris

case SDL_MOUSEBUTTONDOWN:

    m_boutonsSouris[m_evenements.button.button] = true;

break;


// Cas du relâchement d'un bouton de la souris

case SDL_MOUSEBUTTONUP:

    m_boutonsSouris[m_evenements.button.button] = false;

break;

Ça c'était la partie des boutons. Maintenant il faut s'occuper des mouvements de la souris.

Lorsque la souris est en mouvement, un évènement est déclenché dans la SDL. Cet évènement va permettre de mettre à jour les coordonnées (x, y) du pointeur ainsi que ses coordonnées relatives. Nous allons pouvoir détecter ces mouvements grâce à la constante SDL_MOUSEMOTION. Lorsque cet évènement sera déclenché, on mettra à jour les attributs qui concernent les coordonnées. On prendra les nouvelles valeurs dans le champ m_evenements.motion :

// Cas d'un mouvement de souris

case SDL_MOUSEMOTION:

    m_x = m_evenements.motion.x;
    m_y = m_evenements.motion.y;

    m_xRel = m_evenements.motion.xrel;
    m_yRel = m_evenements.motion.yrel;

break;
Fermeture de la fenêtre

Il ne reste plus qu'un seul évènement à gérer dans cette boucle : le cas de la fermeture de la fenêtre (la croix rouge en haut à droite sous Windows). Depuis le début du tutoriel, on utilise cet évènement dans la boucle principale d'OpenGL pour savoir si on doit quitter le programme :

if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
    terminer = true;

Il faut maintenant enlever ce code pour le migrer dans la méthode updateEvenements(). N'oubliez pas que c'est elle et uniquement elle qui doit mettre à jour tous les évènements.

Nous allons donc ajouter un nouveau case dans le switch pour gérer cette fermeture. On utilisera la constante SDL_WINDOWSEVENT pour savoir si cet évènement a été déclenché. N'oubliez pas de modifier la variable terminer en m_terminer, car on met à jour non plus une variable mais un attribut :

// Cas de la fermeture de la fenêtre

case SDL_WINDOWEVENT:

    if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        m_terminer = true;

break;

Si réunie tous ces cases dans la méthode, on trouve :

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            // Cas de pression sur un bouton de la souris

            case SDL_MOUSEBUTTONDOWN:

                m_boutonsSouris[m_evenements.button.button] = true;

            break;


            // Cas du relâchement d'un bouton de la souris

            case SDL_MOUSEBUTTONUP:

                m_boutonsSouris[m_evenements.button.button] = false;

            break;


            // Cas d'un mouvement de souris

            case SDL_MOUSEMOTION:

                m_x = m_evenements.motion.x;
                m_y = m_evenements.motion.y;

                m_xRel = m_evenements.motion.xrel;
                m_yRel = m_evenements.motion.yrel;

            break;


            // Cas de la fermeture de la fenêtre

            case SDL_WINDOWEVENT:

                if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
                    m_terminer = true;

            break;


            default:
            break;
        }
    }
}
Problème des coordonnées relatives

Cette méthode est presque complète, il reste juste un petit point à régler qui concerne les coordonnées relatives. Ces coordonnées représentent la différence entre la position actuelle et l'ancienne position, elles seront très utiles dans le chapitre sur la caméra.

Le problème avec ces coordonnées c'est que : s'il n'y a aucun évènement alors elles ne sont pas mises à jour, elles conservent donc leurs anciennes valeurs. Ce qui veut dire que le programme considère que la souris continue de bouger, même si elle est inactive.

Pour régler ce problème, on va ré-initialiser les coordonnées avec la valeur 0 au début de la méthode. Ne vous inquiétez pas, si les coordonnées doivent être mises à jour avec de vraies valeurs elles le seront dans le switch.

Grâce à cette astuce, nous n'aurons aucun problème de mouvement fictif. On rajoute donc ces deux lignes de code au début de la méthode :

// Pour éviter des mouvements fictifs de la souris, on réinitialise les coordonnées relatives

m_xRel = 0;
m_yRel = 0;

Notre méthode donne donc au final :

void Input::updateEvenements()
{
    // Pour éviter des mouvements fictifs de la souris, on réinitialise les coordonnées relatives

    m_xRel = 0;
    m_yRel = 0;


    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            // Cas de pression sur un bouton de la souris

            case SDL_MOUSEBUTTONDOWN:

                m_boutonsSouris[m_evenements.button.button] = true;

            break;


            // Cas du relâchement d'un bouton de la souris

            case SDL_MOUSEBUTTONUP:

                m_boutonsSouris[m_evenements.button.button] = false;

            break;


            // Cas d'un mouvement de souris

            case SDL_MOUSEMOTION:

                m_x = m_evenements.motion.x;
                m_y = m_evenements.motion.y;

                m_xRel = m_evenements.motion.xrel;
                m_yRel = m_evenements.motion.yrel;

            break;


            // Cas de la fermeture de la fenêtre

            case SDL_WINDOWEVENT:

                if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
                    m_terminer = true;

            break;


            default:
            break;
        }
    }
}

Voilà ! Maintenant nous sommes capables de mettre à jour tous nos évènements, peu importe leur nombre ils seront tous gérés simultanément par la classe Input. :D

La méthode terminer()

Il ne reste plus qu'une chose à ajouter dans la classe : une méthode qui permet de dire si oui ou non l'utilisateur veut quitter le programme. Jusqu'à maintenant, on utilisait un booléen terminer pour fermer la fenêtre, mais maintenant ce booléen se trouve dans la classe Input.

Or, avec la règle de l'encapsulation on ne peut pas directement vérifier cet attribut. Il nous faut donc coder un accesseur pour récupérer la valeur du booléen.

Le prototype est terriblement simple. :p

bool terminer() const;

Son implémentation l'est tout autant :

bool Input::terminer() const
{
    return m_terminer;
}
Modification de la classe SceneOpenGL

On passe maintenant à l'utilisation de la classe Input dans notre scène 3D. Pour cela, on va remplacer l'ancien code de gestion des évènements par nos nouvelles méthodes.

On commence donc par déclarer un objet de type Input dans la classe SceneOpenGL qui viendra remplacer l'ancien attribut m_evenements :

#include "Input.h"

class SceneOpenGL
{
    public:

    /* *** Méthodes *** */


    private:

    /* *** Attributs *** */

    // Objet Input pour la gestion des évènements

    Input m_input;
}

On n'oublie pas de l'initialiser dans le constructeur :

SceneOpenGL::SceneOpenGL(std::string titreFenetre, int largeurFenetre, int hauteurFenetre) : m_titreFenetre(titreFenetre), m_largeurFenetre(largeurFenetre),
                                                                                             m_hauteurFenetre(hauteurFenetre), m_fenetre(0), m_contexteOpenGL(0), m_input()

Dans la boucle principale, on ne vérifie donc plus le booléen terminer mais la valeur retournée par la méthode terminer() :

// Boucle principale

while(!m_input.terminer())
{
    /* ***** RENDU ***** */
}

Enfin, on supprime notre ancien code de gestion d'évènements que l'on remplace par la méthode updateEvenements() de la classe Input :

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();


    /* *** Rendu *** */
}

Voilà pour la classe Input ! Grâce à elle, tous nos évènements seront mis à jour automatiquement. :)

Maintenant que l'on a fait cela, on va pouvoir passer à l'implémentation de plusieurs méthodes "indispensables" qui vont nous permettre de connaitre l'état du clavier, de la souris, ... Nous devons passer par ces méthodes pour respecter la règle de l'encapsulation, sans quoi nous serions obligés d’accéder directement aux attributs pour avoir nos valeurs.

Les méthodes indispensables

Méthodes

Cette dernière partie sera consacrée à une série de méthodes qui seront utilisées tout au long du tuto. Je vais vous donner une petite liste des fonctionnalités que l'on va implémenter. Il nous faudra une méthode pour :

  • Savoir si une touche est enfoncée

  • Savoir si un bouton de la souris est enfoncé

  • Savoir si le pointeur de la souris a bougé

  • Récupérer les coordonnées (x, y) du pointeur

  • Récupérer les coordonnées relatives (x, y) du pointeur

  • Cacher le pointeur

  • Capturer le pointeur dans la fenêtre

Houla on va devoir coder tout ça ? o_O

Et bien oui. Mais sachez pour vous rassurer que la plus grosse méthode de cette liste ne fera que 4 lignes. :p

Ces méthodes ne se contenteront que de faire une simple action (renvoyer un booléen par exemple) avec parfois des bloc if else. Rien de compliqué ne vous inquiétez pas.

La méthode getTouche()

On commence par la méthode la plus importante : celle qui permet de savoir si une touche a été enfoncée (ou non). En gros, on va coder un getter sur le tableau m_touches[]. :p Elle prendra en paramètre une variable de type SDL_Scancode correspondant à la touche demandée :

bool getTouche(const SDL_Scancode touche) const;

Cette méthode renverra true si la touche est pressée ou false si elle ne l'est pas. N'oubliez pas de la déclarer en tant que méthode constante, vu que l'on ne modifie aucun attribut :

bool Input::getTouche(const SDL_Scancode touche) const
{
    return m_touches[touche];
}

Fini. :)

Bonus : On va utiliser cette nouvelle méthode dès maintenant. Désormais, je veux que chacune de vos fenêtres SDL de chaque projet que vous ferez puisse se fermer en appuyant sur la touche ECHAP.

Comment feriez-vous ça avec le getter que l'on vient de coder ? Je vous laisse réfléchir un peu. :p

Vous avez trouvé ?

 

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
       break;


    /* *** Rendu *** */
}

Il suffit simplement d'appeler le getter getTouche() avec le scancode SDL_SCANCODE_ESCAPE. Si le getter retourne true, on casse la boucle avec le mot-clef break.

Bonus 2 : Pour savoir si deux touches sont enfoncées simultanément, il suffira d'utiliser un bloc if avec les deux touches demandées :

// Est-ce que les touches Z et D sont pressées ?

if(m_input.getTouche(SDL_SCANCODE_Z) && m_input.getTouche(SDL_SCANCODE_D))
{
    ...
}
La méthode getBoutonSouris()

La méthode, ou plutôt le getter, getBoutonSouris() fera la même chose que getTouche(). C'est-à-dire qu'elle permettra de savoir si un bouton spécifié est enfoncé ou pas. La seule différence avec la méthode précédente c'est que cette fois-ci, on lui donnera en paramètre non pas un SDL_Scancode mais une variable de type Uint8 correspondant au bouton demandé (En réalité, ce sera une constante comme avec les scancodes).

bool getBoutonSouris(const Uint8 bouton) const;

Elle renverra l'état du bouton demandé dans le tableau m_boutonsSouris[] :

bool Input::getBoutonSouris(const Uint8 bouton) const
{
    return m_boutonsSouris[bouton];
}
La méthode mouvementSouris()

La méthode suivante nous permettra de savoir si le pointeur de la souris a bougé. Grâce à elle, nous pourrons déclencher une action dès que la souris bougera. On appellera cette méthode : mouvementSouris(), elle renverra un booléen.

bool mouvementSouris() const;

Pour détecter un mouvement de souris il suffit de comparer la position relative du pointeur grâce aux attributs m_xRel et m_yRel. Si ces deux attributs sont égals à 0, alors le pointeur n'a pas bougé. Si en revanche ils ont une valeur non nulle, alors c'est que le pointeur a bougé.

bool Input::mouvementSouris() const
{
    if(m_xRel == 0 && m_yRel == 0)
        return false;

    else
        return true;
}
Les Getters du pointeur

Les méthodes suivantes sont des getters, elles renvoient chacune un attribut qui concerne la position du pointeur. Vous savez déjà comment fonctionne un getter, je vous épargne donc les explications. :)

Voici leur constructeur :

// Getters

int getX() const;
int getY() const;

int getXRel() const;
int getYRel() const;

Implémentation :

// Getters concernant la position du curseur

int Input::getX() const
{
    return m_x;
}

int Input::getY() const
{
    return m_y;
}

int Input::getXRel() const
{
    return m_xRel;
}

int Input::getYRel() const
{
    return m_yRel;
}
La méthode afficherPointeur()

Les méthodes suivantes peuvent vous paraitre inutiles pour le moment, mais dès que nous utiliserons une caméra mobile pour comprendrez vite leur utilité.

La méthode afficherPointeur() va permettre d'afficher ou de cacher le pointeur à l'écran. Elle prendra en paramètre un booléen qui :

  • S'il est à true : le pointeur est caché

  • S'il est à false : le pointeur est affiché

void afficherPointeur(bool reponse) const;

Cette méthode va faire appel à une fonction SDL pour afficher ou cacher le pointeur :

int SDL_ShowCursor(int toggle);

Cette fonction prend en paramètre une constante qui peut être égale soit à SDL_ENABLE soit à SDL_DISABLE, ce paramètre se comporte un peu comme un booléen. On ne s'occupera pas de la valeur retournée par la fonction.

Voici l'implémentation de la méthode :

void Input::afficherPointeur(bool reponse) const
{
    if(reponse)
        SDL_ShowCursor(SDL_ENABLE);

    else
        SDL_ShowCursor(SDL_DISABLE);
}
La méthode capturerPointeur()

La dernière méthode va permettre d'utiliser le Mode Relatif de la Souris. Ce mode permet de piéger le pointeur dans la fenêtre, il ne pourra pas en sortir. C'est utile voir obligatoire dans un jeu-vidéo par exemple. On appellera cette méthode : capturerPointeur().

Faites attention à cette méthode, si vous l'appelez dans votre code vous devrez prévoir quelque chose pour fermer votre programme. Utilisez un getTouche() par exemple. Si vous ne faites pas ça, vous ne pourrez plus fermer votre fenêtre. Vous devrez alors utiliser les combinaisons CTRL + MAJ + SUPPR ou WINDOWS + TAB (si vous êtes sous Windows) pour pouvoir ré-utiliser la souris.

Le prototype de la méthode est le suivant :

void capturerPointeur(bool reponse) const;

Elle se comportera exactement de la même manière que la méthode précédente. Elle utilisera la fonction SDL_SetRelativeMousseMode() de la SDL pour activer le mode relatif de la souris :

int SDL_SetRelativeMouseMode(SDL_bool enabled);

Cette fonction prendra en paramètre un SDL_bool. Sa valeur peut être soit SDL_TRUE soit SDL_FALSE. Voici son implémentation :

void Input::capturerPointeur(bool reponse) const
{
    if(reponse)
        SDL_SetRelativeMouseMode(SDL_TRUE);

    else
        SDL_SetRelativeMouseMode(SDL_FALSE);
}

Voilà pour l'implémentation des méthodes "indispensables". :)

Télécharger : Code Source C++ du Chapitre 9

Exercices

Énoncés

Je vais vous donner des exercices assez simples pour vous habituez à utiliser votre nouvelle classe. Ceux-ci seront axés sur la pseudo-animation du cube (celle qui permettait de le faire tourner). Vous n'aurez besoin d'utiliser que les méthodes qui sont déjà codées, vous n'aurez donc pas besoin d'en rajouter.

Il est préférable que vous incluiez le code relatif aux évènements avant la fonction glClear(). En effet, vous verrez dans le futur que nous aurons parfois de faire plusieurs affichages de la même scène. Pour économiser du temps de calcul les inputs doivent se gérer avant ces affichages. La seule chose qui peut se trouver après la fonction c'est la méthode rotate() car elle ne fait pas partie des évènements mais des matrices (et donc de l'affichage).

Autant prendre les bonnes habitudes dès maintenant, surtout que ça ne mange pas de pain. :)

Exercice 1 : L'animation initiale du cube permettait de le faire pivoter selon un angle et par rapport à l'axe Y. Votre objectif est de dé-automatiser cette rotation pour qu'elle réponde aux touches du clavier Flèche Gauche et Droite (SDL_SCANCODE_LEFT et SDL_SCANCODE_RIGHT). Vous pouvez vous aider du code que l'on avait utilisé dans le chapitre 7 :

// Incrémentation de l'angle

angle += 4.0;

if(angle >= 360.0)
    angle -= 360.0;


// Sauvegarde de la matrice

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

    modelview = rotate(modelview, angle, vec3(0, 1, 0));


    // Affichage du cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Exercice 2 : Même exercice que précédemment sauf que l'axe concerné n'est plus Y mais X et les touches sont Flèche Haut et Bas (SDL_SCANCODE_UP et SDL_SCANCODE_DOWN).

Exercice 3 : Rassemblez les deux animations précédentes pour le même cube. C'est-à-dire que vous devez pouvoir appuyer sur les 4 touches directionnelles pour le faire pivoter selon l'axe XetY (Petit indice : vous aurez besoin de deux angles).

Solution

Exercice 1 :

Le but de l'exercice était de "manualiser" la rotation en fonction des touches Haut et Bas. Pour cela, il fallait simplement appeler la méthode getTouche() de l'objet m_input deux fois avec les constantes SDL_SCANCODE_LEFT et SDL_SCANCODE_RIGHT :

// Gestion des évènements

....


// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{

}


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{

}


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

....

Une fois la vérification des touches pressées faite, il ne manquait plus qu'à copier le code de rotation. Bien entendu, il fallait additionner l'angle dans un cas et le soustraire dans l'autre :

// Gestion des évènements

....


// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{
    // Modification de l'angle

    angle -= 4.0;


    // Limitation

    if(angle >= 360.0)
        angle -= 360.0;
}


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{
    // Modification de l'angle

    angle += 4.0;


    // Limitation

    if(angle >= 360.0)
        angle -= 360.0;
}


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

....

Enfin, il ne manquait plus qu'à appeler la méthode rotate() avec l'angle de rotation en paramètre et afficher le cube. Le tout encadré par la sauvegarde et la restauration de la matrice modelview. ;)

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

    modelview = rotate(modelview, angle, vec3(0, 1, 0));


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Exercice 2 :

Cet exercice reprend le même principe que le précédent sauf qu'il fallait, d'une part, modifier les constantes utilisées :

// Rotation du cube vers le bas

if(m_input.getTouche(SDL_SCANCODE_DOWN))
{
    // Modification de l'angle

    angle -= 4.0;


    // Limitation

    if(angle >= 360.0)
        angle -= 360.0;
}


// Rotation du cube vers le haut

if(m_input.getTouche(SDL_SCANCODE_UP))
{
    // Modification de l'angle

    angle += 4.0;


    // Limitation

    if(angle >= 360.0)
        angle -= 360.0;
}

Et d'autre part, il fallait également modifier l'axe de rotation de la méthode rotate() :

// Rotation du repère

modelview = rotate(modelview, angle, vec3(0, 1, 0));

Exercice 3 :

Le dernier exercice était un poil plus dur que les deux autres, il fallait utiliser deux angles et gérer 4 touches du clavier.

Vous pouviez donner n'importe quel nom aux angles :

// Angles de la rotation

float angleX(0.0);
float angleY(0.0);

La gestion des 4 touches n'était pas compliquée du moment que vous utilisiez le bon angle avec le bon axe de rotation :

// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{
    angleY -= 5;

    if(angleY > 360)
        angleY -= 360;
}


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{
    angleY += 5;

    if(angleY < -360)
        angleY += 360;
}
// Rotation du cube vers le haut

if(m_input.getTouche(SDL_SCANCODE_UP))
{
    angleX -= 5;

    if(angleX > 360)
        angleX -= 360;
}


// Rotation du cube vers le bas

if(m_input.getTouche(SDL_SCANCODE_DOWN))
{
    angleX += 5;

    if(angleX < -360)
        angleX += 360;
}

J'ai divisé le code en deux pour le rendre plus lisible. Il fallait évidemment tout coder au même endroit. ;)

Enfin, pour gérer les deux rotations, il fallait simplement appeler la méthode rotate() deux fois. Un appel prenait en compte l'angle angleX et l'autre l'angle angleY :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

    modelview = rotate(modelview, angleY, vec3(0, 1, 0));
    modelview = rotate(modelview, angleX, vec3(1, 0, 0));


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Encore une fois, n'oubliez pas d'utiliser la sauvegarde/restauration lorsque vous faites une transformation. :)

Notre classe Input est maintenant complète et prête à l'emploi. Avec elle, nous pourrons gérer tous nos évènements simplement. Nous serons capable de gérer la pression de plusieurs touches, des mouvements de la souris, etc. Et tout ça de manière simultanée.
Pour utiliser les évènements, il suffira juste de passer un objet de type Input aux modèles que l'on veut afficher.
De plus, l'avantage d'avoir une classe à part entière c'est que l'on pourra ajouter de nouvelles méthodes au fur et à mesure du tutoriel. Nous n'aurons pas besoin de revenir en arrière pour modifier le code, il suffira d'ajouter ce qu'il faut dans la classe Input.

Après ce chapitre repos, je vous propose de passer à un chapitre hyper méga important qui concerne les Textures avec OpenGL ! :D

Exemple de certificat de réussite
Exemple de certificat de réussite