Partie 6: Dessiner les murs

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 5 (Windows 32bits)
Exécutable du jeu (Windows 32bits)

Avant d'essayer de compiler le code, allez dans l'onglet "Projets" dans le menu de gauche, séléctionnez l'onglet "Run" pour votre kit,
et mettez dans "Working directory" le chemin de "editor\data" pour l'éditeur ou "game\data" pour le jeu.

Adapter les images - des cases aux murs

Dans la partie 4, on a vu que les graphismes des "murs" dans le jeu d'origine représentaient en fait des cases et qu'il utilisait une représentation de la map basée sur les cases.
De notre coté, on a choisi une représentation basée sur les murs, alors pour simplifier notre travail, on va séparer les murs de face et ceux de coté.


J'expliquerai plus tard la convention de nommage utilisé pour ces fichiers.
Vous pouvez trouver tous ces murs à leur bonne place dans le fichier Gimp "wall_pos.xcf"

Et de l'extérieur...

Les graphs d'origine affichaient le coté "extérieur" des murs entourant une case.
Seuls 3 types de murs apparaissent dans les graphs: les murs de gauche, ceux de droite, et ceux de face (le mur d'une case qui est le plus proche de nous).
Donc, potentiellement, chaque case pouvait avoir 3 murs de dessinés:


La flèche indique la direction vers laquelle on regarde. Note: pour simplifier, dans la plupart de cet article on considérera que l'on regarde vers le nord (le haut de la map),
donc les cases que l'on verra seront "au-dessus" de nous dans la map.
Vous pouvez voir que le coté "arrière" de la case (le plus loin de nous) n'est jamais dessiné.
En fait, quand vous regardez les graphs, aucune case n'a 3 de ses murs de dessinés. Voici tous les murs qu'on peut voir avec le cône de vision approximatif:


Alors comment savoir quels murs on doit dessiner pour chaque case ?
On peut répondre à ça en utilisant la méthode du "backface culling": "On ne dessine que les murs qui sont orientés vers nous".
Ou on peut voir ça en termes de lancer de rayons, ce qui est un peu plus simple à comprendre je pense.
Imaginez que vous lancez un rayon vers un mur à partir de la position du joueur.
Si le rayon touche le coté "extérieur du mur avant son coté "intérieur", on dessine ce mur.

... vers l'intérieur

Mais dans notre représentation en mémoire de la map, Nous avons stocké les murs intérieurs de chaque case.
Alors, est-il possible d'effectuer le même dessin en utilisant les murs intérieurs au lieu des murs extérieurs ?
Essayons d'utiliser le même algorithme: on envoie des rayons depuis le joueur, mais on ne dessine un mur que si le rayon touche son coté intérieur avant le coté extérieur.
Voilà tous les murs qu'on va dessiner:


Si on numérote les cases dans cette image, vous comprendrez comment j'ai nommé les fichiers:


Les noms de fichiers ont cette forme: Wall<xx>_<y>.png où <xx> est le numéro de la case, comme ci-dessus, et <y> est une lettre qui nous donne le coté du mur à l'intérieur
de la case ("L" pour gauche, "R" pour droite, "F" pour "de face").

Comparer les 2 façons de dessiner

Maintenant, revenons aux schémas rouges et verts ci-dessus. On peut dire qu'il sont équivalents dans le sens où ils ont le même nombre de murs.
Chaque mur rouge correspond à un mur vert.
Et c'est une bonne chose parce que je n'ai pas le talent pour dessiner de nouveaux murs...

Mais sont-ils vraiment équivalents ? Quand on dessine les murs du diagramme vert est-ce qu'on n'obtient pas des murs qui se superposent bizarrement comme on l'a vu dans la partie 4?
En fait, ça marche si on suit l'ordre qu'on a défini avant, c'est à dire: Vous pouvez vous en convaincre en essayant de cacher/"rendre visible" les calques correspondants dans "wall_pos.xcf".

Mais il y a aussi une chose importante à prendre en compte. Dans le jeu, il n'y a pas que des murs. Il y a aussi des objets sur le sol et des monstres qui rampent entre ces murs.
Est-ce qu'ils vont "coller" avec notre façon de dessiner les murs ?
En fait, on va seulement devoir suivre une simple règle pour que ça marche: pour une case donnée il faudra qu'on dessine les murs en premier, puis les objets et les monstres
qui sont dans cette case. Comme les murs qu'on dessine sont les murs "intérieurs" de la case, ils apparaitront toujours derrière les monstres.
Vous pouvez aussi essayer ça dans Gimp.

Implémentation

Maintenant, implémentons toutes ces données dans le code.
Dans les sources du jeu de cette partie, dans "main.cpp", vous verrez qu'on commence par charger la map (comme on l'a vu dans la partie précédente):

		map.load((char *)"maps/test.map");
				
Vous allez aussi voir un paquet de nouveaux fichiers dans "sources": Et bien sur, dans "data" vous verrez aussi que les graphs des murs ont changé depuis la partie précedente, mais on en a déjà parlé au début.

La table

Dans "walls.cpp" vour trouverez une grosse table au début:

		static const CDispTile   gDispTiles[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH][3] =
		{
			// Rang 3 (le plus loin)
			{
				{{"", 0, 0}, {"", 0, 0},               {"", 0, 0}               }, // 03
				{{"", 0, 0}, {"Wall13_L.png", 8, 58},  {"", 0, 0}               }, // 13
				{{"", 0, 0}, {"Wall23_L.png", 78, 59}, {"Wall23_R.png", 134, 59}}, // 23
				{{"", 0, 0}, {"", 0, 0},               {"Wall33_R.png", 180, 58}}, // 33
				{{"", 0, 0}, {"", 0, 0},               {"", 0, 0}               }  // 43
			},

			// Rang 2
			{
				{{"Wall02_F.png",   0, 58}, {"", 0, 0},               {"", 0, 0}               }, // 02
				{{"Wall12_F.png",   7, 58}, {"Wall12_L.png",  0, 57}, {"", 0, 0}               }, // 12
				{{"Wall22_F.png",  77, 58}, {"Wall22_L.png", 60, 52}, {"Wall22_R.png", 146, 52}}, // 22
				{{"Wall32_F.png", 146, 58}, {"", 0, 0},               {"Wall32_R.png", 216, 57}}, // 32
				{{"Wall42_F.png", 216, 58}, {"", 0, 0},               {"", 0, 0}               }  // 42
			},

			// Rang 1
			{
				{{"", 0, 0},                {"", 0, 0},               {"", 0, 0}               }, // 01
				{{"Wall11_F.png",   0, 52}, {"", 0, 0},               {"", 0, 0}               }, // 11
				{{"Wall21_F.png",  59, 52}, {"Wall21_L.png", 33, 42}, {"Wall21_R.png", 164, 42}}, // 21
				{{"Wall31_F.png", 164, 52}, {"", 0, 0},               {"", 0, 0}               }, // 31
				{{"", 0, 0},                {"", 0, 0},               {"", 0, 0}               }  // 41
			},

			// Rang 0 (le plus proche)
			{
				{{"", 0, 0},                {"", 0, 0},              {"", 0, 0}               }, // 00
				{{"Wall10_F.png",   0, 42}, {"", 0, 0},              {"", 0, 0}               }, // 10
				{{"Wall20_F.png",  32, 42}, {"Wall20_L.png", 0, 33}, {"Wall20_R.png", 191, 33}}, // 20
				{{"Wall30_F.png", 191, 42}, {"", 0, 0},              {"", 0, 0}               }, // 30
				{{"", 0, 0},                {"", 0, 0},              {"", 0, 0}               }  // 40
			}
		};
				
Elle contient les données de nos murs "verts".
Les cases sont stockées dans les même ordre que celui du schéma, donc en commençant avec la case 03, puis 13, puis 23, etc...
Pour chaque case, on trouve les données des 3 murs qui peuvent être dessinés (celui de face, celui de gauche et celui de droite).
Ces données sont le nom du fichier graphique et les coordonnées auxquelles il doit apparaitre à l'écran.
Il y a beaucoup de cases "vides", parce que comme on le voit dans notre schéma, il y a beaucoup de murs qu'on ne voit pas dans notre cône de vision.

Maintenant, voyons comment utiliser ces données pour vraiment dessiner les murs.

La boucle d'affichage

Dans "game.cpp" vous pouvez voir que la fonction displayMainView() a bien changé.
Après que nous ayons dessiné le sol et le plafond, on trouve maintenant cette boucle:

		// dessine les murs
		CVec2   tablePos;
		CVec2   localPos;
		CVec2   mapPos;
		EWallSide   side;
		CTile*  tile;

		for (int y = 0; y < WALL_TABLE_HEIGHT; ++y)
			for (int x = 0; x < WALL_TABLE_WIDTH; ++x)
			{
				tablePos = walls.getDrawSequence(CVec2(x, y));
				localPos = walls.getMapPos(tablePos);
				mapPos = player.getPosFromLocal(localPos);
				tile = map.getTile(mapPos);

				if (tile != NULL)
				{
					side = player.getWallSide(eWallSideUp);
					if (tile->mWalls[side].mType != 0)
						drawWall(&image, tablePos, eWallSideUp);

					side = player.getWallSide(eWallSideLeft);
					if (tile->mWalls[side].mType != 0)
						drawWall(&image, tablePos, eWallSideLeft);

					side = player.getWallSide(eWallSideRight);
					if (tile->mWalls[side].mType != 0)
						drawWall(&image, tablePos, eWallSideRight);

					// objects.draw();
					// monsters.draw();
				}
			}
				
On peut voir 2 "for" qui bouclent sur toutes les cases de notre table.

Puis on apppelle getDrawSequence() qui nous renvoie la case qui suit l'ordre dont on a parlé plusieurs fois: le plus à gauche/droite d'abord, etc...
Donc "tablePos" contient les coordonnées dans la table de la case qu'on va dessiner.

getMapPos() convertit ces coordonnées en coordonnées dans la map relatives à la position du joueur (le point noir dans les schémas).
A ce moment on considère toujours qu'on regarde le nord. Par exemple, la case "00" dans le schéma aura pour coordonnées (-2, 0)

Ensuite, on appelle la fonction getPosFromLocal de "player.cpp" qui nous donne les vraies coordonnées dans la map, en prenant en compte la position du
joueur et la direction vers laquelle il regarde.

On récupère un pointeur sur la case et on teste si on n'est pas en dehors de la map.

Et on teste les 3 murs de la case (de face, gauche et droite) pour savoir si on doit les dessiner.
Avant chaque test, on appelle la fonction getWallSide de player.cpp qui nous renvoie le bon mur qu'on doit tester, en fonction de l'orientation du joueur.
Par ex. si on veut tester le mur "en haut" de la case quand on considère qu'on regarde le nord, alors, si le joueur est en train de regarder vers l'est, on testera
le mur "à droite" de cette case.
La fonction drawWall récupère le nom du fichier graphique dans la table et le dessine à sa position à l'écran si il n'est pas vide.

Les dernières lignes de la boucle ne sont que des commentaires qui indiquent où on appelera les fonctions pour dessiner les monstres et les objets dans le futur.

Si vous lancez ce code avec la map fournie vous vous retrouverez dans une case entourée de murs (vous êtes à la position (0, 0), tourné vers le nord).
Comme je l'ai dit, vous ne pouvez pas vous déplacer dans la map pour l'instant, on s'occupera de ça dans la prochaine partie.
Mais vous pouvez éventuellement changer la position du joueur à la main dans le code pour tester si tout s'affiche comme cela devrait.