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.