c++ apprendre et programmer é manipulation …+_apprendre-et... · 2017-04-20 · c++ pour les...

29
"C++ APPRENDRE ET PROGRAMMER" Sélection de Bonnes Feuilles -> extrait du Chap 5 : "Manipulation d’une matrice et classes" Août 2012

Upload: halien

Post on 12-Sep-2018

228 views

Category:

Documents


0 download

TRANSCRIPT

"C++ APPRENDRE ET PROGRAMMER" Sélection de Bonnes Feuilles -> extrait du Chap 5 :"Manipulation d'une matrice et classes"Août 2012

C e chapitre va être l’occasion de développer le jeu du labyrinthe, un programmeun petit peu plus évolué par rapport à ce que nous avons fait jusqu’ici. Nous

utiliserons en ce sens la bibliothèque graphique OpenGL.

Cette bibliothèque a été présentée au chapitre Graphique de données.

Le jeu du labyrinthe consiste à déplacer un bonhomme dans un niveau"labyrinthique", un peu à la manière d’un Pacman, le "fossile" du jeu vidéo. Desennemis donneront du fil à retordre au joueur afin d’éviter que cela ne devienne unparcours de santé. De plus, les déplacements de ces ennemis seront contrôlés parune intelligence artificielle d’une complexité extrême, proche du hasard, ce quiapportera un côté "imprévisible" au déroulement du jeu.

Le niveau du jeu sera vu "de dessus", en deux dimensions. Le joueur pourra faireévoluer son personnage dans le niveau en le contrôlant à l’aide des flèchesdirectionnelles de son clavier. Partant d’un point A, il essaiera de rejoindre unpoint B tout en évitant les ennemis sanguinaires rodant aux alentours.

Nous souhaitons que la création des niveaux de jeu soit facilement réalisable. Notreobjectif est d’offrir à quiconque la possibilité de s’en donner à cœur joie pour"designer" ses propres niveaux et y placer stratégiquement des ennemis. De plus, lataille des niveaux ne sera pas fixée, ce qui permettra de créer des plateaux de toutessortes : petits, grands, carrés, rectangulaires, etc.

5.1 Mise en place du projet

Configuration pour OpenGLCréez un nouveau projet vide de type Application Console Win32. Nommez-leLabyrinthe dans une nouvelle solution également appelée Labyrinthe. Créez-y unnouveau fichier source vierge, main.cpp par exemple. Incluez les fichiers d’en-têtehabituels et créez une fonction main vide. En employant GLUT, configurez le projetpour qu’il puisse accueillir cette bibliothèque.

Pour configurer le projet, reportez-vous au chapitre Graphique de données. Parailleurs, un récapitulatif complet, expliquant comment configurer un projet VisualC++ pour les bibliothèques OpenGL et GLUT, est consultable dans l’annexeConfigurer un projet pour GLUT et OpenGL.

Vérifiez que le fichier d’en-tête glut.h est bien inclus depuis votre fichier source.

#include "GL/glut.h"

Mise en place du projet 139

5

Mise en place des données principalesLa première donnée que nous pouvons déclarer est la taille du niveau. Nous auronsforcément besoin du nombre de colonnes et de lignes de la grille du niveau pourpouvoir le gérer. Déclarez deux valeurs entières globales.

int NbColonnes, NbLignes; // Taille du niveau

Dans la fonction principale du programme, commençons par affecter des valeursaux trois variables globales pour ne pas les laisser non initialisées (les affectationsmultiples du genre TailleX=TailleY=0 sont possibles). Créons la fenêtre OpenGL etdéfinissons la fonction LabyAffichage en tant que fonction d’affichage, et LabyRedim entant que fonction de redimensionnement.

Reportez-vous au chapitre Graphique de données pour savoir comment utiliserGLUT et créer une fenêtre OpenGL.

Listing 5-1 : Fonction principale du programme

void main(void){

NbColonnes=NbLignes=0; // Initialisation de la taille

/* Gestion de graphique */// Position de la fenêtreglutInitWindowPosition(10, 10);// Taille de la fenêtreglutInitWindowSize(500, 500);// Mode d'affichageglutInitDisplayMode(GLUT_RGBA | GLUT_SINGLE);// Création de la fenêtreglutCreateWindow("Labyrinthe");// Fonction d'affichageglutDisplayFunc(LabyAffichage);// Fonction de redimentionnementglutReshapeFunc(LabyRedim);

glutMainLoop();}

Bien entendu, nous devons définir ces deux fonctions d’événements dans notreprogramme. La fonction d’affichage ne doit posséder aucun paramètre. C’est là quetoutes les instructions d’affichage seront appelées. Nous effacerons tout d’abordl’écran de jeu afin de pouvoir commencer le dessin sur un support vierge, enspécifiant la couleur de fond en blanc. L’instruction glMatrixMode (que nous nedétaillerons pas dans ce chapitre) permet de spécifier la matrice active en vue de

140 Chapitre 5 - Manipulation d’une matrice et classes

disposer les éléments sur la scène lors du dessin. Après les instructions d’affichage,l’appel à glFlush permet de terminer le dessin de la scène.

Listing 5-2 : Fonction d’affichage

void LabyAffichage();

void LabyAffichage() {// Définit la couleur de fondglClearColor(1.0, 1.0, 1.0, 1.0);// Efface l'écranglClear(GL_COLOR_BUFFER_BIT);// Définit la matrice de modélisation activeglMatrixMode(GL_MODELVIEW);

/* Instructions d'affichage ici */

glFlush(); // Achève l'affichage}

Les fonctionnalités plus avancées d’OpenGL, comme la manipulation des matricesde modélisation, sont traitées au chapitre Initiation à la 3D temps réel : OpenGL.

La fonction de redimensionnement demande en paramètre deux valeurs entièrescorrespondant à la taille de la fenêtre (largeur/hauteur). L’instruction gluOrtho2Dpermet de spécifier l’échelle du dessin. Le premier couple (0.0, NbColonnes) permetde définir les coordonnées maximales horizontalement (gauche/droite) et (NbLignes,0.0) les coordonnées extrêmes verticalement (bas/haut, et non haut/bas !). Ainsil’appel à gluOrtho2D(0.0, NbColonnes, NbLignes, 0.0) spécifie le pixel de coordonnées(0, 0) comme étant le coin supérieur gauche de la fenêtre et le pixel (NbColonnes,NbLignes) le coin inférieur droit.

Listing 5-3 : Fonction de redimensionnement

void LabyRedim(int x, int y);

void LabyRedim(int x, int y) {glViewport(0, 0, x, y);glMatrixMode(GL_PROJECTION);glLoadIdentity();gluOrtho2D(0.0, (double)NbColonnes,

(double)NbLignes, 0.0);}

Mise en place du projet 141

5

5.2 Gérer le niveau : les matricesLes niveaux de jeu dans lesquels le joueur évoluera seront des labyrinthes, en deuxdimensions et "vus de dessus" comme dans Pacman ou Bomberman. Dans notreprogramme, nous stockerons ces informations dans un tableau, non pas à une seuledimension comme celui qui nous a servi à stocker N élèves (tableau "ligne"), mais àdeux dimensions (il contiendra donc des lignes et des colonnes).

Pour savoir comment stocker N élèves dans un tableau à une dimension, reportez-vous au chapitre Interface de saisie.

Pour tracer les niveaux, nous pourrions définir les tableaux "directement dans lecode", c’est-à-dire "en dur" comme on le dit en jargon de programmation. L’ennui decette solution provient du manque inhérent d’évolutivité et de souplesse duprogramme. Elle nous contraindrait à modifier le code du programme et donc à lerecompiler avec Visual, s’il devenait nécessaire de modifier la conception ou la tailled’un niveau. Un joueur ne pourrait pas, dès lors, créer ses propres niveaux de jeu.

Une solution beaucoup plus pratique, et évolutive, est de stocker les données d’unprogramme dans des fichiers externes. Dans n’importe quel jeu vidéo, les animationsdes personnages ne sont pas conservées en dur dans le programme. Elles sontstockées dans des fichiers externes spéciaux, édités à l’aide de logiciels d’animation(3D Studio, Maya…). De même pour les textures (les images plaquées sur lespolygones d’une scène 3D) sont généralement stockées dans des fichiers image detype .bmp, .jpg ou encore .tga.

Stocker les niveaux

Pour notre part, nous allons stocker les niveaux de notre jeu dans des fichiers textefacilement éditables à l’aide de n’importe quel éditeur de texte, tel le Bloc-notes.

Figure 5-1 :Fichier niveau.txt

142 Chapitre 5 - Manipulation d’une matrice et classes

Ouvrons donc le Bloc-notes de Windows et créons un nouveau fichier texte quenous nommerons niveau.txt. Nous allons devoir à présent "dessiner" le niveau de jeusous forme d’une grille de caractères. Choisissons par exemple le caractère 0 pourspécifier un mur, et le caractère 1 une allée (ou un couloir). Formons cette grille encomposant un assemblage astucieux de 0 et de 1. Voici un exemple de petit niveaulabyrinthique réalisable :

Sauvegardons ce fichier dans le dossier du projet Labyrinthe\Labyrinthe, à côté dufichier source de ce dernier.

Bien qu’étant stocké dans ce fichier texte, le contenu du niveau de jeu devra êtrerécupéré dans notre programme. Le type de données que nous allons utiliser pourstocker une grille de ce format est un tableau à deux dimensions : une dimensionpour les colonnes, et une dimension pour les lignes. Mathématiquement, cela senomme une matrice. Nous allons voir comment allouer un tableau de ce type.

De façon statique, un tableau à deux dimensions de 4 × 5 contenant des booléenss’alloue de la façon suivante :

bool tableau_statique[4][5];

Cependant, dans notre cas, ne connaissant pas la taille du niveau à l’avance, nousdevons passer par une allocation dynamique. Avant de pouvoir allouer cettematrice, nous devons connaître ses dimensions. Il serait possible de les détermineren lisant une première fois le contenu du fichier, puis en le lisant une deuxième foispour remplir le tableau. Mais il est plus simple et plus rapide d’écrire la taille de lamatrice directement dans le fichier où est stocké le niveau. Spécifions tout d’abordson nombre de colonnes, puis son nombre de lignes.

Figure 5-2 :Enregistrement

du fichier niveau.txt

Gérer le niveau : les matrices 143

5

Allouer un tableau à deux dimensionsUn tableau à une dimension est défini par un pointeur pointant sur la premièrecellule. Allouer un tableau d’entiers à l’aide de l’instruction int* tab = new int[N]aurait pour effet de créer N cellules pouvant accueillir chacune un entier.

Un tableau à deux dimensions est en réalité un tableau de tableaux, ce quicorrespond à un double pointeur. La marche à suivre pour allouer un tableau deC × L entiers est donc d’allouer un tableau de C cellules. Chaque cellule peutaccueillir un pointeur (simple) sur un entier. Il faut ensuite allouer un tableau deL entiers pour chacune de ces C cellules. Le schéma suivant représente uneallocation à deux dimensions :

Double pointeurCommençons par déclarer en global le double pointeur permettant d’identifier lamatrice du niveau. Un double pointeur se déclare à l’aide de deux étoiles. De lamême manière, un triple pointeur se déclare avec trois étoiles. Étant donné quel’information contenue dans un fichier texte est du texte, choisissons une matrice decaractères (type char) pour stocker les données du niveau. Cela simplifiera la lecture.

char** Matrice; // Matrice contenant le niveau

Figure 5-3 :Ajout de la taille

de la grille dans lefichier du niveau

Figure 5-4 :Schématisation

d’une allocationdynamique d’un

tableau à deuxdimensions

144 Chapitre 5 - Manipulation d’une matrice et classes

Initialisons ce double pointeur à la valeur NULL (il ne pointe sur rien) au début de lafonction main (vous pouvez tout à fait l’initialiser directement lors de sa déclaration).

Matrice = NULL; // Initialisation du double pointeur

Créons une fonction OuvrirNiveau qui permettra de charger un niveau. Elle prendracomme paramètre une chaîne de caractères correspondant au nom du fichiercontenant le niveau à charger. L’appel de cette fonction sera de la formeOuvrirNiveau("niveau.txt"). Une chaîne de caractères étant un tableau, et un tableauétant défini par un pointeur sur la première cellule, un paramètre chaîne decaractères est du type char*.

void OuvrirNiveau(char* nom_fichier);

void OuvrirNiveau(char* nom_fichier) // Définition{

// Instruction d'ouverture du niveau}

Tableau en paramètre

Passer un tableau (par exemple d’entiers) en paramètre à une fonction seréalise via la syntaxe int * nom. Il est également possible d’utiliser la syntaxeint nom[]. Ainsi les deux déclarations suivantes sont équivalentes :

void fonction1(int* tableau); // Ces deux déclarationsvoid fonction2(int tableau[]); // sont identiques

À l’intérieur de notre fonction, nous allons allouer la matrice Matrice avant de lire lefichier niveau.txt pour charger les informations contenues. Pour allouer cettematrice, nous devons connaître sa taille NbColonnes × NbLignes. Dans un premiertemps, fixons ces valeurs.

NbColonnes = 10;NbLignes = 8;

Allocation dynamiqueMaintenant que la taille de la matrice est fixée, nous pouvons allouer cette dernière.Comme nous l’avons mentionné précédemment, une allocation dynamique à deuxdimensions nécessite deux étapes :

j allouer un tableau de NbColonnes pointeurs sur char :

// Allocation du tableau du niveauMatrice = new char*[NbColonnes];

Gérer le niveau : les matrices 145

5

j allouer un tableau de NbLignes cellules (de type char) par pointeur :

for(int i=0; i< NbColonnes; i++)Matrice[i] = new char[NbLignes];

Il est préférable d’initialiser les valeurs contenues dans cette matrice à des valeurspar défaut comme le caractère 0, caractérisant un mur. La syntaxe pour accéder auxcellules d’un tableau à une dimension repose sur des crochets. Ici l’instructionMatrice[3] donne accès au tableau caractérisant la quatrième ligne de la matrice. Lasyntaxe pour accéder au caractère stocké à la quatrième ligne de la deuxièmecolonne est Matrice[1][3]. De la même façon, accéder aux cellules d’un tableau à troisdimensions, dans un jeu de Rubik’s Cube par exemple, se ferait ainsi : Rubiks[x][y][z].

Pour parcourir la matrice entièrement, c’est-à-dire pour initialiser chacune de sescellules, nous allons faire appel à une double boucle. Nous devons passer toutes lescolonnes en revue, et pour chacune de ces colonnes, parcourir les NbLignes lignes.Notez qu’il est également possible de faire l’inverse (c’est-à-dire parcourir d’abordles lignes puis les colonnes), cela ne change rien ici.

// Initialisation des valeurs du tableaufor(int i=0; i<NbColonnes; i++)

for(int j=0; j<NbLignes; j++)Matrice[i][j] = '0';

Boucle en bloc

Il est possible de programmer cette double boucle en spécifiant les blocs decette façon :

for(int i=0; i<NbColonnes; i++){

for(int j=0; j<NbLignes; j++){

Matrice[i][j] = ’0’;}

}

Mais cela n’est pas nécessaire. En effet, la boucle parcourant les lignescompte comme une seule instruction aux yeux de la première boucletraitant les colonnes.

Libérer la mémoire d’une matriceQui dit allocation dit désallocation ! Si nous ne respectons pas ce principe deréservation et de libération de mémoire, nous nous exposons à une "fuite de

146 Chapitre 5 - Manipulation d’une matrice et classes

mémoire". Préparons une fonction LibererMemoire chargée de supprimer le tableauMatrice.

Listing 5-4 : Déclaration et corps de la fonction de désallocation

void LibererMemoire();

/* … */

void LibererMemoire() // Définition{

/* Instructions de libération de mémoire */}

Une désallocation consiste en une allocation inverse. Une matrice est composéed’une ligne de pointeurs, chacun d’entre eux étant alloué d’une colonne. Il faut donctout d’abord libérer la mémoire occupée par les NbColonnes colonnes à l’aide d’uneboucle. Dans un deuxième temps, la libération de la ligne de NbLignes pointeurspermet de supprimer la totalité des données de la matrice.

Listing 5-5 : Libération de la mémoire allouée par une matrice

void LibererMemoire(){

// Vérifie que Matrice a bien été allouéeif(Matrice != NULL){

for(int i=0; i<NbColonnes; i++)// Libération des colonnesdelete [] Matrice[i];

// Libération de la ligne de pointeursdelete [] Matrice;

}}

Tester avant de désallouer

Avant de libérer de la mémoire allouée dynamiquement et accessible par unpointeur, mieux vaut tester que ce pointeur pointe effectivement sur unecase mémoire existante. Car tenter de libérer de la mémoire n’existant pasaura pour effet de faire planter le programme. C’est pourquoi avantd’appeler les instructions delete, nous avons testé au préalable que lamatrice a bien été allouée en comparant le double pointeur Matrice avec lavaleur NULL.

Une bonne question à se poser à présent est : quand doit-on appeler cette fonctionde libération de mémoire ? La libération de données allouées dynamiquement doit

Gérer le niveau : les matrices 147

5

être effectuée lorsque ces données ne sont plus utiles au programme. Ici nous nousservirons de la matrice du niveau pendant la totalité du jeu : la fonctionLibererMemoire est à appeler en fin de programme. Pour l’instant, nous ne savons pasquand elle peut intervenir, car le programme va être monopolisé jusqu’à la fin par laboucle de traitement de GLUT à partir de l’instruction glutMainLoop. Nous verronsoù appeler LiberationDonnees plus tard, quand nous connaîtrons la condition d’arrêtdu jeu.

Récupérer le contenu d’un fichier texteNous allons maintenant ouvrir le fichier où est stocké le niveau pour charger sesdonnées et remplir ainsi la matrice Matrice. Les fonctions utiles à la manipulation defichiers en C++ sont dans le fichier fstream, à inclure dans le projet.

#include <fstream>

Ouvrir le fichierAu début de la fonction OuvrirNiveau, déclarons une variable de type ifstream ("inputfile stream", "fichier en ouverture" en français). En réalité, ici nous déclarons, non pasune variable, mais un "objet". Un objet est une variable dont le type est une classe(nous traiterons les classes ultérieurement). Une classe est en quelque sorte une"structure plus évoluée", car en plus de pouvoir posséder des variables membres, ellepeut contenir des fonctions membres. Par exemple, les objets de type ifstreampossèdent une fonction membre open qui permet d’ouvrir le fichier passé enparamètre. L’objet fichier de type ifstream peut ouvrir le fichier toto.txt à l’aide del’instruction fichier.open("toto.txt"). Dans notre cas, c’est le paramètre passé à lafonction OuvrirNiveau qui correspond au nom du fichier à ouvrir.

ifstream fichier; // Objet de type ifstreamfichier.open(nom_fichier); // Ouverture en lecture seule

Si le fichier à charger n’existe pas ou si son nom est erroné, le programme ne pourrapas charger le niveau et donc fonctionner. Heureusement il est possible de tester sil’ouverture (et même l’écriture) d’un fichier s’est déroulée sans incident. Il suffitpour cela de vérifier la valeur de l’objet de type ifstream : si elle est fausse(équivalente à l’état false ou à la valeur numérique 0), cela signifie que le fichier nes’est pas ouvert correctement ; dans le cas contraire (si la valeur de l’objet est true ouégale à toutes valeurs numériques différentes de 0), le fichier est ouvert et prêt àl’emploi.

if(!fichier) { // Test de l'existence du fichiercout<<"Erreur lors de l'ouverture du fichier !"<<endl;system("pause");exit(1);

}

148 Chapitre 5 - Manipulation d’une matrice et classes

Lire dans le fichierMaintenant que le fichier est correctement ouvert en lecture, nous allons récupérerson contenu. Lire une information contenue dans un fichier et la stocker dans unevariable se réalise de la même façon qu’une récupération de saisie au clavier cin. Eneffet, la classe ifstream propose elle aussi l’utilisation de l’opérateur >> pour capterun flux entrant. Récupérons tout d’abord les dimensions du niveau en remplaçant laportion de code où nous spécifions temporairement ces valeurs.

// Lecture de la taille du niveaufichier >> NbColonnes;fichier >> NbLignes;

Une fois que la taille du niveau est connue, l’allocation dynamique et l’initialisationde la matrice doivent être réalisées. Ensuite, nous pourrons lire les donnéescontenues dans le fichier. Récupérons caractère par caractère les cellules de la grille.Mais attention : il est indispensable de parcourir tout d’abord les lignes, puis lescolonnes. En effet, le sens de lecture d’un fichier (qui est d’ailleurs le sens de lectureque nous connaissons et utilisons tous) est de la gauche vers la droite et de haut enbas.

A B C DE F G HI J K L

Imaginez que ce texte soit contenu dans un fichier. Lire ce fichier en parcourant lescolonnes avant les lignes reviendrait à capturer ceci :

A E IB F JC G KD H L

Programmons la double boucle qui parcourt les lignes puis les colonnes en capturantle caractère en cours dans la bonne cellule de la matrice.

// Lecture du tableau du niveau, caractère par caractèrefor(int j=0; j<NbLignes; j++)

for(int i=0; i<NbColonnes; i++)fichier >> Matrice[i][j];

Lorsqu’un fichier devient inutile, il est nécessaire de le fermer. Ce bon réflexecontribue à un programme propre et performant. Les objets de type ifstreampossèdent une fonction membre close, qui permet de fermer le fichier ouvert aprèsutilisation. Appelons donc cette fonction.

fichier.close(); // Fermeture du fichier

Gérer le niveau : les matrices 149

5

Voici un récapitulatif complet de la fonction d’ouverture du niveau OuvrirNiveau :

Listing 5-6 : Fonction OuvrirNiveau complète

void OuvrirNiveau(char* nom_fichier){

ifstream fichier; // Objet de type ifstreamfichier.open(nom_fichier); // Ouverture du fichier

if(fichier == 0) { // Test de l'existence du fichiercout << "Erreur lors de l'ouverture du fichier !"

<< endl;system("pause");exit(1);

}

// Lecture de la taille du niveaufichier >> NbColonnes;fichier >> NbLignes;

// Allocation du tableau du niveauMatrice = new char*[NbColonnes];for(int i=0; i<NbColonnes; i++)

Matrice[i] = new char[NbLignes];

// Initialisation des valeurs du tableaufor(int i=0; i<NbColonnes; i++)

for(int j=0; j<NbLignes; j++)Matrice[i][j] = '0';

// Lecture du tableau du niveau, caractère par caractèrefor(int j=0; j<NbLignes; j++)

for(int i=0; i<NbColonnes; i++)fichier >> Matrice[i][j];

fichier.close(); // Fermeture du fichier}

Maintenant nous devons appeler la fonction OuvrirNiveau pour ouvrir le niveau. Celava se passer dans la fonction main. Il est impératif d’effectuer cet appel avantl’instruction glutMainLoop, qui lance la boucle de traitement du jeu et n’en sortjamais.

OuvrirNiveau("niveau.txt"); // Ouverture de "niveau.txt"

150 Chapitre 5 - Manipulation d’une matrice et classes

Afficher le niveauÀ présent que la grille de jeu est correctement stockée en mémoire dans notretableau à deux dimensions Matrice, nous pouvons l’afficher. Il ne s’agit pas d’afficherla matrice de caractères brute telle quelle à l’écran, mais plutôt d’utiliser lesinformations contenues à l’intérieur pour dessiner un "joli décor" de jeu.

Actuellement, la fonction d’affichage ressemble à ceci :

Listing 5-7 : Fonction d’affichage OpenGL

void LabyAffichage(){

// Définit la couleur de fondglClearColor(1.0, 1.0, 1.0, 1.0);// Efface l'écranglClear(GL_COLOR_BUFFER_BIT);// Définit la matrice de modélisation activeglMatrixMode(GL_MODELVIEW);

/* Instructions d'affichage ici */

glFlush(); // Achève l'affichage}

Elle se contente d’effacer l’écran et de le remplir d’une couleur de fond blanche.

Principe d’affichage du décorPour tracer le niveau de jeu à partir de la matrice de caractères, plusieurs solutionssont envisageables. Il serait possible de tracer des lignes entourant les "blocs demurs", ou alors au contraire de tracer des lignes délimitant les allées. Mais lasolution la plus simple, qui est d’ailleurs celle que nous allons utiliser, consiste à"remplir" les cellules occupées par un mur en dessinant un carré de couleur à leurposition. La couleur du fond est blanche ; c’est celle des allées. Pour la couleur desmurs, choisissons par exemple une couleur grise à l’aide de la fonction glColor3d :(0, 0, 0) étant le noir et (1, 1, 1) étant du blanc, (0.5, 0.5, 0.5) fera l’affaire. Nousallons ensuite annoncer un ordre d’affichage avec glBegin. Pour dessiner des carrés,utilisons le paramètre GL_QUADS (pour "quadrilatère"). À partir de maintenant,chaque "groupe" de quatre points (vertices) formera un quadrilatère. Parcouronstoutes les cellules de la matrice et traçons un quadrilatère gris si la cellule en coursest un mur (donc si le caractère stocké est 0). De façon générale, les vertices àdéfinir (à l’aide de glVertex2d) pour tracer le carré correspondant à la cellule d’indice[i][j] ont pour coordonnées (i, j) (i, j+1) (i+1, j+1) (i+1, j).

Gérer le niveau : les matrices 151

5

Fonction d’affichageProgrammons le code nécessaire à l’affichage du niveau dans une nouvelle fonctionpour éviter que LabyAffichage ne soit trop longue.

Listing 5-8 : Fonction d’affichage du niveau

void DessinerNiveau(); // Déclaration en début de fichier

void DessinerNiveau() // Définition{

glColor3d(0.5, 0.5, 0.5); // Couleur grise

// Commence l'affichage de quadrilatèresglBegin(GL_QUADS);// Parcourt toutes les cellules de la matricefor(int i=0; i<NbColonnes; i++)

for(int j=0; j<NbLignes; j++)// Si c'est un mur, on dessine un carréif(Matrice[i][j] == '0'){

// Place les points du carréglVertex2d(i, j);glVertex2d(i, j+1);glVertex2d(i+1, j+1);glVertex2d(i+1, j);

}glEnd(); // Achève l'affichage

}

Appelons cette fonction depuis la "fonction d’affichage principale" LabyAffichage,juste avant l’instruction glFlush.

void LabyAffichage(){

/* … */// Définit la matrice de modélisation activeglMatrixMode(GL_MODELVIEW);DessinerNiveau(); // Affiche le niveauglFlush(); // Achève l'affichage

}

Figure 5-5 :Correspondanceentre l’indice de

la cellule et laposition des

vertices à relier

152 Chapitre 5 - Manipulation d’une matrice et classes

Disproportion

L’affichage peut sembler disproportionné. Cela vient du fait que la grille duniveau créée est rectangulaire, tandis que la fenêtre GLUT est carrée. Si ladéformation vous gêne, il suffit de redimensionner manuellement la fenêtreafin d’obtenir un rapport de taille correct.

5.3 Classe JoueurLe niveau est défini, chargé et tracé. Mais il est vide. L’intérêt est de pouvoir évoluerdans ce niveau en y déplaçant un personnage. C’est ce que nous allons traiter danscette section.

Classes et autres concepts afférents

Concept de classeJusqu’à maintenant, tout ce que nous avons vu ne relève pas vraiment de la"programmation orientée objet" (ou POO) du langage C++. La notion de classe quenous allons présenter va nous permettre d’introduire cette nouvelle approche de laprogrammation.

Figure 5-6 :Affichage duniveau de jeu

Classe Joueur 153

5

Une classe est un nouveau type de données qui contient des données et desméthodes. Les méthodes permettent de travailler avec les données et de lesmanipuler. En d’autres termes, les données d’une classe sont des variables (desentiers, des flottants…) ou des objets (des objets d’autres classes). Les méthodes decette classe sont des fonctions utilisables.

Un objet est une instance d’une classe. Les termes "instance d’une classe C" et "objetdu type C" sont équivalents. Par exemple, dans une déclaration de variable int var;, int(qui est un type) est comparable à une classe, et var (qui est une variable) à uneinstance ou à un objet.

Cela rappelle les structures. En effet, les classes sont des structures plus évoluées.Par convention, les structures sont utilisées pour stocker seulement des données. Lesclasses ajoutent à cela la possibilité d’inclure des méthodes, de spécifier descatégories de membres, et le concept d’héritage.

Nous reviendrons sur l’héritage au chapitre Hiérarchie de classes et listes chaînées.

La syntaxe de déclaration d’une classe est la suivante :

Listing 5-9 : Syntaxe de déclaration d’une classe

class nom_de_la_classe{

/* Tout ce qui est déclaré ici fait partie de la classe */

}; // Ne pas oublier le point-virgule !

Visibilité des donnéesToutes les données et méthodes déclarées dans le bloc de la classe sont membres decette classe. Il existe trois catégories de membres de classe :

j les membres publics (mot-clé public), accessibles à "tout le monde" ;j les membres privés (mot-clé private), accessibles seulement aux membres et

amis de la classe concernée ;j les membres protégés (mot-clé protected), sur lesquels nous reviendrons

lorsque nous expliquerons la notion d’héritage.

Pour spécifier une catégorie de données, il suffit de l’indiquer à l’aide de la syntaxedu type catégorie:. Tous les membres, déclarés après cette spécification, font partie dela catégorie indiquée. Par défaut, la catégorie est private.

154 Chapitre 5 - Manipulation d’une matrice et classes

Listing 5-10 : Catégories de données dans une classe

class nom_de_la_classe{

/* Membres privés */int variable_private;void fonction_private(void);

public:/* Membres publics */float variable_public;void fonction_public(void);

};

Tous les membres sont accessibles depuis les fonctions membres de la classe. Parexemple, il est possible de modifier la valeur de variable_private depuis fonction_publicou fonction_private. Par contre, seuls les membres publics sont accessibles àl’extérieur, comme en témoigne le code suivant :

Listing 5-11 : Visibilité des membres d’une classe

void main(){

// Déclaration d'une instance de la classenom_de_la_classe Objet;

Objet.variable_public = 10; // Accès autoriséObjet.fonction_public(); // Appel autoriséObjet.variable_private = 10; // Accès interdit : erreurObjet.fonction_private(); // Appel interdit : erreur

}

Différence entre struct et class

En C, les classes n’existent pas et les structures ne peuvent posséder quedes données publiques (il n’y a pas de fonctions membres par exemple).En C++, les structures et les classes sont équivalentes, à ceci près : lacatégorie des membres est par défaut public dans une structure (private pourune classe). Mais une structure peut tout à fait posséder des fonctionsmembres, ou hériter d’une structure mère (ou même d’une classe !).Malgré ce lien de parenté entre structure et classe et les possibilités que celaoffre, nous utiliserons les structures seulement pour conditionner desdonnées publiques, sans fonction membre et trace d’héritage. Il estpréférable de garder les "structures style C" et de travailler avec des classespour le reste.

Classe Joueur 155

5

EncapsulationPar tradition, il est préférable de déclarer les classes dans des fichiers d’en-têtespécifiques, du même nom que la classe pour éviter toute ambiguïté. Les définitionsdes classes sont alors à placer dans des fichiers sources .cpp portant le même nom.

Dans l’Explorateur de solutions du projet, de la même façon que nous avons ajoutéle fichier principal main.cpp, ajoutons un fichier Joueur.cpp dans le dossier Fichierssources, et un fichier Joueur.h dans le dossier Fichiers d’en-tête.

Ouvrons le fichier d’en-tête Joueur.h. Incluons glut.h qui nous sera utile par la suite,et saisissons la déclaration de la classe Joueur. Pour l’instant, un joueur "a besoin" desa position sur la grille de jeu. Plaçons ces données en tant que membres privés.

#include "GL/glut.h"

class Joueur{private:

// Position Colonne/Ligne sur la matriceint PosC, PosL;

};

Vie publique, vie privée

Ici la spécification private ne sert à rien puisque la catégorie des membres estpar défaut private. Cela dit, nous préférons la spécifier malgré tout : plus lecode est explicite, moins il est source d’erreurs.

Figure 5-7 :Explorateur de

solutions

156 Chapitre 5 - Manipulation d’une matrice et classes

Bien que loin d’être complète, la classe Joueur est déjà utilisable. Essayons de créerun objet de type Joueur. Dans le fichier principal main.cpp, déclarons en global unobjet de type Joueur ; il ne doit pas, bien entendu, porter le nom Joueur. Nommons-lemonJoueur.

Joueur monJoueur; // Objet global de type Joueur

Théoriquement, les erreurs de compilation devraient être au rendez-vous, dont laprincipale : "erreur de syntaxe : absence de ’;’ avant l’identificateur ’monJoueur’". Eneffet, le fichier main.cpp ne connaît pas encore la classe Joueur, cette dernière étantdéclarée dans le fichier Joueur.h. Incluons cet en-tête (ici le fichier Joueur.h faisantpartie intégrante du projet, nous devons utiliser la syntaxe #include "fichier" et non#include <fichier>).

#include "Joueur.h" // Inclusion du fichier d'en-tête Joueur

L’oubli étant corrigé, nous pouvons utiliser l’objet monJoueur. Par exemple, dans lafonction main, saisissez monJoueur. (monJoueur suivi d’un point) pour accéder auxmembres de l’objet. Si Visual décide de le faire fonctionner, vous devriez voirapparaître les membres PosC et PosL.

Les petits cadenas sur les icônes, symbolisant ces deux variables, rappelle queleur catégorie est privée : il est impossible d’y accéder d’ici. Tenter de leur

affecter une valeur quelconque (par exemple avec l’instruction monJoueur.PosC = 2)fait réagir le compilateur :

error C2248: 'Joueur::PosC' : impossible d'accéder à private✂ membre déclaré(e) dans la classe 'Joueur'

Icônes existantes

Voici un tableau présentant les différents icônes que vous pourrez trouverpour représenter les membres des classes :

Figure 5-8 :Liste des

membres desinstances de

Joueur

Classe Joueur 157

5

Tableau 5-1 : Icônes représentatives des membres d’une instance

Icône Type Catégorie

Variable, objet, pointeur public

Variable, objet, pointeur private

Variable, objet, pointeur protected

Fonction public

Fonction private

Fonction protected

C’est bien là le but de la protection de ce type de membres. Il s’agit d’interdire leurmodification et leur accès manuel. Vous ne pourrez jamais par mégarde (vous ouvotre collègue programmeur travaillant sur le même projet) modifier les données devotre classe, et mettre en péril son bon fonctionnement, et celui du programme.D’un point de vue organisation, le principe de la POO "pure" est de définir toutes lesdonnées en tant que membres privés. Elles doivent être manipulables via desméthodes déclarées publiques, qui elles, sont accessibles à l’utilisateur. C’est leprincipe de l’encapsulation.

AccesseursFidèle au principe de l’encapsulation, nous avons déclaré les données PosC et PosLen tant que membres privés. Dans notre programme, nous avons besoin deconnaître la position du joueur à différents endroits (par exemple, pour savoir s’il atrouvé la sortie du niveau). Pour pouvoir accéder malgré tout aux valeurs de cesmembres privés, nous allons créer des accesseurs. Un accesseur est une fonctionpublique qui retourne la valeur d’un membre, généralement de visibilité privée. Leprogrammeur ne peut pas modifier la valeur du membre via cet accesseur. Il accèdeen quelque sorte à ce membre en "lecture seule".

Noms des accesseurs

Habituellement, les fonctions d’accès aux valeurs des données d’une classesont nommées Get suivies du nom de la donnée. Par exemple, l’accesseur àla variable Prix pourrait être appelé GetPrix.

158 Chapitre 5 - Manipulation d’une matrice et classes

Définissons les accesseurs aux membres PosC et PosL. Ces fonctions étant membresde la classe Joueur, nous pouvons accéder à toutes les données (privées ou non)contenues dans la classe Joueur depuis le corps de ces fonctions.

Listing 5-12 : Déclaration d’accesseurs

class Joueur{private:

// Position Colonne/Ligne sur la matriceint PosC, PosL;

public:// Accesseursint GetPosC(){

return PosC;}

int GetPosL(){

return PosL;}

};

Définitions à la volée

Il est possible de définir directement le code d’une fonction dans une classe.C’est généralement ce qui est fait lorsque les fonctions comptent peu delignes de programmation. Il est en outre possible de déclarer la fonctionmembre dans la déclaration de la classe et de placer sa définition àl’extérieur, habituellement dans un fichier nom_de_la_classe.cpp. Nousopterons pour cette solution pour les fonctions "conséquentes".

Nos deux accesseurs ne modifient pas les valeurs de nos membres. Ils se contententde renvoyer ces valeurs (ce qui est généralement le cas de tout accesseur). Il estjudicieux ici de les déclarer en tant que fonctions constantes à l’aide du mot-cléconst. Une fonction membre constante d’une classe ne peut pas modifier demembres de sa classe.

Listing 5-13 : Accesseurs : fonctions constantes

class Joueur{private:

// Position Colonne/Ligne sur la matriceint PosC, PosL;

Classe Joueur 159

5

public:// Accesseursint GetPosC() const {return PosC;}int GetPosL() const {return PosL;}

};

Maintenant que les accesseurs sont déclarés et définis, nous pouvons les utiliser.

ConstructeursLes valeurs de PosC et PosL n’ont jamais été initialisées, ce qui pose problème si l’oncherche à accéder à ces valeurs inexistantes.

Pour initialiser les membres privés d’une classe, il est possible de créer une fonctionpublique qui s’en charge.

Listing 5-14 : Fonction publique d’initialisation

class Joueur{private:

// Position Colonne/Ligne sur la matriceint PosC, PosL;

public:// Accesseursint GetPosC() const {return PosC;}int GetPosL() const {return PosL;}

// Initialisationvoid Initialisation() {

PosC = PosL = 0;}

};

Appeler la fonction Initialisation pour chaque instance de la classe Joueur permetd’initialiser ses données. Mais il y a plus pratique : une "fonction" appeléeautomatiquement à la construction de l’instance d’une classe. C’est le constructeur.

Le constructeur se déclare un peu de la même façon qu’une fonction membre, saufqu’il doit porter le même nom que la classe et ne posséder aucun type de retour, pasmême le type void (c’est d’ailleurs ce qui le différencie d’une fonction membreclassique). Il doit être déclaré public.

Figure 5-9 :Accès à la valeur

d’une variable viason accesseur

160 Chapitre 5 - Manipulation d’une matrice et classes

Listing 5-15 : Constructeur de la classe Joueur

class Joueur{private:

// Position Colonne/Ligne sur la matriceint PosC, PosL;

public:// Accesseursint GetPosC() const {return PosC;}int GetPosL() const {return PosL;}

// Constructeur par défautJoueur() {

PosC = PosL = 0;}

};

Nous pouvons utiliser les accesseurs créés pour consulter les valeurs de PosC et PosLde l’instance monJoueur.

cout << "Coordonnees du joueur (" << monJoueur.GetPosC()<< ", " << monJoueur.GetPosL() << ")" << endl;

Le constructeur de la classe Joueur est un constructeur par défaut, c’est-à-dire qui neprend pas de paramètre. En effet, il est possible de spécifier des paramètres à unconstructeur, et même de faire cohabiter constructeurs par défaut et paramétrés. Leconcept mis en avant ici est très important : il s’agit de la surcharge de fonction.

Surcharge de fonction et constructeurs paramétrésQue ce soit parmi les membres d’une classe ou en global, le C++ offre la possibilitéde déclarer plusieurs fonctions portant le même nom, à condition que le type ou lenombre de leurs paramètres diffèrent. On parle alors de surcharge de fonction.

Listing 5-16 : Exemples de surcharges de fonctions

// Déclaration de la fonction "de base"void fonction(int a, int b);// Surcharge correcte : le paramètre 2 n'a pas le même typevoid fonction(int a, float b); // Surcharge// Surcharge correcte : le nombre de paramètres est différentvoid fonction(int a);

Lors de l’appel de l’une de ces surcharges, c’est uniquement le type des valeurspassées en paramètre qui permet de déterminer le corps de fonction qui seraexécuté.

Classe Joueur 161

5

Listing 5-17 : Appel de surcharge de fonction

fonction(10, 6); // Appel de la première fonctionfonction(10, 4.0f); // Appel de la deuxième fonctionfonction(10); // Appel de la troisième fonction

C’est le type des paramètres qui importe, et non leur nom. Par exemple, la surchargesuivante est interdite :

void fonction(int c, int d);

Si elle était autorisée, comment déterminer si l’appel à l’instruction fonction(10, 6)doit exécuter la première fonction ou la nouvelle surcharge ?

Ce concept de surcharge de fonction se retrouve chez les constructeurs de classe. Enplus du constructeur par défaut (sans paramètre) qu’il est conseillé de créer, il estpossible de lui définir autant de surcharges que désiré. Cela est particulièrementutile pour définir directement la valeur de certains membres au moment d’instancierun objet. Imaginez la classe Pixel suivante ; le constructeur paramétré va permettrede définir directement la position du pixel à la construction.

Listing 5-18 : Surcharge de constructeur dans une classe

class Pixel {int x, y; // Coordonnées

public:// ConstructeursPixel() {x=y=0;} // par défautPixel(int a, int b) {x=a; y=b;} // paramétré

};

Lors de l’instanciation (création d’instance) d’un objet de la classe Pixel, ne passpécifier de paramètre implique l’utilisation du constructeur par défaut. Pour utiliserl’un des constructeurs paramétrés, il faut spécifier la valeur des paramètres après lenom de l’instance déclarée.

Listing 5-19 : Utilisation de constructeurs différents lors de l’instanciation d’objets

Pixel pix1; // Constructeur par défaut : pixel (0, 0)Pixel pix2(4, 8); // Constructeur paramétré : pixel (4, 8)

Constructeur par défaut

Il est préférable de déclarer un constructeur par défaut pour chaque classecréée, même si elle ne requiert par d’initialisation de variables ou d’objets.Cela permet d’éviter des erreurs par la suite, lors de l’instanciation d’objetsde cette classe.

162 Chapitre 5 - Manipulation d’une matrice et classes

DestructeurAlors qu’un constructeur est appelé à la création d’une instance d’une classe, undestructeur est appelé à sa destruction. Il doit contenir toutes les instructionsconsistant à libérer la mémoire occupée par les allocations dynamiques dans laclasse. Un destructeur se déclare de la même façon qu’un constructeur : il doitporter le même nom que la classe, ne posséder aucun type de retour et doit êtredéclaré public. La seule différence vient du caractère ~ (tilde) qui doit précéder sonnom.

class TheClasse{public:

TheClasse(); // Constructeur~TheClasse(); // Destructeur

};

Dans notre cas, la classe Joueur ayant seulement deux données membres sous formede variables, l’utilisation d’un destructeur s’avère inutile. Son utilisation est parcontre indispensable lors d’allocation de mémoire comme pour des tableauxdynamiques.

Surcharge de destructeur ?

Contrairement à un constructeur, le destructeur d’une classe ne peut êtresurchargé. Il ne prend pas de paramètre.

Afficher le joueurNotre joueur possède donc une position dans le niveau, que nous pouvons consulterpar le biais d’accesseurs. Cette position n’existant pour l’instant qu’à l’étatnumérique, nous devons la matérialiser en affichant le joueur à l’écran : c’est ce quel’on appelle un avatar. Déclarons une fonction publique chargée de l’afficher.

Listing 5-20 : Déclaration en tant que membre public de la classe Joueur

void Dessiner();

Définition de fonction d’une classeComme cette fonction sera assez lourde, nous n’allons pas la programmer "à lavolée" dans la déclaration de la classe. Puisqu’il existe un moyen de définir lesfonctions membres dans le fichier Joueur.cpp, profitons-en : ouvrons le fichier

Classe Joueur 163

5

Joueur.cpp et incluons Joueur.h pour que les définitions puissent "voir" leurdéclaration.

#include "Joueur.h"

La définition externe d’une fonction membre d’une classe s’écrit sous cette forme :

type_retour nom_classe::nom_fonction(paramètres){

/* Corps de la fonction */}

La différence avec la définition d’une classe non membre vient de la formenom_classe:: précédant le nom de la fonction. Définissons la fonction Dessiner.

Listing 5-21 : Fonction d’affichage du joueur

void Joueur::Dessiner() // Définition dans le fichier Joueur.cpp{

glPushMatrix();glTranslated(PosC+0.5, PosL+0.5, 0.0);

glColor3d(0.0, 0.0, 0.0); // Couleur noireglutSolidSphere(0.3, 12, 12); // Sphère de la tête

glColor3d (1.0, 1.0, 0.0); // Couleur jauneglTranslated(0.1, -0.1, 0.0);

glutSolidSphere(0.05, 12, 12); // Premier œilglTranslated(-0.2, 0.0, 0.0);

glutSolidSphere(0.05, 12, 12); // Deuxième œilglPopMatrix();

}

Pour plus de précisons sur OpenGL, reportez-vous au chapitre Initiation à la 3Dtemps réel : OpenGL.

Assurez-vous que le fichier d’en-tête glut.h est bien inclus depuis Joueur.h, sans quoiles appels aux instructions OpenGL et GLUT réalisées dans le corps de la fonctionDessiner seraient impossibles.

Appelons comme il se doit l’affichage du joueur dans la fonction d’affichagegénérale LabyAffichage.

Listing 5-22 : Appel de la fonction Dessiner de monJoueur depuis LabyAffichage

void LabyAffichage(){

// Définit la couleur de fondglClearColor(1.0, 1.0, 1.0, 1.0);// Efface l'écranglClear(GL_COLOR_BUFFER_BIT);

164 Chapitre 5 - Manipulation d’une matrice et classes

// Définit la matrice de modélisation activeglMatrixMode(GL_MODELVIEW);

DessinerNiveau(); // Affiche le niveaumonJoueur.Dessiner(); // Affiche l'avatar du joueur

glFlush(); // Achève l'affichage}

Le constructeur de la classe Joueur spécifie la position initiale du joueur. Pour lefaire apparaître en bas du mini-labyrinthe, nous pourrions spécifier les coordonnées(colonne d’indice 5, ligne d’indice 7). Cependant, tous les labyrinthes n’ayant pasforcément cette configuration, il est plus intéressant de définir la position de départdu joueur lors de la création du niveau.

Définir la position de départ

Prise en compte d’un caractère-cléJusqu’à maintenant, le caractère 0 correspond à un mur, et le caractère 1 à uneallée. Une bonne solution est de définir un autre caractère-clé pour placer le pointde départ du joueur. Choisissons par exemple le caractère j (comme "joueur").

Reprenons la fonction de chargement du niveau nommée OuvrirNiveau. Examinons ladouble boucle de lecture de la matrice contenue dans le fichier.

// Lecture du tableau du niveau, caractère par caractèrefor(int j=0; j<NbLignes; j++)

for(int i=0; i<NbColonnes; i++)fichier >> Matrice[i][j];

C’est ici que nous allons tester si le caractère lu est un j et agir en conséquence. Justeaprès avoir rempli la cellule Matrice[i][j], testons le caractère directement à l’aide d’un

Figure 5-10 :Ajout d’un j dans

la matrice duniveau

Classe Joueur 165

5

commutateur switch (le j ne sera pas le seul caractère-clé à être testé). Un bonréflexe est de tester non seulement la minuscule j, mais aussi la majuscule J, pourpallier les étourderies. Pour imposer à un commutateur de réaliser une même actiondans deux cas différents, il suffit de positionner les deux case à la suite.

Listing 5-23 : Deux conditions pour une action

// Lecture du tableau du niveau, caractère par caractèrefor(int j=0; j<NbLignes; j++)

for(int i=0; i<NbColonnes; i++){

fichier >> Matrice[i][j]; // Lecture du caractère

switch(Matrice[i][j]) // Test du caractère lu{// Position initiale de joueurcase 'j': // Teste à la fois la minusculecase 'J': { // et la majuscule

/* Définir ici la position de départ */break;}

}}

MutateursDéfinir la position de départ du joueur revient à initialiser les valeurs de PosC et PosLde l’instance monJoueur. Mais ces deux membres sont privés. Selon le principe de laPOO, créons des fonctions dont le but est de modifier la valeur de ces deuxmembres : ce sont des mutateurs (encore appelés "modificateurs").

Listing 5-24 : Mutateurs des données privées PosC et PosL

class Joueur{

/* … */// Mutateursvoid SetPosC(int C) {PosC = C;}void SetPosL(int L) {PosL = L;}/* … */

}

Nom des mutateurs

Il est courant de nommer les fonctions modificatrices par Set suivi du nomde la donnée à modifier. Par exemple, le mutateur de la variable Prix pourraitse nommer SetPrix.

166 Chapitre 5 - Manipulation d’une matrice et classes