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


précédentsommairesuivant

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 :

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

 
Sélectionnez
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();
};
 
Sélectionnez
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. Echange 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 bateau de chaque type.

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

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

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

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


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.