Intégrer Irrlicht au projet

Enfin ! Nous allons nous servir du coeur du jeu : le moteur 3D, nous allons savoir comment interfacer Irrlicht avec notre structure déjà programmée pour concevoir ce jeu en se facilitant la vie.

Histoires de librairies

Irrlicht, ça ne fonctionne pas après un claquement de doigts, il y a quelques règles à respecter. Tout d'abord, rappatrier le fichier .dll qui se trouve dans l'archive d'Irrlicht que vous avez téléchargée.

Irrlicht

Ce fichier dll devra se trouver dans le même répertoire que le fichier exécutable de votre jeu, c'est-à-dire, pendant vos tests, dans le répertoire bin/Debug de votre projet. De toute façon si vous oubliez ça, votre application utilisant Irrlicht vous le dira ("ouin il me manque une dll").

La deuxième chose sera d'inclure "irrlicht.h" à votre code à chaque fois que c'est nécessaire. Si vous utilisez une classe du moteur, un typedef ou autre, vous devrez faire une inclusion de ce header (le reste, il s'en charge).

Inclure Irrlicht
  1. #include <irr/irrlicht.h>

Device et video driver

Un jeu vidéo comme son nom l'indique, c'est avant tout visuel, graphique... vidéo quoi ! C'est pourquoi la plupart des fonctions d'Irrlicht dépendront de deux objets très importants : IrrlichtDevice et IVideoDriver. Non je ne parle pas chinois, ce sont deux classes dont on va utiliser les fonctionnalités très souvent. Petits exemples : modifier le titre de la fenêtre, activer le brouillard, récupérer une texture etc... Nous allons voir tout cela en détail.

Avec l'architecture objet qu'on a mis en place pour notre jeu, un problème s'est posé : comment accéder tout le temps aux deux objets IrrlichtDevice et IVideoDriver ? En effet dans énormément de cas nous avons besoin de faire appel à ces deux objets. La solution réside dans la fonctionnalité static de C++. En plaçant ses deux objets dans la portée public de Game et définis en static, on va pouvoir y faire appel n'importe où dans notre programme.

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

A la ligne 5, comme prévu nous avons inclut le header d'Irrlicht, utilisé son namespace principal à la ligne 7, puis on déclare deux objets static à la ligne 14 et 15 pour le "device" et le "driver".

Créer le device

La toute première chose à faire au démarrage du jeu, c'est de créer l'objet IrrlichtDevice. Nous avons déclaré un pointeur nommé device, il faut maintenant l'initialiser dans le constructeur de Game (c'est en gros le démarrage du jeu).

Classe Game (morceau de 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. }

On utilise la fonction createDevice() du namespace irr pour initialiser l'objet device, détaillons ses 7 paramètres :

Et si on testait en compilant le projet ? ... Malheur ! Une erreur de compilation, ce n'est pas dû à createDevice mais au fait que device est un objet static. Il y a donc quelque chose à faire avant : déclarer la classe Game et ses objets.

Classe Game (morceau de Game.cpp)
  1. #include "Game.h"
  2. #include <iostream>
  3. #include <irr/irrlicht.h>
  4.  
  5. using namespace irr;
  6. using namespace std;
  7.  
  8. class Game;
  9. IrrlichtDevice *Game::device;
  10.  
  11. Game::Game() {
  12.   device = createDevice(video::EDT_OPENGL,core::dimension2d<s32>(848,480),32,false,true,false,0);
  13.  
  14.   currentScene = new Scene();
  15. }

On déclare donc la classe (ici c'est un prototypage) à la ligne 8 puis on déclare son membre static device pour pouvoir l'utiliser dans ce fichier.

Si on compile, tout se passe bien. On pourra appeler Game::device n'importe où dans notre programme, on peut faire de même avec driver, déclarons-le juste après device.

Video driver et boucle principale

Classe Game (morceau de Game.cpp)
  1. #include "Game.h"
  2. #include <iostream>
  3. #include <irr/irrlicht.h>
  4.  
  5. using namespace irr;
  6. using namespace std;
  7.  
  8. class Game;
  9.  
  10. IrrlichtDevice *Game::device;
  11. video::IVideoDriver *Game::driver;
  12.  
  13. Game::Game() {
  14.   device = createDevice(video::EDT_OPENGL,core::dimension2d<s32>(848,480),32,false,true,false,0);
  15.   driver = device->getVideoDriver();
  16.  
  17.   currentScene = new Scene();
  18. }

On a déclaré le driver, on peut donc l'initialiser avec la méthode getVideoDriver() du device. C'est tout simple et le driver video est initialisé. La prochaine grosse étape est de s'occuper de la boucle principale du jeu, une partie très importante qu'on va peaufiner tout de suite.

Actuellement, notre boucle principale (méthode run() de Game) s'exécute à l'infinie, ce qui est pas très bon, surtout en ce qui concerne la mesure du temps. Car dans notre jeu, on doit bien effectuer des mouvements, des rotations etc... tout cela ça se fait avec une vitesse et la vitesse est dépendante du temps, ça c'est de la physique je vous apprend rien. On a donc besoin de mesurer avec précision le temps et de réduire le nombre d'exécutions de la boucle principale. Effectivement là notre boucle s'exécute énormément de fois par seconde et le nombre dépend de la rapidité du processeur. l'intérêt pour nous ici est donc de limiter ce nombre à quelque chose de précis et ce, quel que soit l'ordinateur sur lequel va tourner le jeu.

L'idée est de placer un méchanisme d'attente à l'intérieur de la boucle principale pour attendre un certain temps avant de passer à la prochaine boucle. Ce mécanisme c'est ni plus ni moins qu'une boucle elle-même. Pour savoir si un certain temps est passé, on va devoir mémoriser le temps actuel plus le temps qu'on souhaite attendre et boucler jusqu'à ce que le temps actuel est supérieur ou égal à ce qu'on a mémorisé. En clair si on veut attendre 16 millisecondes, on stocke d'abord le temps actuel de l'ordinateur en millisecondes + 16 dans une variable qu'on appellera waitTime, et on boucle tant que le temps actuel de l'ordinateur en milliseconde est inférieur à waitTime. Pour récupérer le temps actuel en millisecondes, une fonction d'Irrlicht le permet : getRealTime().

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   u32 waitTime, now;
  3.   
  4.   while(device->run()) {
  5.     currentScene->updateAll();
  6.     
  7.     now = device->getTimer()->getRealTime();
  8.     waitTime = now + 16;
  9.  
  10.     while(now < waitTime) {
  11.       now = device->getTimer()->getRealTime();
  12.     }
  13.   }
  14. }

Déjà vous observez la déclaration de deux variables de type "u32" à la ligne 2. Il s'agit d'un typedef d'Irrlicht qui désigne un entier non signé de 32 bits. Je vous conseille d'utiliser ces types car d'une part c'est plus court à taper que "unsigned int", et d'autre part ça garantit la compatibilité entre les versions d'Irrlicht et les systèmes d'exploitation.

Donc à chaque boucle principale, on stocke dans "now" la valeur actuelle du temps en millisecondes, "waitTime" dispose lui de la valeur majorée de 16. Ainsi dans le while à la ligne 8, on restera bloqué à cet endroit tant que "now" sera inférieur à "waitTime". Puisqu'on met à jour la valeur de "now" dans la petite boucle, il arrivera bien un moment à now sera supérieur ou égale à "waitTime", à ce moment là, on aura attendu 16 millisecondes dans cette boucle, on peut en sortir et continuer à exécuter la boucle principale et ainsi de suite...

Notre boucle principale s'exécute maintenant environ 60 fois par secondes. Pour changer cette fréquence, il suffit de modifier la valeur 16 avec une autre. Avec 25, la fréquence sera de 40 boucles par secondes...

Le dessin de la scène

Irrlicht propose des fonctions qui permettent de calculer et dessiner une scène 2D/3D. La première à connaître est beginScene() qui comme son nom l'indique, commence le calcul de la scène. Nous allons placer cette fonction en début de boucle principale car nous devrons dessiner la scène 60 fois par secondes pour avoir quelque chose d'animé à l'écran (c'est comme pour le cinéma, vous vous rappelez ?).

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   u32 waitTime, now;
  3.   while(device->run()) {
  4.     driver->beginScene(true,true,video::SColor(255,190,190,190));
  5.     currentScene->updateAll();
  6.     
  7.     now = device->getTimer()->getRealTime();
  8.     waitTime = now + 16;
  9.  
  10.     while(now < waitTime) {
  11.       now = device->getTimer()->getRealTime();
  12.     }
  13.   }
  14. }

La fonction fait appel à cinq paramètres :

Nous allons maintenant terminer le calcul de la scène, à la fin de la boucle principale, quand tout a été fait et tracé. On utilise pour cela la fonction endScene().

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   u32 waitTime, now;
  3.   while(device->run()) {
  4.     driver->beginScene(true,true,video::SColor(255,190,190,190));
  5.     currentScene->updateAll();
  6.     
  7.     now = device->getTimer()->getRealTime();
  8.     waitTime = now + 16;
  9.  
  10.     while(now < waitTime) {
  11.       now = device->getTimer()->getRealTime();
  12.     }
  13.     
  14.     driver->endScene();
  15.   }
  16. }

Une boucle principale pas fiable

Posons-nous une question, et si le calcul de la scène prend 10 millisecondes, combien de temps aura pris toute la boucle principale ? 10ms + 16ms d'attente = 26 ms ! Il y a bien un soucis, si on laisse le code en l'état, on risque d'avoir des ralentissements aléatoires très fréquents et une vitesse biaisée. La solution réside dans la modification de la valeur du temps d'attente en fonction du temps de calcul. Par exemple, si le calcul de la scène a pris 10ms, on ne doit attendre non pas 16, mais 6ms, le total du temps d'exécution de la boucle sera bien de 16ms.

Comment faire alors ? Il suffit de créer deux nouvelles variables : before et updateTime. la première permettra de mémoriser le temps actuel en millisecondes avant le calcul de la scène. Après le calcul de la scène, on renseigne la variable now avec le temps actuel. On peut donc remplir la variable updateTime avec now moins before. On obtiendra donc le temps de calcul de la scène. Il nous reste plus qu'à fixer waitTime à 16 moins updateTime et on aura le temps précis à attendre pour que la boucle fasse à chaque itération 16ms.

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   u32 waitTime, now, before, updateTime;
  3.   while(device->run()) {
  4.     before = device->getTimer()->getRealTime();
  5.     
  6.     driver->beginScene(true,true,video::SColor(255,190,190,190));
  7.     currentScene->updateAll();
  8.     
  9.     now = device->getTimer()->getRealTime();
  10.     updateTime = now - before;
  11.     waitTime = now + (16 - updateTime);
  12.  
  13.     while(now < waitTime) {
  14.       now = device->getTimer()->getRealTime();
  15.     }
  16.  
  17.     driver->endScene();
  18.   }
  19. }

Le graphe de scène

Irrlicht gère la 3D grâce à un graphe de scène. Il s'agit d'une structure arborescente qui permet d'organiser tout le contenu de la scène. L'idée c'est que lorsque vous placez des objets 3D dans le graphe de scène, ils subissent les modifications de leur parent. Petit exemple, dans votre scène vous ajoutez un helicopter, cet hélicopter bouge, il tourne, il vole quoi. Si vous placez ensuite des passagers, ces derniers seront les enfants de l'helicopter du point de vue du graphe de scène. Donc si l'helicopter fait une rotation, les passagers subissent aussi cette rotation.

Il est évident que le graphe de scène dépend d'une Scene, ça tombe bien on a écrit une classe qui désigne les scènes de notre jeu. On a donc qu'à intégrer un graphe de scène comme propriété de notre classe. On définit donc un pointeur vers un objet de type ISceneManager.

Classe Scene (Scene.h)
  1. #ifndef SCENE_H
  2. #define SCENE_H
  3.  
  4. #include <map>
  5. #include "SceneElement.h"
  6. #include <irr/irrlicht.h>
  7.  
  8. using namespace std;
  9. using namespace irr;
  10.  
  11. class Scene {
  12.   private:
  13.     scene::ISceneManager *graph;
  14.     map<char*,SceneElement*> sceneElementList;
  15.     map<char*,SceneElement*>::iterator seIterator;
  16.  
  17.   public:
  18.     Scene();
  19.     void updateAll();
  20.     ~Scene();
  21. };
  22.  
  23. #endif

Dans la partie source que doit-on faire ? Déjà initialiser ce fameux graphe, pour cela, situons-nous dans le constructeur de la classe Scene et ajoutons la ligne miracle qui se sert de la fonction getSceneManager().

Classe Scene (morceau de Scene.cpp)
  1. #include "Game.h"
  2. #include "Scene.h"
  3. #include <iostream>
  4.  
  5. using namespace std;
  6.  
  7. Scene::Scene() {
  8.   graph = Game::device->getSceneManager();
  9.  
  10.   sceneElementList["logo"] = new SceneElement();
  11. }

N'oublions pas l'inclusion de Game.h sans quoi l'instruction Game::device pourrait ne pas passer au compilateur.

Mais cela ne suffit pas, si vous ajoutez plus tard des objets à votre graph, vous ne les verrez pas à l'écran. Pour la bonne et simple raison que nous n'avons toujours pas dit à Irrlicht d'afficher ces objets. Et oui, on a commencé le calcul de la scène avec beginScene(), on a créé un graphe de scène et on l'a initialisé, puis de retour dans la boucle principale, on attend et on arrête le calcul de la scène, à aucun moment on a dit qu'on affichait la scène en question. Pour y remédier, on utilisera la fonction drawAll() du graphe de scène.

Classe Scene (morceau de Scene.cpp)
  1. void Scene::render() {
  2.   graph->drawAll();
  3. }

J'ai choisi de le faire dans une nouvelle méthode de la classe Scene que j'ai appelé render(). J'ai opté pour cette solution afin de rester libre d'afficher la scène quand je veux et pas au moment où on met à jour la scène avec updateAll().

On n'oublie pas de bien définir la méthode render() dans le header de Scene :

Classe Scene (morceau de Scene.h)
  1. class Scene {
  2.   private:
  3.     scene::ISceneManager *graph;
  4.     map<char*,SceneElement*> sceneElementList;
  5.     map<char*,SceneElement*>::iterator seIterator;
  6.  
  7.   public:
  8.     Scene();
  9.     void updateAll();
  10.     void render();
  11.     ~Scene();
  12. };

On ajoute également l'appel à cette dernière méthode (ligne 8) créée dans la boucle principale (classe Game), pour que l'affichage de la scène se fasse 60 fois par seconde :

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   u32 waitTime, now, before, updateTime;
  3.   while(device->run()) {
  4.     before = device->getTimer()->getRealTime();
  5.     
  6.     driver->beginScene(true,true,video::SColor(255,190,190,190));
  7.     currentScene->updateAll();
  8.     currentScene->render();
  9.     
  10.     now = device->getTimer()->getRealTime();
  11.     updateTime = now - before;
  12.     waitTime = now + (16 - updateTime);
  13.  
  14.     while(now < waitTime) {
  15.       now = device->getTimer()->getRealTime();
  16.     }
  17.  
  18.     driver->endScene();
  19.   }
  20. }

On compile et on exécute...

Execution du jeu

Un fond gris ! C'est beau dites donc ! Je sais c'est très peu après un si long tutorial mais on avance beaucoup mine de rien, les fondations de notre jeu se durcissent, on programme petit à petit une structure qui vous permettra de mettre sur pied quelque chose de concret et de fonctionnel. Au prochain chapitre, on va s'attaquer aux événements et là ça deviendra un peu plus excitant...