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

Pierre SCHWARTZ

Ce tutoriel va montrer comment réaliser un jeu de bataille navale en réseau. J'y expliquerai une architecture de classes permettant de s'abstraire complètement du côté graphique, et de l'aspect réseau. Ce jeu de bataille navale devra permettre de jouer en réseau à la bataille navale et de changer l'interface graphique. Prérequis : C++, un peu de SDL et un peu de Irrlicht. ♪

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

- Auteur : Pierre SCHWARTZ

- Parution : 16 janvier 2007

- Licence : 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.

Sommaire

- A. Architecture globale

- A-I. Architecture en couches

- A-II. Écriture abstraite du jeu

- A-II-1. Classe abstraite de la couche de présentation

- A-II-2. Classes de gestion des données

- A-II-3. Écriture un peu moins abstraite du jeu

- B. Couche basse : le réseau

- B-I. Les sockets

- B-II. Échange des données au début du jeu

- B-III. Les échanges de données au cours du jeu

- C. Couche haute : l'affichage SDL

- C-I. Écran de choix Client/Serveur

- C-II. Écran de placement des bateaux

- C-III. Écran du jeu proprement dit

- C-IV. L'écran final

- C-V. Quelques points à retenir

- D. Couche haute : l'affichage Irrlicht

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

- D-II. Écran de choix Client/Serveur

- D-III. Écran de placement des bateaux

- D-IV. Écran du jeu proprement dit

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

- D-VI. Les écrans finaux

- D-VII. Lecture bloquante non bloquante

- E. On revient au jeu dans sa globalité

- E-I. Fonctionnement du jeu

- E-II. Téléchargement

- E-III. Remerciements

A - Architecture globale

A-I - Architecture en couches

Si on veut pouvoir séparer les différents aspects du jeu, il va nous falloir une architecture en couches. Le jeu sera donc organisé en 3 couches : la couche réseau, la couche de manipulation des données et la couche d'affichage. On peut même voir cette architecture comme l'architecture OSI d'un réseau : les cinq premières couches correspondent à la couche réseau (on se base sur la couche 5 : socket), la couche 6 correspond à la couche de manipulation des données et la dernière couche (7, présentation) s'occupe de l'interface graphique. Il suffira de remplacer une couche par une autre pour changer un aspect du jeu. Par contre nous avons une contrainte : les couches doivent être définies comme des interfaces implémentées par les versions des couches.

Je vais détailler la création de deux versions de la couche de présentation : une version en utilisant SDL et une version utilisant Irrlicht.

Voici le schéma de notre architecture :

La réflexion doit se faire sur la manipulation des données du jeu et non sur la manière de les afficher. Il faut déterminer tous les états dans lesquels le jeu pourra se trouver. La couche de présentation permettra les opérations suivantes :

- choisir si le joueur veut créer une partie réseau ou bien en rejoindre une ;

- positionner les bateaux sur la grille de jeu ;

- choisir la case de la grille à attaquer ;

- afficher le résultat du jeu.

La classe abstraite pour notre couche de présentation devra donc proposer ces différentes méthodes. Et elles seront implémentées dans les classes filles. Ainsi, toute classe implémentant ces méthodes pourra être utilisée comme couche de présentation. On pourra tout à fait imaginer une couche qui gère tout par des fichiers et qui n'affiche rien. Ça n'influera pas le moins du monde.

A-II - Écriture abstraite du jeu

Le jeu doit pouvoir être conçu sans avoir à tenir compte de la couche de présentation. Tout ce qu'on a le droit de faire, c'est d'appeler les méthodes virtuelles de l'interface.

créer la couche de présentation comme dérivée de l'interface
choisir le mode client ou serveur
chacun positionne ses bateaux sur sa grille

selon ce mode client ou serveur, créer ou rejoindre une partie
tant que les deux joueurs ont encore des bateaux
lancer le jeu, avec des attaques à tour de rôle entre les joueurs

afficher le résultat perdu ou gagné

A-II-1 - Classe abstraite de la couche de présentation

Voici la description de l'interface pour la couche de présentation :

class Gfx{
public:
virtual int choose_client_server() = 0;
virtual int positionner_bateaux(Flotte&) = 0;
virtual std::pair<int,int> choisir_attaque(Essais, Flotte)=0;
virtual void affiche_gagne()=0;
virtual void affiche_perdu()=0;

virtual void update(Essais, Flotte)=0;
virtual ~Gfx(){}
};

On remarquera que nulle part il n'est question de SDL ou de Irrlicht. C'est une classe abstraite. Les liens avec la couche inférieure (la couche de manipulation des données) sont faits par les passages de paramètres de type Essais et Flotte.

A-II-2 - Classes de gestion des données

Les deux classes Flotte et Essais sont les classes permettant de gérer toutes les données du jeu. Flotte gère tous les bateaux du joueur, leur position, leurs dégâts. Tandis que Essais gère ce que le joueur a déjà attaqué chez l'adversaire et ce que l'adversaire a déjà essayé chez le joueur.

Le code de ces deux classes est très classique, il ne contient que de la manipulation et du stockage d'informations :

class Flotte{
public:
std::vector<Bateau> _lb;
void placer_aleatoirement();
int get_effectif();

int get_effectif_depart();
private:
int effectif_depart;
};

La classe Flotte contient une fonction intéressante : placer_aleatoirement. Toujours dans un souci de simplicité je place les bateaux aléatoirement sur la grille. On se contente de chercher un arrangement des bateaux de telle sorte qu'ils ne sortent pas de la grille et qu'ils ne se chevauchent pas.

La classe Bateau est toute aussi triviale. Elle stocke la position, l'orientation, la taille et les dégâts d'un bateau ainsi que les fonctions associées. J'ai choisi de ne stocker que la position du début d'un bateau. La taille et l'orientation nous permettent de déduire les autres positions.

class Bateau{
public:
Bateau();
Bateau(std::string&, std::pair<int,int>, int, bool);

// touché ou dans l'eau ?
bool isHit(std::pair<int,int>&);

// modifie un point du bateau. si tout est touché, on renvoie true = coulé
bool setDegats(std::pair<int,int>&);

// accesseurs, modificateurs
// ...

private:
std::string _nom;

std::pair<int,int> _pos1;

// pour se souvenir de l'orientation du bateau
bool _horizontal;

int _taille;

std::vector<bool> _degats;
};

La classe Essais est un conteneur de tableaux, rien de très extraordinaire :

class Essais{
public:
Essais();

// renvoie le nombre de coups au but
int get_effectif_adverse();

// renvoie un booléen pour savoir si un pair est dans la map
bool is_in(const std::string&, std::pair<int,int>&);

// ajoute un élément dans un vector de la map
void add(const std::string&, std::pair<int,int>&);

// renvoie la taille d'un vector
int get_size(const std::string&);

// renvoie un élément de l'un des vectors
std::pair<int,int> get(const std::string&, int);

private:
// les données. plusieurs vectors référencés par leur nom
std::map<std::string, std::vector< std::pair < int,int > > > _d;
};

Je vous fais grâce du code de ces fonctions, tout se retrouve dans l'archive contenant les fichiers sources.

Avec simplement ces classes, on peut écrire tout le jeu (du moins les couches manipulation des données et présentation).

A-II-3 - Écriture un peu moins abstraite du jeu

Je vous remets le schéma général du jeu :

créer la couche de présentation comme dérivée de l'interface
choisir le mode client ou serveur
chacun positionne ses bateaux sur sa grille

selon ce mode client ou serveur, créer ou rejoindre une partie
tant que les deux joueurs ont encore des bateaux
lancer le jeu, avec des attaques à tour de rôle entre les joueurs

afficher le résultat perdu ou gagné

Si on met ça en C++, ça va nous donner quelque chose comme :

// création de la couche de présentation
Gfx *ma_gui = new IrrInterface; // ici une interface Irrlicht

// choix client ou serveur
int server = ma_gui->choose_client_server();

switch(server){
case 0:{ // serveur
Server s;
// création du serveur, attente du client, échange d'informations
// ...

// le serveur attend que le client ait positionné ses bateaux
s.wait();

// le serveur positionne ses bateaux sur sa grille
if (ma_gui->positionner_bateaux(f)==1){ // on souhaite quitter
break;
}

// déroulement du jeu, détaillé par la suite
// ...

// quelqu'un a perdu, on affiche un écran
// if perdu
ma_gui->affiche_perdu();
//else
ma_gui->affiche_gagne();

break;
}

case 1:{ // client
Client c;
// création du client, connexion au serveur, échange d'informations
// ...

// le client positionne ses bateaux sur sa grille
ma_gui->positionner_bateaux(f);

// le client informe le serveur qu'il a placé ses bateaux
c.ok();

// déroulement du jeu, détaillé par la suite
// ...

// quelqu'un a perdu, on affiche un écran
// if perdu
ma_gui->affiche_perdu();
//else
ma_gui->affiche_gagne();

break;
}

case 2 : // quitter
break;
}

delete ma_gui;

Bien, arrivés ici, le jeu tourne (enfin, sur le papier), les données sont correctement mises à jour (dans la classe de manipulation des données - Flotte et Essais), on peut maintenant s'occuper un peu du réseau pour gérer la couche basse.

B - Couche basse : le réseau

B-I - Les sockets

Toute la communication réseau se fera au travers de sockets. Pour des raisons de simplicité, je me suis appuyé sur une classe de socket permettant d'utiliser facilement des objets strings. Chaque joueur va instancier sa couche basse en client ou en serveur.

Voici la classe de base dont vont hériter les classes Client et Serveur :

class Joueur{
public:
Joueur(Gfx*);
void attaque(std::pair<int,int>, Essais&);
void defend(Flotte*, Essais*);
void wait();
void ok();

~Joueur();

// plus une fonction définie en thread
// ...

protected:
Gfx *_la_gui; // pointeur vers l'interface graphique
Socket *_s;
};

Les fonctions wait et ok servent juste à synchroniser les deux joueurs. J'utilise des sockets bloquantes, on peut donc synchroniser avec des simples envois/réceptions.

La fonction attaque envoie des coordonnées à attaquer, attend la réponse de l'adversaire et met à jour la structure Essais passée par référence. Idem pour la fonction defend.

Chaque fois qu'un joueur appellera la fonction attaque, il faudra que l'adversaire soit en bloqué sur la réception dans la fonction defend.

Les classes filles Client et Serveur sont sans surprise :

class Client : public Joueur{
public:
Client(Gfx*g):Joueur(g){};
//                  host        port
int connect(const std::string& , int);
//              filename
int connect(const std::string&);
Flotte get_game_data();
};

class Server : public Joueur{
public:
Server(Gfx *g):Joueur(g){};
int open_socket(int);
void wait_for_client();
Flotte get_game_data();
int send_game_data_to_client(Flotte&);
~Server();
private:
SocketServer *ze_socket;
};

On remarque les fonctions get_game_data et send_game_data_to_client qui vont réaliser l'échange de données au début du jeu. Cet échange du serveur vers le client nous garantit que les deux joueurs joueront avec les mêmes données, choisies par le serveur. Les données échangées sont simplement le nombre et la taille des bateaux à placer sur la grille.

Le serveur peut acquérir ces données de la manière qui lui plaît, j'ai choisi de les lire depuis un simple fichier texte.

Les deux fonctions Client::connect permettent simplement de choisir l'adresse de connexion. La première version spécifie en dur l'adresse et le port du serveur tandis que la seconde version va chercher ces données dans un fichier texte.

B-II - Échange des données au début du jeu

L'échange des informations du serveur vers le client au début du jeu est on ne peut plus classique : on se contente d'envoyer le nom du bateau et sa taille, et ce pour chaque bateau. On pourrait améliorer l'échange de données en n'envoyant que le nombre de bateaux de chaque type.

int Server::send_game_data_to_client(Flotte &f){
// on attend que le client soit prêt à recevoir
wait();

// il faut envoyer la flotte au client
std::vector<Bateau>::iterator i;
string a_envoyer;

// on envoie le nombre de lignes qu'on va envoyer
ostringstream oss;    oss << f._lb.size();    a_envoyer = oss.str();
_s->SendLine(a_envoyer);

// on boucle sur tous les bateaux
for (i = f._lb.begin(); i!=f._lb.end(); i++){
ostringstream oss;
oss << i->get_taille();
// on envoie le nom du bateau et sa taille
a_envoyer = i->get_nom() + ":"+ oss.str();
cout << a_envoyer << endl;
_s->SendLine(a_envoyer);
}

cout << "Données envoyées au client" << endl;
cout << "########################" << endl;

return 0;
}

La fonction de réception côté client est exactement la duale :

Flotte Client::get_game_data(){
// on avertit le serveur qu'on est prêt
ok();

Flotte f;
string recu;
recu = _s->ReceiveLine();
istringstream iss(recu);
int nb_a_recevoir;
iss >> nb_a_recevoir;

// on boucle sur chaque bateau à recevoir
for (int i=0; i<nb_a_recevoir; i++){
recu = _s->ReceiveLine();
cout << recu << endl;

// séparation des données pour créer la flotte
// on découpe la chaîne suivant le :
string nom;
string taille;
int itaille;
istringstream iss(recu);
std::getline( iss, nom, ':' );
std::getline( iss, taille, ':' );

istringstream staille(taille);
staille >> itaille;

// on rajoute les bateaux dans la flotte de départ, ils ne sont pas positionnés
Bateau b(nom, pair<int,int>(0,0), itaille, false);
f._lb.push_back(b);
}

cout << "Données du jeu reçues" << endl;
cout << "#######################" << endl;

return f;
}

On remarquera que les coordonnées des bateaux ne sont pas envoyées. D'une part ça n'est pas utile, mais aussi le principe de la bataille navale et de ne pas dire où sont placés les bateaux :)

B-III - Les échanges de données au cours du jeu

L'autre échange qu'il peut y avoir dans le jeu de bataille navale concerne chaque action des joueurs. Chaque fois qu'un joueur décide d'attaquer une case, on envoie les coordonnées à l'adversaire qui va renvoyer le résultat.

void Joueur::attaque(pair<int,int> c, Essais &e){

string r;
// on passe le pair en 2 string et on les envoie
ostringstream oss1, oss2;
oss1 << c.first;
_s->SendLine(oss1.str());
oss2 << c.second;
_s->SendLine(oss2.str());

// on récupère le résultat de l'attaque
r = _s->ReceiveLine();

// on interprète
if (r == "dans l'eau\n")
e.add("dans l'eau", c);
else // touché ou coulé
e.add("touches", c);
}

Et la fonction duale :

void Joueur::defend(Flotte &f, Essais &e){
string r;

// Réception des coordonnées
r = _s->ReceiveLine();
istringstream iss1(r);
int n1;  iss1 >> n1;
r = _s->ReceiveLine();
istringstream iss2(r);
int n2;  iss2 >> n2;

// Création de la coordonnée
pair<int,int> coup(n1,n2);

// est-on touché ?
int num = f._lb.size();
for (int i=0; i<num; i++){
if (f._lb[i].isHit(coup)){
// Est-ce que le bateau est coulé ?
if (f._lb[i].setDegats(coup)){
r = f._lb[i].get_nom()+" coule";
_s->SendLine(r);
return;
}else{
// Sinon il est simplement touché !
r = "touche";
_s->SendLine(r);
return;
}
}
}

// Rien n'a été touché
_s->SendLine("dans l'eau");
e.add("ploufs adverses", coup);
}

Pour changer la couche basse, il suffira de réimplémenter les classes Socket et Joueur pour par exemple utiliser SDL_net ou Qt au lieu de winsock comme c'est le cas actuellement.

Passons maintenant à la partie la plus intéressante : la couche haute dont je vais expliquer deux versions : SDL et Irrlicht.

C - Couche haute : l'affichage SDL

Vous pouvez déjà trouver pas mal de documentation sur SDL sur Internet : sur le site officiel libsdl.org ou bien ici même sur developpez.com.

Pour réaliser une couche haute pour l'affichage, il nous faut implémenter les fonctions virtuelles de la classe interface Gfx.

class SDLinterface : public Gfx{
public:
SDLinterface();
int choose_client_server();
int positionner_bateaux(Flotte&);
std::pair<int,int> choisir_attaque(Essais, Flotte);
void update(Essais, Flotte);
void affiche_gagne();
void affiche_perdu();

virtual ~SDLinterface();
private:
void dessiner_2_grilles(Essais, Flotte);

// tous les sprites
SDL_Surface *screen;
SDL_Surface *creer, *rejoindre, *quitter, *fond1, *placez, *instructions, *fond_grille, *bateau, *interface_sdl,
*fond_jeu, *petit_bateau, *degats, *plouf, *a_vous, *en_attente, *gagne, *perdu;
};

On remarque qu'il y a toutes les fonctions de Gfx, mais aussi des fonctions propres à l'interface SDL. On remarque aussi tous les attributs de type SDL_Surface * pour stocker tous les sprites que nous allons utiliser dans le jeu. Les sprites sont chargés dans le constructeur et sont détruits dans le destructeur.

C-I - Écran de choix Client/Serveur

Je me suis appuyé sur le tutoriel de fearyourself sur la réalisation de menus en SDL pour réaliser le premier écran du jeu : le choix entre le mode client et le mode serveur.

Pour résumer :

- on charge les sprites relatifs aux différents items du menu en spécifiant un indice de transparence ;

- on positionne les sprites ;

- on crée des rectangles aux dimensions et positions des sprites ;

- lors d'un clic on vérifie si on se situe dans l'un de ces rectangles pour déclencher une procédure.

int SDLinterface::choose_client_server(){
// on affiche un menu
SDL_BlitSurface(fond1, NULL, screen, NULL);

// on positionne les boutons
SDL_Rect r;
r.x = 160; r.y = 100;    SDL_BlitSurface(creer, NULL, screen, &r);
SDL_Rect r0; r0.w = creer->w;     r0.h=creer->h;      r0.x=r.x;   r0.y=r.y;

r.x = 97; r.y = 200;     SDL_BlitSurface(rejoindre, NULL, screen, &r);
SDL_Rect r1; r1.w = rejoindre->w; r1.h=rejoindre->h;  r1.x=r.x;   r1.y=r.y;

r.x = 281; r.y = 400;    SDL_BlitSurface(quitter, NULL, screen, &r);
SDL_Rect r2; r2.w = quitter->w;   r2.h=quitter->h;    r2.x=r.x;   r2.y=r.y;

SDL_Flip(screen);

// on lance une boucle pour attraper un clic sur un bouton
bool clic_correct = false;
int numero_clique=-1;
SDL_Event event;
while (!clic_correct){
SDL_WaitEvent(&event);
switch(event.type){
case SDL_QUIT:
clic_correct = true;
numero_clique = 2;
break;

case SDL_MOUSEBUTTONUP:
if (event.button.x > r0.x && event.button.x < r0.x+r0.w && event.button.y > r0.y && event.button.y < r0.y+r0.h){
numero_clique = 0;
clic_correct = true;
}else
if (event.button.x > r1.x && event.button.x < r1.x+r1.w && event.button.y > r1.y && event.button.y < r1.y+r1.h){
numero_clique = 1;
clic_correct = true;
}else
if (event.button.x > r2.x && event.button.x < r2.x+r2.w && event.button.y > r2.y && event.button.y < r2.y+r2.h){
numero_clique = 2;
clic_correct = true;
}
break;
}
SDL_Flip(screen);
}

// 1 = client
// 0 = serveur
// 2 = quitter
return numero_clique;
}

Ce qui nous affiche :

C-II - Écran de placement des bateaux

L'étape suivante est d'implémenter la fonction permettant de placer les bateaux sur la grille. J'ai choisi une grille 11x11 (allez savoir pourquoi). Une pression sur la touche A réarrange les bateaux sur la grille et une pression sur Entrée valide le choix et passe à l'écran suivant. Je choisis d'afficher un sprite carré sur chaque position occupée par un bateau.

int SDLinterface::positionner_bateaux(Flotte& f){

// on affiche la grille et le texte
SDL_BlitSurface(fond_grille, NULL, screen, NULL);
SDL_Rect r;
r.x = 150; r.y = 1;    SDL_BlitSurface(placez, NULL, screen, &r);
r.x = 544; r.y = 75;   SDL_BlitSurface(instructions, NULL, screen, &r);
SDL_Event event;

bool clic_correct = false;
while (!clic_correct){
SDL_WaitEvent(&event);
switch(event.type){
case SDL_QUIT:
clic_correct = true;
break;

case SDL_KEYDOWN:
switch(event.key.keysym.sym){
case SDLK_a:{
// on place aléatoirement les bateaux sur la grille
f.placer_aleatoirement();
// afficher le résultat du placement aléatoire
SDL_BlitSurface(fond_grille, NULL, screen, NULL);
r.x = 150; r.y = 1;    SDL_BlitSurface(placez, NULL, screen, &r);
r.x = 544; r.y = 75;   SDL_BlitSurface(instructions, NULL, screen, &r);
// et boucler pour afficher tous les petits carrés des bateaux
for (int i=0; i<int(f._lb.size()); i++)
for (int j=0; j<f._lb[i].get_taille(); j++){
r.x = (f._lb[i].is_horizontal()==true)  ? f._lb[i].get_pos().first  + j : f._lb[i].get_pos().first;
r.y = (f._lb[i].is_horizontal()==false) ? f._lb[i].get_pos().second + j : f._lb[i].get_pos().second;

r.x*=48;
r.y*=48;

// la grille commence au point 7,63
r.x+=7;
r.y+=63;

SDL_BlitSurface(bateau, NULL, screen, &r);
}

break;
}
case SDLK_RETURN:
clic_correct = true;
break;
default:
break;
}
break;
}
SDL_Flip(screen);
}
return 0;
}

On remarquera les opérations directement sur le placement des sprites sur l'image pour les câler au pixel près sur la grille. Chaque sprite fait 48 pixels de côté. Et en effet, les coordonnées stockées dans la couche de manipulation de données sont comprises entre 0 et 10, je dois les remettre à l'échelle pour les faire coller au sprite.

Ce qui nous donne :

C-III - Écran du jeu proprement dit

Cet écran permet au joueur de cliquer sur la case qu'il souhaite attaquer. Il permet aussi de visualiser d'un seul coup d'œil les endroits déjà attaqués, les coups au but, et les coups que l'adversaire a déjà faits.

On place à gauche les bateaux du joueur et à droite les bateaux de l'adversaire.

On se contente d'attendre un clic sur la grille de l'adversaire, de vérifier que la case cliquée n'a pas déjà été cliquée, puis on renvoie les coordonnées cliquées.

On remarquera que les grilles de jeu n'ont pas la même dimension que dans l'écran précédent, les opérations pour caler les sprites sur les pixels ne sont donc plus tout à fait les mêmes.

pair<int,int> SDLinterface::choisir_attaque(Essais e, Flotte f){
// on attend un clic sur une case, pas encore attaquée
pair<int,int> endroit;

dessiner_2_grilles(e,f);
SDL_Rect r;
r.x = 280;
r.y = 140;
SDL_BlitSurface(a_vous, NULL, screen, &r);
SDL_Flip(screen);

SDL_Event event;
bool clic_correct = false;
while (!clic_correct){
SDL_WaitEvent(&event);
switch(event.type){
case SDL_QUIT:
clic_correct = true;
endroit.first = endroit.second = -1;
break;
case SDL_MOUSEBUTTONUP:
if ((event.button.x > 410) && (event.button.y > 200)){
endroit.first = event.button.x-410;
endroit.second = event.button.y-200;

endroit.first/= 35;
endroit.second/=35;
if (endroit.first >= TAILLE_GRILLE)  endroit.first = TAILLE_GRILLE-1;
if (endroit.second >=TAILLE_GRILLE)  endroit.second= TAILLE_GRILLE-1;

bool deja_tire = false;
// on vérifie qu'on n'a pas déjà tiré ici
deja_tire = e.is_in("dans l'eau", endroit) || e.is_in("touches", endroit);

if (deja_tire)
clic_correct = false;
else
clic_correct = true;
}

break;
}
SDL_Flip(screen);
}

// redessiner l'écran avec le "en attente"
update(e,f);

return endroit;
}

On remarque l'appel à la fonction update qui met à jour l'écran avec le résultat de l'attaque (un coup dans l'eau ou un bateau touché). Cette fonction se contente d'afficher tous les sprites.

C-IV - L'écran final

Le dernier écran affiche simplement si le joueur a perdu ou gagné. Puis on attend qu'il appuie sur une touche pour quitter.

void SDLinterface::affiche_gagne(){
SDL_BlitSurface(fond1, NULL, screen, NULL);
SDL_Rect r;
r.x = 127;
r.y = 100;
SDL_BlitSurface(gagne, NULL, screen, &r);
SDL_Flip(screen);

SDL_Event event;
bool clic_correct = false;
while (!clic_correct){
SDL_WaitEvent(&event);
switch(event.type){
case SDL_QUIT:
clic_correct = true;
break;

case SDL_KEYDOWN:
clic_correct = true;
break;
}
}
}

L'affichage quand le joueur a perdu est sensiblement le même, on change juste le sprite à afficher.

C-V - Quelques points à retenir

- Tous les évènements sont directement gérés dans la boucle d'affichage. C'est la raison pour laquelle on se retrouve avec un gros switch dans chaque écran. Ça ne sera plus le cas dans l'affichage avec Irrlicht.

- La scène est reconstruite à chaque frame. À chaque frame on rajoute tous les sprites correspondant aux coups dans l'eau, aux bateaux, aux dégâts … ça ne sera plus le cas dans l'affichage avec Irrlicht.

- L'interface graphique avec SDL consomme très peu de ressources système, en effet, la boucle d'affichage ne tourne que lorsque des évènements sont détectés.

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

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.

E - On revient au jeu dans sa globalité

Maintenant qu'on a la couche haute, la couche basse et la couche de manipulation des données, on peut écrire le fil directeur du jeu plus en détail :

Gfx *ma_gui = new IrrInterface;

// choix client ou serveur
int server = ma_gui->choose_client_server();

switch(server){
case 0:{ // serveur
cout << "mode serveur démarré" << endl;
Server s;
// ouvrir la socket
s.open_socket(1000);

// attendre le client
s.wait_for_client();

// récupérer les données du jeu
Flotte f = s.get_game_data();

// envoyer les données du jeu au client
s.send_game_data_to_client(f);

// le serveur attend que le client ait positionné ses bateaux
s.wait();

// le serveur positionne ses bateaux sur sa grille
if (ma_gui->positionner_bateaux(f)==1){ // on souhaite quitter
break;
}

Essais e;

// début du jeu
// tant qu'il nous reste des bateaux
// et qu'on n'a pas touché tous ceux de l'adversaire
while (f.get_effectif() > 0 && f.get_effectif_depart()-e.get_effectif_adverse()>0){
pair<int,int> coup = ma_gui->choisir_attaque(e,f);
if (coup==pair<int,int>(-1,-1)) // on quitte
break;

// traitement du tir
s.attaque(coup, e);
ma_gui->update(e,f);

// on a gagné
if (f.get_effectif_depart()-e.get_effectif_adverse() ==0)
break;

s.defend(&f,&e);
ma_gui->attendre();
ma_gui->update(e,f);
}

// quelqu'un a perdu, on affiche un écran
if (f.get_effectif() == 0){
ma_gui->affiche_perdu();
cout << "terminé : perdu" << endl;
}else
if (f.get_effectif_depart()-e.get_effectif_adverse() == 0){
ma_gui->affiche_gagne();
cout << "terminé : gagné" << endl;
}

break;
}
case 1:
// le dual pour le client
// ...
delete ma_gui;

}

Et il vous suffit de changer la première ligne par l'instanciation d'une autre interface pour avoir tout à fait un autre jeu d'un point de vue graphique. Ensuite pour rendre les deux interfaces vraiment indépendantes, on peut tout à fait les compiler séparément et les utiliser au travers de bibliothèques dynamiques dll/so.

E-I - Fonctionnement du jeu

Pour fonctionner correctement, toutes les dll doivent être accessibles depuis l'exécutable. Il faut aussi que les données du jeu soient accessibles. Dans l'archive des sources, ce répertoire est accédé via ../data. Si vous changez l'emplacement de l'exécutable, assurez-vous que ce répertoire est toujours accessible. Au besoin, recompilez les sources avec un autre chemin.

Une partie doit être créée avant qu'un client ne tente de la rejoindre. Ça peut paraître évident, mais ça va mieux en le disant. Si vous jouez sur des réseaux séparés, assurez-vous de rediriger le port utilisé pour faire une redirection de port. Par défaut, le port utilisé est le port 1000.

Le joueur qui rejoint la partie est le premier à placer ses bateaux. Pendant qu'il les place, le serveur est bloqué en attente. Puis c'est au serveur de placer ses bateaux. Le serveur est le premier à jouer.

E-II - Téléchargement

Si vous souhaitez juste exécuter le jeu, il vous faut télécharger les DLL, les binaires et les données du jeu. L'archive des sources n'est utile que si vous voulez consulter le code et/ou le modifier. Le code source est distribué sous licence GPL.

- Les dll : Irrlicht.dll + SDL.dll + SDL_Image.dll + libpng12.dll 826 Ko

- Les sources + le projet Code::Blocks 32 Ko

- Les binaires Windows en version SDL et Irrlicht 581 Ko

- Les données du jeu, nécessaires à l'exécution 1 Mo

E-III - Remerciements

Je remercie Julp et fearyourself pour leur relecture et Léon Bérelle, qui m'a permis d'utiliser l'une de ses créations :