Partie 29: Escaliers et mémorisation des maps

Téléchargements

Code source
Exécutable de l'éditeur de niveaux (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.

Escaliers dans l'éditeur

Les escaliers sont un peu comme les portes. Ils se trouvent tous deux au milieu d'une case et leur orientation est
importante.
Alors comme pour les portes, on va utiliser à la fois une case et un mur pour les définir

La case contiendra les données: le niveau de destination, la position où on va apparaître et la direction dans
laquelle on sera tourné.

		<tile name="Escaliers">
			<image>Stairs.png</image>
			<param type="int">Niveau</param>
			<param type="int">X</param>
			<param type="int">Y</param>
			<param type="enum" values="Haut;Gauche;Bas;Droite">Cote</param>
		</tile>
				
Et le mur nous donnera l'orientation de l'image. J'ai choisi d'utiliser le mur du "fond" de l'escalier (celui qui
est surligné en rouge dans cette image):


Et c'est tout ce qu'on a à faire dans l'éditeur.
J'ai aussi créé une map du niveau 2 qui est juste une grande pièce pour qu'on puisse voir les graphismes de
l'escalier depuis toutes les positions.

Les escaliers dans le jeu

Vous trouverez les images des escaliers dans "data/gfx/3DView/stairs".
Il y a 6 images pour les escaliers descendants quand ils sont en face de nous.
Je les ai nommées d'après les index de la table des murs.


Vous pouvez voir que 3 des images sont pour les escaliers qui sont devant nous, et les autres sont pour ceux qui
sont 1 case à gauche.
Bien sur, les images sont retournées pour le coté droit.

Il y a le même nombre d'images pour les escaliers montants qui sont en face de nous.


Quand les escaliers osnt perpendiculaires par rapport à nous, on peut seulement en voir une petite partie:


Et enfin, quand on est sur la même case, on ne voit qu'une petite partie des rampes.


Le code pour afficher les escaliers est dans "tiles.cpp". Il n'est pas très compliqué. Comme pour les autres
éléments graphiques, on utilise des tables de coordonnées qui suivent l'ordre de la table des murs.
Notez que j'ai ajouté une variable CGame::currentLevel pour pour garder le numéro du niveau qui est chargé en ce
moment. On se sert de cette variable pour savoir si les escaliers montent ou descendent.

		struct SStairsData
		{
			char        file[32];
			uint16_t    x, y;
		};

		// table pour les escaliers descendant, en face
		static const SStairsData   gDownFacingStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			{{"", 0, 0}, {"Down_13_F.png", 8, 63}, {"Down_23_F.png", 75, 58}, {"Down_13_F.png", 141, 63}, {"", 0, 0}}, // Row 3
			{{"", 0, 0}, {"Down_12_F.png", 0, 57}, {"Down_22_F.png", 63, 57}, {"Down_12_F.png", 163, 57}, {"", 0, 0}}, // Row 2
			{{"", 0, 0}, {"Down_11_F.png", 0, 51}, {"Down_21_F.png", 35, 50}, {"Down_11_F.png", 192, 51}, {"", 0, 0}}, // Row 1
			{{"", 0, 0}, {"", 0, 0},               {"", 0, 0},                {"", 0, 0},                 {"", 0, 0}}  // Row 0
		};

		// table pour les escaliers montant, en face
		static const SStairsData   gUpFacingStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			[...]
		};

		// table pour les escaliers descendant, de coté
		static const SStairsData   gDownSideStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			{{"", 0, 0}, {"", 0, 0},                      {"", 0, 0}, {"", 0, 0},                       {"", 0, 0}}, // Row 3
			{{"", 0, 0}, {"Side_12_F.png",      60,  89}, {"", 0, 0}, {"Side_12_F.png",      156,  89}, {"", 0, 0}}, // Row 2
			{{"", 0, 0}, {"Down_Side_11_F.png", 32,  95}, {"", 0, 0}, {"Down_Side_11_F.png", 172,  95}, {"", 0, 0}}, // Row 1
			{{"", 0, 0}, {"Side_10_F.png",       0, 106}, {"", 0, 0}, {"Side_10_F.png",      208, 106}, {"", 0, 0}}  // Row 0
		};

		// table pour les escaliers montant, de coté
		static const SStairsData   gUpSideStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			[...]
		};

		void CTiles::drawStairs(QImage* image, CTile* tile, CVec2 tablePos)
		{
			const SStairsData*  data;

			// trouve le coté des escaliers
			EWallSide   side;
			int i;

			for (i = 0; i < 4; ++i)
				if (tile->mWalls[i].getType() == eWallStairs)
					break;
			side = (EWallSide)i;
			bool isDown = (tile->getIntParam("Niveau") > game.currentLevel);

			if (side != player.getWallSide(eWallSideDown))
			{
				// récupère les données de la table
				if (side == player.getWallSide(eWallSideUp))
				{
					// escaliers en face
					if (isDown)
						data = &gDownFacingStairs[tablePos.y][tablePos.x];
					else
						data = &gUpFacingStairs[tablePos.y][tablePos.x];
				}
				else
				{
					// escaliers de coté
					if (isDown)
						data = &gDownSideStairs[tablePos.y][tablePos.x];
					else
						data = &gUpSideStairs[tablePos.y][tablePos.x];
				}

				// affichage
				if (data->file[0] != 0)
				{
					std::string fileName = std::string("gfx/3DView/stairs/") + std::string(data->file);
					CVec2   pos = CVec2(data->x, data->y);
					bool    flip = (tablePos.x > 2);
					QImage  stairsImage = fileCache.getImage(fileName);
					graph2D.drawImage(image, pos, stairsImage, 0, flip);
				}
			}
			else if (tablePos == CVec2(2, 3))
			{
				// cas spécial si on est sur la case de l'escalier
				if (isDown)
				{
					QImage  stairsImage = fileCache.getImage("gfx/3DView/stairs/Down_20_L.png");
					graph2D.drawImage(image, CVec2(0, 109), stairsImage);
					graph2D.drawImage(image, CVec2(194, 109), stairsImage, 0, true);
				}
				else
				{
					QImage  stairsImage = fileCache.getImage("gfx/3DView/stairs/Up_20_L.png");
					graph2D.drawImage(image, CVec2(0, 91), stairsImage);
					graph2D.drawImage(image, CVec2(194, 91), stairsImage, 0, true);
				}
			}
		}
				

Prendre les escaliers

Il y a plusieurs façons de "prendre" les escaliers pour aller au niveau suivant:
Le dernier cas est pour éviter de montrer les cotés de l'escalier, qui ne sont pas dessinés quand vous êtes sur sa
case.

La fonction pour changer de niveau et de position d'après les données de la case courante est assez simple.

		void CPlayer::takeStairs()
		{
			CTile*  tile = map.getTile(pos);
			int     level = tile->getIntParam("Niveau");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));
			int     side = tile->getEnumParam("Cote");
			game.loadLevel(level, newPos, side);
		}
				
La fonction CGame::loadLevel() charge évidemment le niveau et positionne le joueur dans la nouvelle map.

Mais appeler cette fonction directement pendant les déplacements du joueur qu'on a énumérés serait dangereux.
Charger un nouveau niveau en plein milieu d'une frame alors que certains traitements de l'ancien niveau ne sont pas
terminés pourrait conduire à des bugs bizarres.
On va plutôt mettre un flag à true quand on fait ces mouvements:

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnLeft()
		{
			shouldTakeStairs = false;
			[...]
			CTile*  tile = map.getTile(pos);

			if (tile->getType() == eTileStairs)
				shouldTakeStairs = true;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnRight()
		{
			shouldTakeStairs = false;
			[...]
			CTile*  tile = map.getTile(pos);

			if (tile->getType() == eTileStairs)
				shouldTakeStairs = true;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveLeft()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveRight()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveForward()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveBackward()
		{
			shouldTakeStairs = false;

			if (interface.isInInventory() == false)
			{
				CTile*  tile = map.getTile(pos);

				if (tile->getType() == eTileStairs)
				{
					shouldTakeStairs = true;
				}
				else
				{
					[...]
				}
			}
		}

		//-----------------------------------------------------------------------------------------
		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]

			if (canMove == true)
			{
				[...]

				if (destTile->getType() == eTileStairs)
					shouldTakeStairs = true;
			}
		}
				
Et à la fin de la boucle de jeu, on teste ce flag pour appeler notre fonction:

		QImage& CGame::mainLoop()
		{
			[...]

			if (player.shouldTakeStairs)
				player.takeStairs();

			return image;
		}
				
Et on n'oublie pas de remettre ce flag à false dans takeStairs().

Mémorisation de la map

Maintenant, si on lance le jeu comme ça, quand on descendra au niveau 2, et qu'on remontera au niveau 1, on
rencontrera un problème.
Tous les objets qu'on a ramassés réapparaissent sur le sol, et la première porte du niveau s'est refermée, donc on
ne peut pas revenir dans la zone des miroirs et au début du niveau.

Il faut qu'on fasse une image de l'état de la map avant de la quitter, et qu'on restaure cet état quand on rentre
à nouveau dans le niveau.
Alors on va stocker l'état de toutes les maps qu'on a visitées dans une liste dans CGame:

		class CSnapshot
		{
		public:
			CSnapshot();
			CSnapshot(const CSnapshot& rhs);
			virtual ~CSnapshot();

			CSnapshot&  operator=(const CSnapshot& rhs);
			void    copyPressPlates(bool* src);

			int     level;
			CMap    map;
			bool*   pressPlateStates;

		private:
			void    freePressPlates();
		};

		class CGame
		{
			[...]
			std::vector<CSnapshot>   mVisitedLevels;
		};
				
Les données qu'on va stocker pour chaque map sont:
J'ai aussi rajouté quelques fonction utilitaires pour gérer les "pressPlateStates".

Maintenant, avant d'aller plus loin, je voulais vous parler d'un bug que j'ai déjà eu dans une précédente partie et
que j'avais oublié de mentionner.
Quand vous utilisez un conteneur STL comme std::vector ils ne fonctionnent pas toujours comme vous pourriez le
penser.
En particulier, quand vous leur ajoutez un objet avec la fonction push_back(), ça ne stocke pas simplement votre
objet dans le conteneur.
A la place, ça fait un genre de copie temporaire, puis ça la détruit, en appelant le destructeur de votre classe.
Mais le problème c'est que ça n'appelle pas le constructeur standard pour créer cette copie, donc votre destructeur
peut crasher parce que certaines variables ne sont pas initialisées convenablement.
En fait, ça appelle un constructeur par copie pour créer l'objet, par exemple:

		CMap::CMap(const CMap& rhs)
		{
			mTiles = NULL;
			*this = rhs;
		}
				
Donc j'ai du écrire des constructeurs par copie pour quelques classes, et en même temps j'en ai profité pour écrire
des opérateurs "=" pour ces classes, parce que c'était plus facile de définir les constructeurs par copie de cette
façon.

Une autre remarque: quand on copie une map, on ne copie pas toutes les données. Donc, j'ai choisi de passer en
"static" les tables qui contiennent les noms et les types de paramètres (mTilesParams, mWallsParams et
mObjectsParams) qui sont communes à toutes les maps.

Maintenant pour vraiment faire une image du niveau, j'ai écrit une fonction snapshotLevel():

		void CGame::snapshotLevel()
		{
			if (currentLevel != 0)
			{
				// Si le niveau est déjà mémorisé, on l'efface
				std::vector<CSnapshot>::iterator it;

				for (it = mVisitedLevels.begin(); it != mVisitedLevels.end(); ++it)
					if (it->level == currentLevel)
					{
						mVisitedLevels.erase(it);
						break;
					}

				// ajoute une nouvelle entrée
				CSnapshot   s;
				s.level = currentLevel;
				s.map = map;
				s.copyPressPlates(tiles.getPressPlates());
				mVisitedLevels.push_back(s);
			}
		}
				
Cette fonction est appelée à chaque fois qu'on charge un nouveau niveau dans la fonction loadLevel():

		void CGame::loadLevel(int level, CVec2 pos, int side)
		{
			// mémorise le niveau courant
			snapshotLevel();
			currentLevel = level;

			// cherche si le nouveau niveau est mémorisé...
			for (size_t i = 0; i < mVisitedLevels.size(); ++i)
			{
				CSnapshot*  s = &mVisitedLevels[i];

				if (s->level == level)
				{
					map.freeMemory();
					map = s->map;
					tiles.setPressPlates(s->pressPlateStates);
					player.pos = pos;
					player.dir = side;
					return;
				}
			}

			// ...sinon, charge le nouveau niveau
			static char fileName[256];
			sprintf(fileName, "maps/level%02d.map", level);
			map.load(fileName);
			player.pos = pos;
			player.dir = side;
			tiles.initMap();
			walls.init();
		}
				
Et voila. Maintenant, à chaque fois qu'on prendra un escalier pour revenir à un niveau qu'on a déjà visité, on le
retrouvera dans le même état que quand on l'avait quitté.