III. Le moteur graphique▲
III-A. Vue d'ensemble▲
Le moteur graphique va gérer tout l'affichage, qu'il s'agisse des objets 3D ou plus simplement des menus. Il devra implémenter toutes les fonctions virtuelles pures de la classe mère engine. C'est également le moteur graphique qui va gérer les entrées clavier et souris.
class graphics_engine : public engine,
irr::IEventReceiver
{
public:
graphics_engine(game*, bool); // objet contenant et mode passif
~graphics_engine();
bool OnEvent(irr::SEvent);
void frame();
inline gui_system *get_gui(){return gui;}
private:
gui_system *gui;
irr::video::IVideoDriver *driver;
CEGUI::System *sys;
irr::IrrlichtDevice *idevice;
void process_event(engine_event&);
scene_graph *sg;
bool console_visible;
// ...
};Les positions de tous les objets du jeu sont stockées dans le graphe de scène d'Irrlicht.
III-B. Le graphe de scène▲
J'ai encapsulé le graphe de scène fourni par Irrlicht dans une classe plus spécialisée, permettant de gérer les accès concurrents, et surtout qui va propager les modifications aux autres joueurs.
class scene_graph{
public:
scene_graph(graphics_engine*);
~scene_graph();
irr::scene::ISceneManager *smgr;
void add_node(int,irr::core::vector3df&, irr::core::vector3df&);
void move_node(int,irr::core::vector3df&, irr::core::vector3df&);
void remove_node(int);
void render();
void set_observer(network_interface*);
void remove_observer();
boost::mutex smgr_mutex;
private:
graphics_engine *parent;
network_interface *o;
};Il conviendra d'attacher un observateur réseau à cet objet.
Les fonctions add_node, move_node et remove_node doivent être précisées pour spécifier un identifiant de machine réseau à laquelle il n'est pas nécessaire de transmettre la modification. Il suffit de rajouter un paramètre dans ces fonctions. J'aborderai ce point dans la partie relative au moteur de réseau.
Il faudra aussi créer au démarrage du jeu une collection de meshs et leur associer un identifiant, qui servira à appeler la fonction add_node.
III-C. Constructeur du moteur graphique▲
Le constructeur va créer le contexte d'affichage, en tenant compte d'un éventuel mode passif, il va aussi créer le contexte CEGUI pour pouvoir afficher des menus.
graphics_engine::graphics_engine(game *g, bool passive):engine(g)
{
// on crée le contexte openGL
if (!passive)
idevice = irr::createDevice(irr::video::EDT_OPENGL, irr::core::dimension2d<irr::s32>(800, 600), 32, false, false, false);
else
idevice = irr::createDevice(irr::video::EDT_NULL);
driver = idevice->getVideoDriver();
sg->smgr = idevice->getSceneManager();
// on masque le curseur
idevice->getCursorControl()->setVisible(false);
// on crée le contexte CEGUI
try{
CEGUI::IrrlichtRenderer *myRenderer = new CEGUI::IrrlichtRenderer(idevice);
new CEGUI::System(myRenderer);
}catch(CEGUI::Exception &e){
shared::get()->log.error(std::cerr << e.getMessage() << std::endl);
}catch(...){
shared::get()->log.error(std::cerr << "unknown exception" << std::endl);
}
// on définit le niveau de log de CEGUI
CEGUI::Logger::getSingleton().setLoggingLevel((CEGUI::LoggingLevel)3);
// on charge le thème graphique des menus
try{
CEGUI::SchemeManager::getSingleton().loadScheme("./data/gui/schemes/TaharezLook.scheme");
}catch(CEGUI::Exception &e){
std::cout << e.getMessage() << std::endl;
}
// on charge une police d'écriture
CEGUI::FontManager::getSingleton().createFont("./data/gui/fonts/Commonwealth-10.font");
CEGUI::System::getSingleton().setDefaultMouseCursor("TaharezLook", "MouseArrow");
gui = new gui_system();
gui->set_parent(this);
}III-C-1. Un petit mot sur CEGUI▲
CEGUI est une bibliothèque permettant de gérer très simplement des interfaces graphiques dans des contextes OpenGL, DirectX, Irrlicht et Ogre3D. Chaque interface est décrite sous forme de fichier XML et il est possible de dessiner très simplement des écrans en utilisant des outils comme le CEGUI Layout Editor

Pour afficher une interface, il suffit de charger le fichier XML correspondant. Les différents évènements peuvent être redirigés vers des fonctions LUA ou des fonctions C++.
Le thème graphique d'une interface est aisément modifiable : il suffit d'éditer le fichier image représentant tous les objets graphiques ainsi que les fichiers de configuration XML associés.

L'affichage d'une interface se fait de la manière suivante :
void gui_system::display_interface(){
CEGUI::WindowManager& winMgr = CEGUI::WindowManager::getSingleton ();
winMgr.destroyAllWindows();
// ajout éventuel d'options sur le rendu de l'interface
// chargement du fichier XML
background->addChildWindow (winMgr.loadWindowLayout (layout_path+"menu.layout", "root/"));
// redirection des évènements
CEGUI::WindowManager::getSingleton().getWindow("root/b_quit")->
subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(&gui_system::close, this));
CEGUI::System::getSingleton ().setGUISheet (background);
}Les redirections des évènements et les noms des objets graphiques sont différents d'une interface à l'autre, il y aura donc autant de fonctions que d'écrans à afficher.
Les noms des objets graphiques doivent être spécifiés dans le fichier XML. Ces noms sont repris dans le code C++. Dans le cas où un objet n'a plus le même nom, une exception CEGUI est levée. Il faudra donc attraper ces exceptions.
III-D. �? chaque frame du moteur graphique▲
Le moteur graphique a un travail très simple : il se contente d'afficher le graphe de scène ainsi que l'interface CEGUI. Ces deux affichages doivent être protégés des éventuels accès concurrents :
void graphics_engine::frame(){
// on vérifie si l'utilisateur n'a pas fermé la fenêtre
if (!driver || !idevice->run())
parent->stooooop();
driver->beginScene(true, true, irr::video::SColor(0,100,100,100));
// on dessine le graphe de scène
sg->render();
// on appelle l'affichage de l'interface
gui->render();
driver->endScene();
}Le code d'affichage de l'interface est encore plus simple :
void gui_system::render(){
boost::mutex::scoped_lock lock(shared::get()->mutex_gui);
CEGUI::System::getSingleton().renderGUI();
}L'interface est aussi protégée des accès concurrents par un mutex, stocké dans notre singleton.
III-E. Les entrées clavier / souris▲
Irrlicht se charge d'écouter tous les évènements clavier / souris. Pour les écouter, il suffit de créer un irr::IEventReceiver. Plusieurs possibilités pour ça. La première solution est de faire hériter le moteur graphique de irr::IEventReceiver. irr::IEventReceiver possède une méthode virtuelle pure, il nous faut donc l'implémenter. Irrlicht sera le seul point d'entrée des évènements, c'est donc lui qui va envoyer les évènements à CEGUI.
bool graphics_engine::OnEvent(irr::SEvent e){
if (!idevice)
return false;
switch (e.EventType){
case irr::EET_MOUSE_INPUT_EVENT:
// envoyer la position de la souris à CEGUI
CEGUI::System::getSingleton().injectMousePosition((float)e.MouseInput.X, (float)e.MouseInput.Y);
// envoyer les clics à CEGUI
switch (e.MouseInput.Event){
case irr::EMIE_LMOUSE_PRESSED_DOWN :
CEGUI::System::getSingleton().injectMouseButtonDown(CEGUI::LeftButton);
break;
case irr::EMIE_LMOUSE_LEFT_UP :
CEGUI::System::getSingleton().injectMouseButtonUp(CEGUI::LeftButton);
break;
case irr::EMIE_MOUSE_WHEEL :
CEGUI::System::getSingleton().injectMouseWheelChange(e.MouseInput.Wheel);
break;
default:
break;
}
return true;
case irr::EET_KEY_INPUT_EVENT:
if (e.KeyInput.PressedDown){
if (e.KeyInput.Key == irr::KEY_F2){
// afficher/masquer la console
gui->toggle_console();
break;
}
// injecter certaines touches
switch(e.KeyInput.Key){
case irr::KEY_RETURN: CEGUI::System::getSingleton().injectKeyDown(CEGUI::Key::Return); break;
case irr::KEY_BACK: CEGUI::System::getSingleton().injectKeyDown(CEGUI::Key::Backspace); break;
case irr::KEY_TAB: CEGUI::System::getSingleton().injectKeyDown(CEGUI::Key::Tab); break;
default:
CEGUI::System::getSingleton().injectChar(e.KeyInput.Char);
break;
}
}
break;
default:
return false;
}
return false;
}On remarquera l'appel à gui_system::toggle_console() pour afficher / masquer la console. En effet, la console a exactement le même rôle, qu'elle soit accessible par le réseau ou directement au clavier. Il suffit de rediriger l'évènement de validation de l'interface de la console vers la fonction de traitement d'instruction.
Au fur et à mesure que le jeu se complexifiera, la fonction OnEvent va très vite devenir très lourde et surtout une abominable imbrication de switch et de if. C'est pourquoi on va se tourner vers l'autre solution : nous allons créer une classe virtuelle d'écouteur d'évènements et nous allons instancier des instances de classes filles qui redéfinissent le traitement des évènements. Ainsi, à chaque changement d'état du jeu, nous n'aurons qu'à récupérer un pointeur vers l'écouteur d'évènement à utiliser. Il faudra donc instancier tous les écouteurs au démarrage du moteur graphique et les stocker dans un conteneur les associant au numéro de l'état du jeu : par exemple une
std::map< int, boost::shared_ptr < event_handler > >Les changements à apporter sont mineurs :
- il ne faut plus faire hériter le moteur graphique de irr::IeventReceiver ;
- il faut créer une classe event_handler :
class event_handler : public irr::IEventReceiver{
public:
event_handler(graphics_engine* ge){parent = ge;}
virtual ~event_handler(){}
virtual bool OnEvent(irr::SEvent)=0;
inline graphics_engine* get_parent(){return parent;}
protected:
graphics_engine *parent;
};- créer des classes filles :
class menu_event_handler : public event_handler{
public:
menu_event_handler(graphics_engine*);
bool OnEvent(irr::SEvent){
// tout le code de gestion d'évènements
}
};- instancier tous les écouteurs d'évènements et les stocker dans un conteneur dédié
l_eh[ON_MENUS] = boost::shared_ptr<event_handler>(new menu_event_handler(this));
l_eh[ON_GAME] = boost::shared_ptr<event_handler>(new game_event_handler(this));- créer une petite fonction pour changer d'écouteur d'évènements :
void graphics_engine::set_state(int s){
state = s;
idevice->setEventReceiver(l_eh[s].get());
}Ainsi les gestions des évènements sont plus claires et aussi plus rapides puisqu'il y a moins de switch et de if à résoudre. Il n'a plus qu'à créer les différentes classes relatives aux différents états du jeu.
On peut juste signaler que les écouteurs d'évènements doivent être contenus dans le moteur graphique, qu'ils ne doivent pas exister en mode passif.


