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.
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 :
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. Écran de choix Client/Serveur▲
L'écran est très simple : une image en arrière-plan et trois 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.
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.
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
;
}
D-III. Écran 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 :
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.025
f);
_nflotte1 =
_smgr->
addWaterSurfaceSceneNode(_mflotte->
getMesh(0
), 3.0
f, 300.0
f, 30.0
f);
_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.0
f, 300.0
f, 30.0
f);
_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.
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. À 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.
_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.
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 :
Et dès que le joueur a validé son choix, on passe à l'étape suivante : le jeu proprement dit.
D-IV. Écran 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.
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.
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.
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 deux affichages à la fin de la fonction pour mettre à jour l'écran. Pour une raison qui m'échappe, un seul affichage ne suffit pas.
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 :
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
);
}
}
bool
IrrEvent::
EventFini(SEvent event){
switch
(event.MouseInput.Event){
case
EMIE_LMOUSE_PRESSED_DOWN:
_fini =
FINI;
return
true
;
default
:
return
true
;
}
return
false
;
}
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. Ça 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ème 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.