Les événements

Ce cinquième chapitre du tutorial ne va pas vous faire afficher de jolis trucs à l'écran (et oui encore !), en effet, nous allons programmer une classe qui permet de gérer les événements. Autrement dit, une classe qui va dans un premier temps, attendre que le joueur appuie sur une ou plusieurs touches de son clavier et par conséquent déclencher une action. C'est plus ou moins comme ça qu'on va concevoir les commandes de notre jeu.

Irrlicht a déjà tout prévu !

Il existe une classe dans Irrlicht qui “écoute” et qui est capable de dire quel événement vient de se produire. Alors nous, dans ce chapitre nous allons nous concentrer sur l'événement clavier. Mais sachez qu'il existe d'autres types d'événements : souris, joystick etc... Ce que nous allons faire, c'est plus où moins ce qui est expliqué dans le tutorial officiel d'Irrlicht sur les événements. Car il faut avouer que le tutorial donne une méthode qui convient parfaitement à la structure de notre jeu.

Il s'agit de dériver la classe IEventReceiver. Comme son nom l'indique, cette classe reçoit les événements pour qu'on puisse les traiter. Comme on ne peut pas modifier la classe directement, on va la dériver. Nous allons appeler notre classe fille EventManager, pour “Gestionnaire d'événements”. Nous allons donc créer un fichier EventManager.h et commencer l'écriture du code :

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5.  
  6. using namespace irr;
  7.  
  8. class EventManager : public IEventReceiver {
  9.   public:
  10.     EventManager();
  11. };
  12.  
  13. #endif
  14.  

Puis le fichier source EventManager.cpp :

Classe EventManager (EventManager.cpp)
  1. #include "EventManager.h"
  2.  
  3. using namespace irr;
  4.  
  5. EventManager::EventManager() {
  6.   
  7. }

Nous allons ensuite créer un lien entre le jeu et le gestionnaire d'événements car c'est dans le jeu qu'on fera des tests pour savoir si il se passe quelque chose. Ce lien, on le créer par la présence du gestionnaire dans la classe Game. On va donc déclarer un objet directement dans Game.h, comme pour driver et device, en static et on n'oublie pas non plus l'instruction include de EventManager.h :

Classe Game (Game.h)
  1. #ifndef GAME_H
  2. #define GAME_H
  3.  
  4. #include <irr/irrlicht.h>
  5. #include "Scene.h"
  6. #include "EventManager.h"
  7.  
  8. using namespace irr;
  9.  
  10. class Game {
  11.   private:
  12.     Scene *currentScene;
  13.  
  14.   public:
  15.     static IrrlichtDevice *device;
  16.     static video::IVideoDriver *driver;
  17.     static EventManager event;
  18.  
  19.     Game();
  20.     void run();
  21.     ~Game();
  22. };
  23.  
  24. #endif

Si on compile le projet, on se rendra compte qu'une erreur survient à la ligne 17. En fait, la classe IEventReceiver contient une méthode OnEvent qui est abstraite. Cela signifie qu'elle est comme incomplète. Pour remédier à cela, on doit redéfinir la méthode en la réecrivant. Dans EventManager.h :

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5.  
  6. using namespace irr;
  7.  
  8. class EventManager : public IEventReceiver {
  9.   public:
  10.     EventManager();
  11.     virtual bool OnEvent(const SEvent &event) {
  12.     
  13.     }
  14. };
  15.  
  16. #endif

Vous voyez l'ajout de la méthode OnEvent, désormais si on compile, mis à part quelques warnings (qu'on corrigera), l'exécutable est créé sans problème. Il faut savoir que quand l'utilisateur appuie sur une touche du clavier, bouge sa souris ou incline son joystick, la méthode OnEvent est appelée automatiquement. Notre objectif maintenant est d'écrire le contenu de OnEvent de manière à ce qu'on traite différents types d'événements.

Le type EET_KEY_INPUT_EVENT

Un nom barbare mais c'est bien avec cette constante qu'on va tester le type d'événement. Vous apercevez dans la signature de la méthode OnEvent un objet de type SEvent et qui est constant, il s'agit même d'une référence à cet objet comme en témoigne la présence du &. L'objet event dispose de quelques propriétés dont EventType qui contient la valeur correspondant au type d'événement déclenché. Je peux donc comparer cette valeur à EET_KEY_INPUT_EVENT et être en mesure de détecter un événement clavier.

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5.  
  6. using namespace irr;
  7.  
  8. class EventManager : public IEventReceiver {
  9.   public:
  10.     EventManager();
  11.     virtual bool OnEvent(const SEvent &event) {
  12.       if(event.EventType == EET_KEY_INPUT_EVENT) {
  13.         
  14.       }
  15.     }
  16. };
  17.  
  18. #endif

On vient de voir que event contenait une propriété appelée EventType. Mais cette structure SEvent contient des sous-structures, une pour chaque type d'événement. Pour notre cas, nous allons nous servir de KeyInput qui est de type SKeyInput. Avec KeyInput, on a accès à des propriétés intéressantes : Key tout d'abord qui contient le code de la touche qui a été enfoncée ou relâchée, PressedDown qui dit que la touche est pressée ou non, on a aussi Control et Shift qui nous disent si ces deux touches spéciales sont enfoncées au moment de l'événement.

Communiquer avec le jeu

La technique préconisée par les créateurs d'Irrlicht est de créer une méthode qui va dire si une touche particulière est enfoncée ou non. Regardez le schéma suivant, si dans une scène on a besoin de tester qu'une touche est enfoncée (par exemple pour déplacer le personnage principal), la fonction isKeyDown sera appelée 60 fois par seconde tant que la touche est appuyée. Cette fonction prendra un paramètre : la touche qu'on souhaite tester (ici KEY_LEFT) et ira interroger un tableau nommé keyDown. En fonction de la touche testée, la fonction ira chercher la valeur correspondant à la touche et la renverra, d'où le true qu'on reçoit à la sortie de la méthode.

Fonctionnement de la méthode isKeyDown

Nous allons donc d'abord créer le tableau keyDown dans la partie privée de la classe, il aura pour taille le nombre de codes de touches disponibles, on l'obtient par la constante KEY_KEY_CODES_COUNT :

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5. #include <iostream>
  6.  
  7. using namespace irr;
  8. using namespace std;
  9.  
  10. class EventManager : public IEventReceiver {
  11.   private:
  12.     bool keyDown[KEY_KEY_CODES_COUNT];
  13.  
  14.   public:
  15.     EventManager();
  16.     bool isKeyDown(EKEY_CODE code);
  17.     virtual bool OnEvent(const SEvent &event) {
  18.       if(event.EventType == EET_KEY_INPUT_EVENT) {
  19.         
  20.       }
  21.     }
  22. };
  23.  
  24. #endif

Nous allons initialiser les cellules du tableau keyDown, c'est-à-dire, le remplir de valeurs par défaut. Pour cela, on effectue une boucle dans le constructeur de EventManager pour remplir le tableau avec des valeurs false :

Classe EventManager (EventManager.cpp)
  1. #include "EventManager.h"
  2.  
  3. using namespace irr;
  4.  
  5. EventManager::EventManager() {
  6.   for(int i = 0; i < KEY_KEY_CODES_COUNT; i++) {
  7.     keyDown[i] = false;
  8.   }
  9. }

Pour la méthode isKeyDown, elle retournera juste la valeur d'une cellule précise qu'on demandera via son paramètre code (type EKEY_CODE).

Classe EventManager (EventManager.cpp)
  1. #include "EventManager.h"
  2.  
  3. using namespace irr;
  4.  
  5. EventManager::EventManager() {
  6.   for(int i = 0; i < KEY_KEY_CODES_COUNT; i++) {
  7.     keyDown[i] = false;
  8.   }
  9. }
  10.  
  11. bool EventManager::isKeyDown(EKEY_CODE code) {
  12.   return keyDown[code];
  13. }

On a plus qu'à terminer la méthode OnEvent pour qu'en cas d'événement clavier, la valeur de la cellule, dont le code de la touche est le même que event.KeyInput.Key, prenne la valeur true et toutes les autres false. On va se servir de event.KeyInput.Key qui contient le code de la touche qui agit et de event.KeyInput.PressedDown qui va renvoyer true ou false selon que c'est appuyé ou relâché.

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5. #include <iostream>
  6.  
  7. using namespace irr;
  8. using namespace std;
  9.  
  10. class EventManager : public IEventReceiver {
  11.   private:
  12.     bool keyDown[KEY_KEY_CODES_COUNT];
  13.  
  14.   public:
  15.     EventManager();
  16.     bool isKeyDown(EKEY_CODE code);
  17.     virtual bool OnEvent(const SEvent &event) {
  18.       if(event.EventType == EET_KEY_INPUT_EVENT) {
  19.         keyDown[event.KeyInput.Key] = event.KeyInput.PressedDown;
  20.       }
  21.     }
  22. };
  23.  
  24. #endif

Seul soucis : lors de la compilation, on a encore plein de warnings, ils sont dûs au fait que la méthode OnEvent dans sa définition dans la classe IEventReceiver doit renvoyer une valeur. D'ailleurs quand on l'a redéfinit, on a spécifié son type de retour : bool. On placera donc la ligne return false; à la fin de la méthode

Méthode OnEvent de EventManager (EventManager.h)
  1. virtual bool OnEvent(const SEvent &event) {
  2.   if(event.EventType == EET_KEY_INPUT_EVENT) {
  3.     keyDown[event.KeyInput.Key] = event.KeyInput.PressedDown;
  4.   }
  5.   return false;
  6. }

Testons un événement pour voir !

Voyons comment on utilise la méthode qu'on a fraîchement créé. Tout d'abord, sachez que la classe EventManager ne fonctionnera pas tant que vous n'aurez pas dit à Irrlicht qu'il faut l'utiliser ! Pour ce faire, rappelez vous de l'appel à createDevice qu'on a vu au chapitre précédent, dans la classe Game.

Constructeur de Game (Game.cpp)
  1. Game::Game() {
  2.   device = createDevice(video::EDT_OPENGL,core::dimension2d<s32>(848,480),32,false,true,false,0);
  3.  
  4.   currentScene = new Scene();
  5. }

Regardez le dernier paramètre de la méthode : 0. On avait dit à l'époque qu'il s'agissait d'une référence à un objet de type IEventReceiver. Le soucis c'est qu'on a uniquement défini l'objet en question, en static dans la classe Game. On a besoin de le déclarer comme les objets driver et device, juste avant le code du constructeur :

Partie de la classe Game (Game.cpp)
  1. #include "Game.h"
  2. #include "EventManager.h"
  3. #include <iostream>
  4. #include <irr/irrlicht.h>
  5.  
  6. using namespace irr;
  7. using namespace std;
  8.  
  9. class Game;
  10.  
  11. IrrlichtDevice *Game::device;
  12. video::IVideoDriver *Game::driver;
  13. EventManager Game::event;
  14.  
  15. Game::Game() {
  16.   cout << "In Game constructor" << endl;
  17.  
  18.   device = createDevice(video::EDT_OPENGL,core::dimension2d<s32>(768,480),32,false,true,false,&event);
  19.   driver = device->getVideoDriver();
  20.  
  21.   currentScene = new Scene();
  22. }

On n'oublie pas l'include de EventManager.h, on a déclaré l'objet (qui n'est pas un pointeur, attention) et on peut donc le mettre en paramètre de createDevice (avec le & pour passer son adresse).

Il est maintenant possible de programmer une action selon un événement simple, on affichera dans la console "Event OK" tant qu'on appuiera sur la touche gauche. On peut placer par exemple cette partie du code dans la boucle principale de la scène, dans la fonction updateAll() :

Partie de la classe Scene (Scene.cpp)
  1. void Scene::updateAll() {
  2.   for(seIterator = sceneElementList.begin(); seIterator != sceneElementList.end(); seIterator++) {
  3.     seIterator->second->update();
  4.   }
  5.   if(Game::event.isKeyDown(KEY_LEFT)) {
  6.     cout << "Event OK" << endl;
  7.   }
  8. }
Action d'un événement simple

Une fois par pression...

Un cas plus difficile mais qui va beaucoup servir : comment faire pour que l'action ne s'exécute qu'une seule fois quand l'événement se produit ? Nous allons voir une méthode basée sur ce qu'on vient de faire : l'utilisation d'un deuxième tableau dans EventManager, nommé KeyOnce. Nous l'ajoutons à la classe EventManager et nous créons une nouvelle méthode nommée isKeyDownOnce :

Classe EventManager (EventManager.h)
  1. #ifndef EVENTMANAGER_H
  2. #define EVENTMANAGER_H
  3.  
  4. #include <irr/irrlicht.h>
  5. #include <iostream>
  6.  
  7. using namespace irr;
  8. using namespace std;
  9.  
  10. class EventManager : public IEventReceiver {
  11.   private:
  12.     bool keyDown[KEY_KEY_CODES_COUNT];
  13.     bool keyOnce[KEY_KEY_CODES_COUNT];
  14.  
  15.   public:
  16.     EventManager();
  17.     virtual bool OnEvent(const SEvent &event) {
  18.       if(event.EventType == EET_KEY_INPUT_EVENT) {
  19.         keyDown[event.KeyInput.Key] = event.KeyInput.PressedDown;
  20.       }
  21.       return false;
  22.     }
  23.     bool isKeyDown(EKEY_CODE code);
  24.     bool isKeyDownOnce(EKEY_CODE code);
  25. };
  26.  
  27. #endif

Le code de la fonction isKeyDownOnce est un peu difficile à expliquer de manière claire (si vous avez des questions, n'hésitez pas à me contacter ou poser la question sur le forum). Cette fonction va renvoyer true une seule fois, quand keyOnce[code] sera true. On va fixer cette valeur à true si elle est à false et si on est en train de presser la touche (isKeyDown). Et on fixera à false la valeur de cette cellule quand elle vaudra true et que la touche n'est pas pressée, ainsi ça réinitialise l'action quand on relâche la touche et on pourra donc reprovoquer l'action si on rappuie sur la touche.

En C++, voila ce que donne cette fonction :

Méthode isKeyDownOnce de EventManager (EventManager.cpp)
  1. bool EventManager::isKeyDownOnce(EKEY_CODE code) {
  2.   if(!keyOnce[code]) {
  3.     if(isKeyDown(code)) {
  4.       keyOnce[code] = true;
  5.       return true;
  6.     }
  7.   } else {
  8.     if(!isKeyDown(code)) {
  9.       keyOnce[code] = false;
  10.     }
  11.   }
  12.   return false;
  13. }

Testons maintenant les deux fonctions, isKeyDownOnce seule et ensuite combinée avec isKeyDown :

Méthode updateAll de Scene (Scene.cpp)
  1. void Scene::updateAll() {
  2.   for(seIterator = sceneElementList.begin(); seIterator != sceneElementList.end(); seIterator++) {
  3.     seIterator->second->update();
  4.   }
  5.   if(Game::event.isKeyDown(KEY_LEFT)) {
  6.     cout << "Left Forever" << endl;
  7.   }
  8.   if(Game::event.isKeyDownOnce(KEY_RIGHT)) {
  9.     cout << "Right Once" << endl;
  10.   }
  11.   if(Game::event.isKeyDown(KEY_CONTROL) && Game::event.isKeyDownOnce(KEY_UP)) {
  12.     cout << "Ctrl+Up Once" << endl;
  13.   }
  14. }

Testez par vous-même les combinaisons en appuyant sur Droite, Gauche et Control + Haut, vous verrez alors les utilisations possibles de ces deux fonctions bien pratiques pour programmer les commandes du jeu.