Architecture d'un jeu vidéo 3D


précédentsommairesuivant

III. Le moteur graphique

III-1. 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.

 
Sélectionnez
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-2. 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.

 
Sélectionnez
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-3. 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.

 
Sélectionnez
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-3-A. 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

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.

Image non disponible
Le thème graphique 'Taharez'

L'affichage d'une interface se fait de la manière suivante :

 
Sélectionnez
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-4. A 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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-5. 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.

Réception des évènements
Sélectionnez
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ènement 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

 
Sélectionnez
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 :
Classe mère des écouteurs d'évènements
Sélectionnez
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 :
La classe fille de gestion des menus
Sélectionnez
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é
 
Sélectionnez
	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 :
 
Sélectionnez
void graphics_engine::set_state(int s){
	state = s;
	idevice->setEventReceiver(l_eh[s].get());
}

Ainsi les gestions des évèmenents 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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2007 Pierre Schwartz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.