Réalisation d'un jeu de bataille navale en C++


précédentsommairesuivant

D. Couche haute : l'affichage Irrlicht

L'affichage via le moteur 3D Irrlicht va ressembler à l'affichage par SDL, il va implémenter les mêmes fonctions. Pour une présentation du moteur 3D Irrlicht, vous pouvez consulter mon tutoriel sur Irrlicht

D-I. Création de l'interface Irrlicht

La création demande la construction des objets Irrlicht : le device pour afficher l'écouteur d'évènements, la caméra, le chargement des meshs et la création de la skybox.

 
Sélectionnez
IrrInterface::IrrInterface(){
                                                                                // fullscreen
    _device   = createDevice( video::EDT_OPENGL,core::dimension2d<s32>(800, 600), 32, false);
    _device->setWindowCaption(L"Bataille navale");
    _driver   = _device->getVideoDriver();
    _smgr     = _device->getSceneManager();
    _camera   = _smgr->addCameraSceneNode(0, core::vector3df(0,200,-300), core::vector3df(0,30,-150), -1);
    _receiver = new IrrEvent(this); // création de l'écouteur d'évènements
    _device->setEventReceiver(_receiver);
 
    // on crée l'interface graphique pour positionner des widgets
    _env = _device->getGUIEnvironment();
 
	// création de la police d'affichage
    _skin = _env->getSkin();
    _skin->setSize(EGDS_BUTTON_HEIGHT , 100);
    _font = _env->getFont("../data/irr/font.bmp");
    if (_font){
        _skin->setFont(_font);
    }
 
    // créer les noeuds des bateaux - ie chargement du mesh
    _mcube = _smgr->getMesh("../data/irr/sphere.3ds");
 
    // on créé la skybox
    _smgr->addSkyBoxSceneNode(
            NULL,
            _driver->getTexture("../data/irr/sd_dn.jpg"),
            _driver->getTexture("../data/irr/sd_lf.jpg"),
            _driver->getTexture("../data/irr/sd_rt.jpg"),
            NULL,
            _driver->getTexture("../data/irr/sd_bk.jpg"));
}

L'écouteur d'évènements est un objet important dans cette interface. En fonction de l'état du jeu, il dispatche l'évènement qu'il reçoit dans une fonction dédiée :

 
Sélectionnez
bool IrrEvent::OnEvent(SEvent event){
    switch (_ecran){
        case ECRAN_CHOIX_CLIENT_SERVEUR:
            return EventChoixClientServeur(event);
        case ECRAN_POSITIONNEMENT:
            return EventChoixPositionnement(event);
        case ECRAN_CHOIX_ATTAQUE:
            return EventChoixAttaque(event);
        case ECRAN_FINI:
            return EventFini(event);
        default :
            return false;
    }
}

D-II. Ecran de choix Client/Serveur

L'écran est très simple : une image en arrière plan et 3 boutons pour choisir si on veut créer une partie (serveur), en rejoindre une (client) ou bien quitter.

Je crée des objets dans une interface graphique gérée par Irrlicht par l'appel à _env->addButton( ... ) ou _env->addImage( ... ) puis on lance la boucle d'affichage.

 
Sélectionnez
int IrrInterface::choose_client_server(){
    // mettre une interface pour choisir le mode
    _receiver->_ecran = IrrEvent::ECRAN_CHOIX_CLIENT_SERVEUR;
    _receiver->_fini = IrrEvent::PAS_FINI;
 
    IGUIImage *img = _env->addImage(
                    _driver->getTexture("../data/irr/bataille_navale.jpg"),
                    irr::core::position2d<int>(0,0));
 
    IGUIButton *b1 = _env->addButton(core::rect<s32>(300,100,500,200), 0, 101, L"Creer une partie");
    IGUIButton *b2 = _env->addButton(core::rect<s32>(300,250,500,350), 0, 102, L"Rejoindre une partie");
    IGUIButton *b3 = _env->addButton(core::rect<s32>(300,400,500,500), 0, 103, L"Quitter");
 
    while(_device->run() && _receiver->_fini != IrrEvent::FINI){
        _driver->beginScene(true, true,  video::SColor(255,100,100,100));
 
        _env->drawAll();
 
        _driver->endScene();
 
        // on ralenti le framerate !!!!
        Sleep(30);
    }
 
    // on retire les objets graphiques
    b1->setVisible(false);
    b2->setVisible(false);
    b3->setVisible(false);
    img->setVisible(false);
 
    // si l'utilisateur a fermé la fenêtre, on quitte
    if (!_device->run())
        return 2;
 
    switch (_receiver->_etat){
        case IrrEvent::QUITTER:
            return 2;
        case IrrEvent::CREER:
            return 0; // serveur
        case IrrEvent::REJOINDRE:
            return 1; // client
        default :{
            cerr << "code de retour inconnu pour choix du client/serveur" << endl;
            return 2;
        }
    }
}

La boucle d'affichage tourne tant que la fenêtre n'a pas été fermée et que le drapeau _receiver->fini n'est pas positionné à FINI. Ce drapeau est mis à jour par la fonction de gestion des évènements. Dès qu'un clic sur l'un des boutons est détecté, le drapeau est mis à FINI et la boucle d'affichage peut s'arrêter pour passer à l'écran suivant.

Le framerate est contrôlé simplement par un Sleep(), qui n'est pas des plus élégants. Irrlicht permet de calculer automatiquement le taux de rafraîchissement : int irr::video::IVideoDriver::getFPS(). Il suffit ensuite d'ajuster un Sleep à la bonne durée selon la vitesse d'affichage que vous souhaitez. Et bien sûr si vous voulez obtenir le meilleur de votre machine, ne mettez pas de Sleep du tout.

La gestion des évènements est très simple : à chaque évènement on regarde s'il s'agit d'un évènement sur l'interface graphique, puis on regarde le numéro de l'objet qui a déclenché l'évènement et on agit en conséquence.

 
Sélectionnez
bool IrrEvent::EventChoixClientServeur(SEvent event){
    // on teste selon l'id de l'objet qui a appelé le onEvent
    switch(event.EventType){
        case EET_GUI_EVENT : {
            s32 id = event.GUIEvent.Caller->getID();
            IGUIEnvironment* env = _parent->get_device()->getGUIEnvironment();
 
            switch(event.GUIEvent.EventType){
                case EGET_BUTTON_CLICKED:{
                    switch(id){
                        case 101: // serveur
                            _etat = CREER;
                            _fini = FINI;
                            return true;
                        case 102:  // client
                            _etat = REJOINDRE;
                            _fini = FINI;
                            return true;
                        case 103 : // quitter
                            _etat = QUITTER;
                            _fini = FINI;
                            return true;
                        default:
                            cerr << "id d'objet inconnu dans l'écran client/serveur" << endl;
                            return false;
                    }
                }
                default:
                    return false;
            }
        }
        default :
            return false;
    }
    return false;
}
Image non disponible

D-III. Ecran de placement des bateaux

Cet écran affiche les deux grilles de jeu et propose un bouton pour placer les bateaux sur la grille du joueur ainsi qu'un bouton pour valider le choix. Les évènements traités sont donc les clics sur les boutons et les pressions des touches directionnelles du clavier pour déplacer la caméra.

Tout d'abord, je crée les deux grilles de jeu. Pour cela, je crée deux meshs qui vont onduler pour simuler de l'eau. C'est l'une des fonctions proposées par Irrlicht rapide et simple à mettre en place :

 
Sélectionnez
int IrrInterface::positionner_bateaux(Flotte &f){
    _receiver->_ecran = IrrEvent::ECRAN_POSITIONNEMENT;
    creer_tous_les_noeuds(f);
 
    // on met un bouton positionner et un bouton valider
	IGUIButton *b1 = _env->addButton(core::rect<s32>(550,420,750,470), 0, 104, L"Placer les bateaux");
	IGUIButton *b2 = _env->addButton(core::rect<s32>(550,500,750,550), 0, 105, L"OK");
    IGUIStaticText *t1 = _env->addStaticText(L"Placez vos bateaux",
                                            core::rect<s32>(200,10,500,50));
    t1->setOverrideColor(video::SColor(0,255,255,0));
 
    // on place la flotte
	_mflotte = _smgr->addHillPlaneMesh("flotte",
		core::dimension2d<f32>(40,40),
		core::dimension2d<s32>(11,11), 0, 0,
		core::dimension2d<f32>(0,0),
		core::dimension2d<f32>(100,100));
 
    _smgr->getMeshManipulator()->makePlanarTextureMapping(_mflotte->getMesh(0), 0.025f);
 
	_nflotte1 = _smgr->addWaterSurfaceSceneNode(_mflotte->getMesh(0), 3.0f, 300.0f, 30.0f);
	_nflotte1->setPosition(core::vector3df(0,0,0));
	_nflotte1->setName((wchar_t*)("flotte1"));
 
	_nflotte1->setMaterialTexture(0,	_driver->getTexture("../data/irr/stones.jpg"));
	_nflotte1->setMaterialTexture(1,	_driver->getTexture("../data/irr/water.jpg"));
 
    _nflotte1->setMaterialType(video::EMT_REFLECTION_2_LAYER);
 
	_nflotte2 = _smgr->addWaterSurfaceSceneNode(_mflotte->getMesh(0), 3.0f, 300.0f, 30.0f);
	_nflotte2->setPosition(core::vector3df(0,0,0));
	_nflotte2->setName((wchar_t*)("flotte2"));
 
	_nflotte2->setMaterialTexture(0,	_driver->getTexture("../data/irr/stones.jpg"));
	_nflotte2->setMaterialTexture(1,	_driver->getTexture("../data/irr/water.jpg"));
 
    _nflotte2->setMaterialType(video::EMT_REFLECTION_2_LAYER);
    _nflotte2->setPosition(core::vector3df(480,0,0));

Le mesh représentant l'eau comporte deux textures, qui sont les textures fournies dans les exemples du SDK Irrlicht. On remarquera juste que j'ai rajouté une bordure noire pour afficher les limites des cases sur l'eau.

Image non disponible
Image non disponible

Dans le fond on aperçoit la skybox, avec elle aussi les textures fournies dans le SDK Irrlicht. La caméra étant toujours orientée dans la même direction, j'ai pu me permettre de ne pas charger le côté arrière ni le côté supérieur. A la différence de l'interface avec SDL, j'ai choisi d'afficher les bateaux en une seule fois et non pas petit bout par petit bout. Je prends un mesh (représentant une sphère) que j'étire pour qu'il ait la bonne taille, puis je le place sur l'eau.

 
Sélectionnez

    _receiver->_fini = IrrEvent::PAS_FINI;
 
    while (_device->run()  && _receiver->_fini != IrrEvent::FINI){
        _driver->beginScene(true, true,  video::SColor(255,100,100,100));
 
        _smgr->drawAll();
        _env->drawAll();
 
        _driver->endScene();
 
        // on attend que le IrrEvent ait attrapé un clic sur le bouton
        if (_receiver->_etat == IrrEvent::BATEAUX_POSITIONNES){
            // on enlève les bateaux
            f.placer_aleatoirement();
 
            // on affiche les nouveaux bateaux
            afficher_flotte(f); // la flotte de bateaux, hein
 
            _receiver->_etat = IrrEvent::ATTENTE_VALIDATION;
        }
 
        Sleep(30);
    }
 
    b1->setVisible(false);
    b2->setVisible(false);
    t1->setVisible(false);
 
    _txt = _env->addStaticText(L" A vous de jouer", core::rect<s32>(100,10,400,100));
    _txt->setOverrideColor(video::SColor(0,255,255,0));
 
    if (_receiver->_fini == IrrEvent::FINI){
        return 0;
    }
    else{
        cerr << "le device s'est fermé pendant le positionnement des bateaux" << endl;
        return 1; // on ferme
    }
}

La fonction de gestion des évènements est légèrement différente : les évènements à attraper ne sont pas les mêmes.

 
Sélectionnez
bool IrrEvent::EventChoixPositionnement(SEvent event){
    // on teste selon l'id de l'objet qui a appelé le onEvent
    switch(event.EventType){
        case EET_GUI_EVENT : {
            s32 id = event.GUIEvent.Caller->getID();
            IGUIEnvironment* env = _parent->get_device()->getGUIEnvironment();
 
            switch(event.GUIEvent.EventType){
                case EGET_BUTTON_CLICKED:{
                    switch(id){
                        case 104: // placer les bateaux
                            _etat = BATEAUX_POSITIONNES;
                            return true;
                        case 105: // valider le placement de bateaux
                            if (_etat==ATTENTE_VALIDATION){
                                _fini = FINI;
                            }
                            return true;
                        // tout autre bouton cliqué
                        default:
                            return false;
                    }
 
                    return false;
                }
                default :
                    return false;
            }
        }
        // évènement clavier
        case EET_KEY_INPUT_EVENT:{
            bougerClavier(event);
            return true;
        }
        default :
            return false;
    }
}

Ce qui nous donne :

Image non disponible

Et dès que le joueur a validé son choix, on passe à l'étape suivante : le jeu proprement dit.

D-IV. Ecran du jeu proprement dit

Dans cet écran, il nous faut repérer les clics sur la grille de droite (représentant le jeu de l'adversaire) puis repérer les coordonnées spatiales correspondant à ces clics. Irrlicht va lancer un rayon depuis la caméra jusqu'aux coordonnées pointées par la souris, puis déterminer l'intersection avec le mesh de l'eau. Irrlicht décompose le mesh de l'eau en octree pour accélérer la recherche d'intersection.

 
Sélectionnez
std::pair<int,int> IrrInterface::choisir_attaque(Essais, Flotte){
    _receiver->_ecran = IrrEvent::ECRAN_CHOIX_ATTAQUE;
 
    _receiver->_fini = IrrEvent::PAS_FINI;
    _receiver->_etat = IrrEvent::ATTENTE_CHOIX; // initialisation
 
    _txt->setText(L" A vous de jouer");
 
    pair<int,int> retour;
 
    while (_device->run()  && _receiver->_fini != IrrEvent::FINI){
        _driver->beginScene(true, true,  video::SColor(255,100,100,100));
 
        _smgr->drawAll();
        _env->drawAll();
 
        _driver->endScene();
 
        // on vérifie qu'on a cliqué
        if (_receiver->_etat == IrrEvent::CHOIX_FAIT){
            // on fait du picking pour trouver les coordonnées cliquées sur la flotte
 
            // on a cliqué sur la flotte
            // on récupère les coordonnées du clic sur l'écran, dans le onEvent
            // on lance un rayon depuis la caméra jusque la position 3D du curseur -> getRayFromScreenCoordinates
            core::line3d< f32 > line = _smgr->getSceneCollisionManager()->getRayFromScreenCoordinates(
                core::position2d<s32> (_mouse.X, _mouse.Y), _camera);
 
            // on a un rayon de la caméra vers le point pointé
            // et on récupère l'intersection avec la flotte
            core::vector3df intersection;
            core::triangle3df tri;
 
            _nflotte2->setTriangleSelector(_selector);
            scene::ISceneNode *selected = _smgr->getSceneCollisionManager()->getSceneNodeFromRayBB(line);
            if (selected != NULL){
                string s((char*)(selected->getName()));
                if (s == "flotte2"){
 
                    // si on a cliqué quelque part sur le sélector
                    if (_smgr->getSceneCollisionManager()->getCollisionPoint(
                        line, _selector, intersection, tri)){
 
                        retour.first =  int(intersection.X - 240);
                        retour.second = int(intersection.Z + 235);
                        retour.first  /=40;
                        retour.second /=40;
 
                        // on vérifie qu'on n'a pas déjà tiré ici
 
                        if (find (_dans_leau.begin(), _dans_leau.end(), retour) == _dans_leau.end() &&
                            find (_mesdegats.begin(), _mesdegats.end(), retour) == _mesdegats.end()){
 
                            if (retour.first < 0 || retour.first >10) 
								cerr << "soucis dans choisir_attaque x : " << retour.first << endl;
                            if (retour.second < 0 || retour.second >10) 
								cerr << "soucis dans choisir_attaque z : " << retour.second << endl;
 
                            _receiver->_fini = IrrEvent::FINI;
                        }
                    }else
                        // on a cliqué ailleurs que sur le sélecteur
                        _receiver->_etat = IrrEvent::ATTENTE_CHOIX;
                }else
                    //  on a cliqué ailleurs que la flotte2
                    _receiver->_etat = IrrEvent::ATTENTE_CHOIX;
            }else
                // on n'a cliqué sur aucun noeud
                _receiver->_etat = IrrEvent::ATTENTE_CHOIX;
        }else
        // on n'a pas cliqué
 
        Sleep(30);
    }
 
    // on refait juste un affichage pour changer le texte
    _txt->setText(L" En attente du joueur ...");
    _txt->draw();
 
    if (_receiver->_fini == IrrEvent::FINI)
        return retour;
 
    // si on arrive là, c'est que la fenêtre a été fermée
    cerr << "le device a été fermé pendant le choix de l'attaque" << endl;
    return pair <int,int>(-1,-1);
}

De la même manière que dans l'interface avec SDL, il nous fait convertir toutes les coordonnées spatiales pour les ramener dans un intervalle [0;10], d'où les multiplications par 40 et les ajouts de 240,235...

La fonction d'écoute d'évènements associée à cet écran est très simple. Elle modifie le drapeau _etat lors d'un clic et elle garde à jour les coordonnées du curseur stockées dans l'interface Irrlicht.

 
Sélectionnez
bool IrrEvent::EventChoixAttaque(SEvent event){
    switch(event.EventType){
        case EET_KEY_INPUT_EVENT:{
            bougerClavier(event);
            return true;
        }
        case EET_MOUSE_INPUT_EVENT:{
            _parent->set_mouse(event.MouseInput.X,event.MouseInput.Y);
            if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN){
                if (_etat == ATTENTE_CHOIX)
                    _etat = CHOIX_FAIT;
            }
 
            return true;
        }
        default :
            return false;
    }
    return false;
}

D-V. Mise à jour de l'écran

Après chaque appel réseau, l'état du jeu peut avoir changé. Et d'ailleurs, il y a souvent un objet à rajouter dans la scène : un indicateur de coup dans l'eau ou bien un indicateur de dégâts. Ces indicateurs sont simplement des sphères blanches ou rouges positionnées aux positions correspondantes. C'est le rôle de la fonction update.

 
Sélectionnez
void IrrInterface::update(Essais  e, Flotte f){
    // il faut vérifier que tout Essais est bien présent
    // il ne peut y avoir que le dernier élément de Essais qui risque d'être manquant
    int nb_eau    = e.get_size("dans l'eau");
    int nb_degats = e.get_size("touches");
    int nb_plouf  = e.get_size("ploufs adverses");
 
    //ce que j'ai mis dans l'eau
    if (nb_eau!=0){
        pair<int,int> coup = e.get("dans l'eau", nb_eau-1);  // coup est dans (0,10)²
 
        // si le dernier élément dans l'eau est déjà présent, alors c'est bon, sinon ...
        if (find(_dans_leau.begin(), _dans_leau.end(), coup) == _dans_leau.end()){
            scene::ISceneNode *n = _smgr->addAnimatedMeshSceneNode(_mcube);
            n->setScale(core::vector3df(2,2,2));
            n->setMaterialTexture( 0, _driver->getTexture("../data/irr/blanc.jpg") );
 
            // on place le noeud
            n->setPosition( core::vector3df(coup.first*40+240 + 10, 0, coup.second*40-235 + 10));
            _dans_leau.push_back(coup);
        }
    }
 
    // les coups que l'adversaire a mis dans l'eau
    // idem pour _ploufs_adverses
    // ... 
 
    // ce que j'ai touché chez l'adversaire
    // idem pour _touches
    // ...
 
    // ce que le joueur a touché chez moi, les données sont stockées dans les bateaux
    // idem à part que les données ne sont pas dans Essais mais dans Flotte
    // ...
 
    // on fait 2 affichages pour voir le résultat immédiat
    _driver->beginScene(true, true,  video::SColor(255,100,100,100));
    _smgr->drawAll();
    _env->drawAll();
    _driver->endScene();
 
    _driver->beginScene(true, true,  video::SColor(255,100,100,100));
    _smgr->drawAll();
    _env->drawAll();
    _driver->endScene();
}
 

On remarquera qu'on fait 2 affichages à la fin de la fonction pour mettre à jour l'écran. Pour une raison qui m'échappe, un seul affichage ne suffit pas.

Image non disponible

D-VI. Les écrans finaux

Voilà, il nous reste à afficher les deux écrans Perdu/Gagné. Ces écrans sont on ne peut plus simples. Il y a une simple image en arrière plan et un écouteur d'évènement qui va arrêter le jeu sur un clic. Voici quand même le code :

 
Sélectionnez
void IrrInterface::affiche_gagne(){
    _receiver->_ecran = IrrEvent::ECRAN_FINI;
    _receiver->_fini = IrrEvent::PAS_FINI;
    _smgr->clear();
    IGUIImage *img = _env->addImage(
                    _driver->getTexture("../data/irr/gagne.jpg"),
                    irr::core::position2d<int>(0,0));
 
    while (_device->run()  && _receiver->_fini != IrrEvent::FINI){
        _driver->beginScene(true, true,  video::SColor(255,100,100,100));
 
        _env->drawAll();
 
        _driver->endScene();
        Sleep(50);
    }
}
 
Sélectionnez
bool IrrEvent::EventFini(SEvent event){
    switch(event.MouseInput.Event){
        case EMIE_LMOUSE_PRESSED_DOWN:
            _fini = FINI;
            return true;
        default:
            return true;
    }
 
    return false;
}
Image non disponible

D-VII. Lecture bloquante non bloquante

Les plus attentifs auront remarqué que lorsqu'un joueur est en attente de l'autre joueur, son interface est complètement gelée, lecture bloquante oblige. Ca n'est pas très grave dans le cas de notre interface SDL puisque celle ci est statique, mais c'est davantage problématique dans le cas de l'interface Irrlicht. En effet, il faut que les objets mobiles continuent à bouger, il faut que l'eau continue à onduler. La lecture socket doit donc devenir une opération non bloquante. J'ai choisi de réaliser ça par un thread.

La fonction defend (puisque c'est surtout dans celle-ci que ça se passe) va créer un thread pour s'occuper de la réception. Ainsi, dès que la fonction defend sera appelée, elle va rendre la main à la fonction suivante, qui dans notre cas est la fonction d'attente. C'est le thread, qui reste bloqué sur la lecture, qui mettra à jour un booléen pour terminer la fonction d'attente. Ainsi, on peut continuer à avoir une boucle d'affichage dans la fonction d'attente, tout en étant bloqué par la lecture de la socket.

Je n'ai pas utilisé d'objets systèmes tels que des mutex ou des sémaphores mais un simple booléen. En effet, seul le thread modifiera le booléen. La fonction d'attente ne fera que le lire. Et dans le cas d'un accès concurrent, tout ce qu'on risque c'est que la boucle d'attente tourne une fois de trop, donc une frame de plus.

Vous pouvez bien évidemment gérer le thread de la manière qui vous plaît. J'ai choisi d'utiliser la méthode décrite dans la FAQ C++ pour appeler une fonction membre comme thread. Il ne s'agit que d'un point mineur. Pour plus de précisions, consultez la faq ou le code source, dans Joueur::ThreadLauncher.


précédentsommairesuivant

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

  

Copyright © 2006 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.