Aller au menu - Aller au contenu

Icône Dans les entrailles de la bête

Avatar
Mise à jour : 23/03/2011
Difficulté : Facile Facile Creative Commons BY-SA
1 621 visites depuis 7 jours, dont 17 sur ce chapitre classé 80/786
Sortez vos casques à lampe frontale, nous allons faire un peu de spéléologie aux tréfonds d'Irrlicht avec ce chapitre qui sera entièrement consacré à la théorie. En nous concentrant plus particulièrement sur certains mécanismes du moteur qui sont transparents pour l'utilisateur et que nous avons donc utilisés jusqu'ici sans y penser.

Il est néanmoins important (voir indispensable) de savoir comment les choses fonctionnent si on veut les utiliser correctement, et à plus forte raison si on veut les modifier sans danger (c'est pour bientôt ;) ).
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Les namespaces

Aussi incroyable que cela puisse paraître, nous n'avons toujours pas vu depuis le début de ce tutoriel à quoi correspondent les différents namespaces présents dans Irrlicht. Ceci étant dit, si vous avez à peu près suivi vous devriez avoir une petite idée pour chacun d'entre eux quand même.

Il y a 6 namespaces dans Irrlicht. Le premier est irr qui contient absolument tout le moteur. Viennent ensuite les 5 namespaces inclus dans irr qui découpent le moteur en catégories :
  • core : contient toutes les classes relatives à la géométrie (line2d, plane3d, ...) et aux structures de données (list, array, ...)
  • gui : contient tout ce qui a un rapport direct avec la GUI. Ce qui inclue la gestion du curseur et des polices de caractères
  • io : contient tout ce qui a un rapport direct avec les fichiers et les flux
  • scene : contient tout ce qui peut être mis (ou ce qui sert à mettre quelque chose) dans une scène
  • video : contient tout ce qui à un rapport avec le driver video

Du polymorphisme et de la GUI

Bien, j'espère que vous êtes au point sur les concepts de la programmation orientée objet et du C++. Les choses vont devenir un peu plus intéressantes. :)

Premièrement, il faut savoir (si vous ne vous en étiez pas encore rendu compte) que nous avons bénéficié des avantages du polymorphisme dès les premiers chapitres de ce tutoriel. En effet, tous les scene node sont des dérivés de la classe abstraite irr::scene::ISceneNode. Mais pour expliquer le fonctionnement de la chose, il est plus simple de commencer avec la GUI.



Si vous avez bonne mémoire, vous devez vous souvenir à quel point il est simple et agréable d'ajouter un nouvel élément à l'environnement de GUI. On fait appel à une méthode, et pouf ! Le nouvel élément est tout de suite pris en charge, pas besoin de s'occuper de la mise à jour, ni de la gestion des events, ni de quoi que ce soit d'autre.

On peut donc en déduire que l'environnement de GUI met à jour lui même tous les éléments qui y sont attachés. Mais déjà, comment fait-il pour "s'attacher" des éléments ? L'autre problème est que chaque élément a un comportement différent, un bouton doit avoir une apparence différente si on clique dessus (changement d'image de fond par exemple) alors qu'une liste déroulante doit être déroulée, etc... Comment l'environnement de GUI sait-il quelle fonction appeler pour faire la mise à jour de l'élément ?



La réponse à cette deuxième question est simple : il appelle toujours la même !

Tous les éléments de la GUI sont des dérivés de la classe abstraite irr::gui::IGUIElement. (Jetez un oeil au diagramme en haut de la page de doc pour vous en convaincre). Et il se trouve que cette classe possède des méthodes purement virtuelles permettant la mise à jour graphique de l'élément (draw) ainsi que la gestion des événements (OnEvent).

De cette manière, chaque élément de la GUI (bouton, edit box, boîte de dialogue...) redéfinit ces méthodes selon ses besoins, et l'environnement de GUI n'a plus qu'à les appeler à chaque calcul du rendu.



Reste la question de "l'attachement". Comment fait l'environnement de GUI pour stocker des pointeurs vers tous les éléments qui lui sont associés ? Et bien en fait, ce n'est pas l'environnement de GUI qui stocke tous les éléments, mais les éléments qui se stockent eux mêmes.

Je m'explique : chaque élément possède obligatoirement un élément parent, et un nombre potentiellement illimité d'éléments enfants.

Vous allez me dire qu'il est possible par exemple d'ajouter un bouton via irr::gui::IGUIEnvironment::addButton sans passer d'argument au paramètre concernant l'élément parent. Et vous avez tout à fait raison mais ce n'est pas pour autant que le bouton n'en a pas. Par défaut, il sera attaché au rootGUIElement, qui est comme son nom l'indique, l'élément racine à la base de tout ce que vous ajoutez à la GUI.

Chaque élément possède donc en attribut un pointeur de type IGUIElement vers son élément parent.
On peut le constater à la ligne 875 du fichier IGUIElement.h :

Code : C++
1
IGUIElement* Parent;

Reste la question des enfants. Comment fait un élément pour stocker un nombre illimité d'accès vers des éléments enfants ? Tout bêtement en se servant d'une liste chaînée. Il s'agit là aussi d'un attribut visible dans le fichier IGUIElement.h, 2 lignes au dessus de Parent :

Code : C++
1
core::list<IGUIElement*> Children;



Histoire d'achever ceux qui commencent à se dire qu'ils vont sauter le chapitre, je vais vous faire une révélation digne d'un soap opera : le guiRootElement et l'environnement de GUI sont en fait... une seule et même classe !

C'est assez simple à comprendre en fait. La classe IGUIEnvironment n'est en réalité qu'une interface (comme l'indique le I de son nom). Aussi ce n'est pas cette classe que nous instancions en utilisant la méthode getGUIEnvironment du device, mais CGUIEnvironment (inutile de chercher dans la doc, elle n'est pas référencée).

Cette classe dérive bien évidemment de IGUIEnvironment, mais aussi de IGUIElement. De cette manière, lorsque vous ajoutez votre bouton en ne spécifiant pas de parent, l'environnement de GUI (l'instance de CGUIEnvironment) stocke tout simplement un pointeur vers l'élément créé dans sa propre liste d'éléments enfants, car c'est lui le guiRootElement.

D'ailleurs, si vous faites appel à la méthode getRootGUIElement de l'environnement de GUI, vous vous retrouverez avec un pointeur qui pointe vers... l'environnement de GUI ! La preuve par le code (tiré du fichier CGUIEnvironment.cpp, ligne 1465) :

Code : C++
1
2
3
4
IGUIElement* CGUIEnvironment::getRootGUIElement()
{
        return this;
}



Maintenant que nous savons comment sont stockés les éléments, et qu'on sait quelles méthodes les mettent à jour, reste à savoir de quelle manière ces méthodes sont appelées. Tout part bien entendu de l'environnement de GUI. Vous n'êtes pas sans savoir que pour que la GUI soit mise à jour à l'écran, il faut appeler la méthode drawAll de l'environnement de GUI à l'intérieur de la boucle de rendu.

Examinons un peu le contenu de cette méthode (tiré du fichier CGUIEnvironment.cpp, ligne 181) :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CGUIEnvironment::drawAll()
{
        if (Driver)
        {
                core::dimension2d<s32> dim = Driver->getScreenSize();
                if (AbsoluteRect.LowerRightCorner.X != dim.Width ||
                        AbsoluteRect.LowerRightCorner.Y != dim.Height)
                {
                        // resize gui environment
                        DesiredRect.LowerRightCorner.X = Driver->getScreenSize().Width;
                        DesiredRect.LowerRightCorner.Y = Driver->getScreenSize().Height;
                        AbsoluteClippingRect = DesiredRect;
                        AbsoluteRect = DesiredRect;
                        updateAbsolutePosition();
                }               
        }
 
        // make sure tooltip is always on top
        if (ToolTip.Element)
                bringToFront(ToolTip.Element);
 
        draw();
        OnPostRender ( os::Timer::getTime () );
}

La seule ligne qui nous intéresse est la 22, l'appel à la fonction draw. (Je vous laisse vous casser les dents sur le reste de la fonction, c'est du code corsé bien comme on l'aime ;) ).


La fonction draw est une méthode virtuelle de IGUIElement, et comme son nom l'indique, elle sert à dessiner l'élément ainsi que tous ses éléments enfants. Etant donné que c'est le guiRootElement qui l'appelle, tous les éléments de la GUI vont être dessinés. Voici le code tiré du fichier IGUIElement.h :

Code : C++
1
2
3
4
5
6
7
8
9
virtual void draw()
        {
                if (!IsVisible)
                        return;
 
                core::list<IGUIElement*>::Iterator it = Children.begin();
                for (; it != Children.end(); ++it)
                        (*it)->draw();
        }

On commence par vérifier ligne 3 que l'élément doit bien être dessiné. Ligne 6, on crée un itérateur pour parcourir la liste des éléments qui sont rattachés à celui-ci. Puis on appelle la méthode draw de chaque élément. Evidemment, chacun des éléments de cette liste va à son tour appeler les méthodes draw de chacun de ses éléments enfants, et ainsi de suite jusqu'à ce que tous les éléments aient été passés en revue.


Si vous avez bien suivi, vous devriez vous demander quel est l'intérêt de cette méthode puisqu'elle ne fait que s'appeler d'élément en élément, et... c'est tout. En regardant le prototype de la fonction, on s'aperçoit qu'elle est virtuelle. Et qu'elle est donc tout naturellement redéfinie pour chaque type d'élément. C'est ce que nous disions plus haut à propos des méthodes de mise à jour. Ce sont toujours les mêmes qui sont appelées, mais elles sont redéfinies selon les classes.

Le code qu'on voit plus haut reste quelle que soit la classe, mais le code permettant le dessin de l'élément change selon le type de celui-ci. Un bouton ne sera pas dessiné de la même manière qu'une edit box ou qu'un static text. Chacun de ces types d'élément possède sa propre classe dérivée de IGUIElement qui redéfinit draw selon ses besoins. Etant donné que le guiRootElement n'est pas visible, il n'a pas besoin de code pour le dessin. ;)

Du polymorphisme et du scene manager

Si vous avez compris tout ce qui précède en ce qui concerne la GUI, réjouissez-vous car le scene manager fonctionne sur le même principe à quelques détails près. Remplacez IGUIElement par ISceneNode et IGUIEnvironment par ISceneManager puis relisez toute la sous-partie précédente. ^^

Non, il y a tout de même quelques différences. Mais commençons d'abord par les similitudes : la classe ISceneManager est abstraite. Et lorsqu'on accède au gestionnaire de scène par le device, celui-ci nous renvoie une instance de la classe CSceneManager (qui n'est évidemment pas documentée). Et bien sûr, CSceneManager qui gère tous les scene node de la scène, dérive de ISceneManager évidemment, mais aussi de ISceneNode...


Comme vous l'aurez deviné, il existe un scene node parent de tous les autres qui s'appelle rootSceneNode et qui n'est autre que le scene manager lui même. Code extrait du fichier CSceneManager.cpp :

Code : C++
1
2
3
4
ISceneNode* CSceneManager::getRootSceneNode()
{
        return this;
}

Les scene node possèdent eux aussi un pointeur vers leur noeud parent et une liste chaînée de noeuds enfants. (lignes 771 et 774 du fichier ISceneNode.h)


La principale différence avec la GUI se trouve au niveau du rendu des scene node. Jetez un oeil à la méthode drawAll de CSceneManager pour vous en convaincre. Elle est tellement complexe que nous n'allons pas la voir tout de suite, mais tout le prochain chapitre lui sera consacré. Sachez néanmoins que si vous avez compris ce qui précède, vous avez fait une grosse partie du boulot. Les méthodes de mise à jour sont ce qu'il y a de plus intéressant à étudier. Ce sont elles qui vont vous permettre de créer les scene node que vous voulez. :)

Un codé pour un rendu

Avec les 2 sous parties précédentes, on a vu comment tous les éléments visibles d'une scène étaient dessinés à l'écran. Continuons sur notre lancée, et voyons les autres étapes du processus de rendu.

Si notre analyse part d'une boucle de rendu classique, on s'aperçoit qu'il y a 2 autres méthodes qui sont en jeu :
  • beginScene (bool backBuffer, bool zBuffer, SColor color)
  • endScene (s32 windowId, core::rect< s32 > *sourceRect)

Toutes deux appartiennent au driver. Comme leurs noms l'indiquent, la première sert à démarrer le rendu, et la seconde à le finir. Si vous avez bonne mémoire, vous devez vous rappeler que l'utilité de leurs paramètres est expliquée au deuxième chapitre de ce tutoriel. ;)

Mauvaise nouvelle (ou pas), nous n'allons pas voir le contenu même des fonctions. Pour la simple et bonne et bonne raison qu'il existe en triple exemplaire (au moins). Les méthodes du driver dépendent directement de l'API 3D utilisée. Comme vous l'avez sûrement déjà deviné, irr::video::IVideoDriver n'est qu'une interface qui est dérivée pour implémenter ses méthodes selon l'API utilisée. Quand vous choisissez de créer votre device en utilisant OpenGL par exemple, c'est une instance de COpenGLDriver que la méthode getVideoDriver vous renvoie.

Ce qui est commun à toutes les implémentations est que beginScene se charge de vider les buffers. Ensuite les éléments de la scène sont dessinés. Et enfin endScene se charge d'afficher le rendu à l'écran et de compter les FPS.

Vous en savez maintenant un peu plus sur le processus de rendu. Bien entendu rien ne vous empêche d'aller regarder le code d'une des implémentations de IVideoDriver si vous êtes familiers de l'API qu'elle manipule (et même si vous ne l'êtes pas ^^ ).
Il est possible que vous ne voyiez pas l'intérêt pour le moment de descendre autant dans le code source. Après tout si Irrlicht doit nous faciliter la vie en s'occupant de tout ça, pourquoi s'y intéresser ? Mais comme expliqué dans l'introduction, une bonne compréhension est quasi indispensable à une bonne utilisation, et complètement indispensable à une bonne modification.

Vous croyez qu'on a fait le tour de tout ce qui touche à la GUI par exemple ? Que nenni ! Nous en sommes loin. Il est tout à fait possible de créer ses propres types d'éléments de GUI, comme de créer ses propres types de scene node, et bien d'autres choses encore... Mais avant d'en arriver là, il nous reste quelques chapitres à voir, et quelques autres morceaux de code source à examiner. ^^
Chapitre précédent Sommaire Chapitre suivant

Partager

Il n'y a pas encore de commentaire pour ce tuto.