Partie 35: Téléporteurs

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.

Paramètres des téléporteurs

Les téléporteurs sont un nouveau type de case.
Voici les paramètres qu'ils utilisent:

		<tile name="Teleporteur">
			<image>Teleporter.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;Aucun">Cote</param>
			<param type="bool">pourChampions</param>
			<param type="bool">pourObjets</param>
		</tile>
				
Comme les escaliers, les téléporteurs peuvent vous amener à un autre niveau à une position (X,Y).
Le paramètre coté a 5 valeurs parce qu'un téléporteur peut vous tourner dans une direction donnée, mais certains
d'entre eux ne changent pas votre direction initiale.
Enfin 2 booléens disent si le téléporteur agit sur vos champions ou sur les objets.

Bien qu'ils ressemblent à des escaliers, vous allez voir que les téléporteurs sont un peu plus compliqués.

Graphismes

Dans les graphismes d'origine, il y a seulement une image 32*32 avec des points aléatoires pour dessiner les téléporteurs.
Ici je laisse le fond rose pour que ce soit plus clair sur cette page web.


Mais voila à quoi ils doivent ressembler dans le jeu:



Les cotés du téléporteur ont la même forme que les murs qu'ils remplacent.
On va devoir faire ça en plusieurs étapes.
D'abord on crée une image temporaire qui a la même taille que le mur qui doit être remplacé par le téléporteur, et
on la remplit avec notre motif de téléporteur en le répétant autant de fois qu'il le faut.


Ensuite, on doit seulement dessiner notre image de téléporteur là où il y a des pixels opaques dans l'image du mur.
Le jeu d'origine utilisait des masques noir et blanc pour faire ça, mais les fonctions graphiques de Qt nous
permettent d'utiliser l'image du mur elle même comme un masque.


Et cette image sera assombrie en fonction de la distance aussi.
Alors voilà à quoi va ressembler une partie du code:

		QImage  wallGfx = fileCache.getImage(fileName);
		CVec2   wallPos(tileInfo->x, tileInfo->y);
		QImage  teleportGfx = fileCache.getImage("gfx/3DView/Teleporter.png");

		// crée l'image du téléporteur
		QImage  tempImg(wallGfx.size(), QImage::Format_ARGB32);
		tempImg.fill(TRANSPARENT);

		int subImgX = (wallGfx.size().width() + teleportGfx.size().width() - 1) / teleportGfx.size().width();
		int subImgY = (wallGfx.size().height() + teleportGfx.size().height() - 1) / teleportGfx.size().height();
		[...]

		for (int y = 0; y < subImgY; ++y)
			for (int x = 0; x < subImgX; ++x)
			{
				CVec2   subImgPos(x * teleportGfx.size().width(),
				                  y * teleportGfx.size().height());
				[...]
				graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
			}

		// assombrit
		int shadow = WALL_TABLE_HEIGHT - 2 - tablePos.y;
		if (shadow < 0)
			shadow = 0;
		graph2D.darken(&tempImg, (float)shadow * 0.2f);

		// dessine l'image
		graph2D.drawImageWithMask(image, wallPos, tempImg, wallGfx);
				

Animation

Dans le jeu les téléporteurs sont animés. Les points semblent bouger aléatoirement toutes les 20 frames.
Pour dessiner ça, quand on crée l'image du téléporteur à partir des carreaux de 32*32, on va les faire pivoter et
les retourner aléatoirement.
Mais comme on veut dessiner la même image pendant 20 frames, on va avoir besoin d'un générateur de nombres
aléatoires avec une graine qu'on peut contrôler. De telle façon qu'il produise la même série de nombres quand on
met la même graine de départ.
J'ai utilisé un simple générateur à congruence.

Pour m'assurer que la graine change seulement toute les 20 frames, j'ai utilisé un compteur qui est incrémenté à
chaque frame, et je l'ai divisé par 20.
J'aurais aussi pu utiliser un timer, mais on ne se soucie pas vraiment que l'animation s'arrête quand le jeu est en
pause ou qu'elle accélère quand on dort car on ne la voit pas de toute façon. Et ça n'affecte pas le gameplay.
Alors voila notre création d'image maintenant:

		int random = game.frameCounter / 20;
		[...]

			// crée l'image du téléporteur
			[...]
			random = (random * 31415821 + 1) % 100000000;

			for (int y = 0; y < subImgY; ++y)
				for (int x = 0; x < subImgX; ++x)
				{
					[...]
					bool flip = ((random & 1) != 0);
					int angle = ((random >> 1) % 4) * 90;
					graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
				}
				

La case entière

Comme pour les escaliers, on va dessiner la case qui contient les téléporteur dans "tiles.cpp".
Mais comme on, dessine une case entière, on verra les face extérieures des "murs" du téléporteur (à moins qu'on
soit à l'intérieur).
Alors on va avoir besoin d'un tableau qui est l'opposé de celui qu'on a utilisé pour les murs (dans lequel on avait
les faces intérieures des murs):

		struct STelepTile
		{
			char        file[16];
			uint16_t    x, y;
		};

		static const STelepTile   gTelepTiles[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH][3] =
		{
			// Rang 3 (le plus loin)
			{
				{{"Wall02_F.png",   0, 58}, {"", 0, 0},                {"Wall13_L.png",  8, 58}}, // 02
				{{"Wall12_F.png",   7, 58}, {"", 0, 0},                {"Wall23_L.png", 78, 59}}, // 12
				{{"Wall22_F.png",  77, 58}, {"", 0, 0},                {"", 0, 0}              }, // 22
				{{"Wall32_F.png", 146, 58}, {"Wall23_R.png", 134, 59}, {"", 0, 0}              }, // 32
				{{"Wall42_F.png", 216, 58}, {"Wall33_R.png", 180, 58}, {"", 0, 0}              }  // 42
			},

			// Rang 2
			{
				{{"", 0, 0},                {"", 0, 0},                {"Wall12_L.png",  0, 57}}, // 01
				{{"Wall11_F.png",   0, 52}, {"", 0, 0},                {"Wall22_L.png", 60, 52}}, // 11
				{{"Wall21_F.png",  59, 52}, {"", 0, 0},                {"", 0, 0}              }, // 21
				{{"Wall31_F.png", 164, 52}, {"Wall22_R.png", 146, 52}, {"", 0, 0}              }, // 31
				{{"", 0, 0},                {"Wall32_R.png", 216, 57}, {"", 0, 0}              }  // 41
			},

			// Rang 1
			{
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}              }, // 00
				{{"Wall10_F.png",   0, 42}, {"", 0, 0},                {"Wall21_L.png", 33, 42}}, // 10
				{{"Wall20_F.png",  32, 42}, {"", 0, 0},                {"", 0, 0}              }, // 20
				{{"Wall30_F.png", 191, 42}, {"Wall21_R.png", 164, 42}, {"", 0, 0}              }, // 30
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}              }  // 40
			},

			// Rang 0 (le plus proche)
			{
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}             }, // 00
				{{"", 0, 0},                {"", 0, 0},                {"Wall20_L.png", 0, 33}}, // 10
				{{"Wall20_F.png",  32, 42}, {"Wall20_R.png", 191, 33}, {"Wall20_L.png", 0, 33}}, // 20
				{{"", 0, 0},                {"Wall20_R.png", 191, 33}, {"", 0, 0}             }, // 30
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}             }  // 40
			}
		};
				
Remarquez que cette table contient le cas où on est à l'intérieur du téléporteur (ligne 20).
Dans ce cas on dessine les 3 murs qui nous entourent.
Et finalement, la fonction complète pour dessiner notre téléporteur est:

		void CTiles::drawTeleporter(QImage* image, CVec2 tablePos)
		{
			for (int i = 0; i < 3; ++i)
			{
				int random = game.frameCounter / 20;
				const STelepTile* tileInfo = &gTelepTiles[tablePos.y][tablePos.x][i];

				if (tileInfo->file[0] != 0)
				{
					static char fileName[256];
					sprintf(fileName, "gfx/3DView/walls/%s", tileInfo->file);
					QImage  wallGfx = fileCache.getImage(fileName);
					CVec2   wallPos(tileInfo->x, tileInfo->y);
					QImage  teleportGfx = fileCache.getImage("gfx/3DView/Teleporter.png");

					// crée l'image du téléporteur
					QImage  tempImg(wallGfx.size(), QImage::Format_ARGB32);
					tempImg.fill(TRANSPARENT);

					int subImgX = (wallGfx.size().width() + teleportGfx.size().width() - 1) / teleportGfx.size().width();
					int subImgY = (wallGfx.size().height() + teleportGfx.size().height() - 1) / teleportGfx.size().height();
					random = (random * 31415821 + 1) % 100000000;

					for (int y = 0; y < subImgY; ++y)
						for (int x = 0; x < subImgX; ++x)
						{
							CVec2   subImgPos(x * teleportGfx.size().width(),
							                  y * teleportGfx.size().height());
							bool flip = ((random & 1) != 0);
							int angle = ((random >> 1) % 4) * 90;
							graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
						}

					// assombrit
					int shadow = WALL_TABLE_HEIGHT - 2 - tablePos.y;
					if (shadow < 0)
						shadow = 0;
					graph2D.darken(&tempImg, (float)shadow * 0.2f);

					// dessine l'image
					graph2D.drawImageWithMask(image, wallPos, tempImg, wallGfx);
				}
			}
		}
				

Rentrer dedans

Quand on marche sur une case de téléporteur et que c'est un téléporteur qui agit sur les champions, c'est
pratiquement comme un escalier.
On positionne un flag appelé shouldTeleport qui sera géré dans CGame::mainLoop().
Quand ce flag est à true, on appelle teleport():

		void CPlayer::teleport()
		{
			CTile*  tile = map.getTile(pos);
			int     level = tile->getIntParam("Niveau");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));
			int     side = tile->getEnumParam("Cote");

			if (side == eWallSideMax)
				side = dir;

			if (level != game.currentLevel)
			{
				game.loadLevel(level, newPos, side);
			}
			else
			{
				player.pos = newPos;
				player.dir = side;
			}
			sound->play(player.pos, "sound/Buzz.wav");
			shouldTeleport = false;
		}
				
Ici, c'est pratiquement la même chose que pour les escaliers, à part que l'on ne va pas toujours dans un autre
niveau, et qu'on ne se tourne pas toujours dans une autre direction.

Poser des objets dedans

Quand on pose un objet sur le sol, on va tester si on est sur la case d'un téléporteur qui agit sur les objets.

            else if (clickedArea->type == eMouseAreaG_DropObject)
            {
                CVec2   pos = CVec2((int)clickedArea->param1, (int)clickedArea->param2);
                int side = (int)clickedArea->param3;

			    [... pose l'objet]

                if (side == eWallSideMax)
                {
                    CVec2   mapPos = pos / 2;
                    CTile*  tile = map.getTile(mapPos);

                    if (tile->getType() == eTileTeleporter && tile->getBoolParam("forObjects") == true)
                        teleportStacks(mapPos);
                }
            }
				
La fonction teleportStacks() va téléporter tout les tas d'objets qui sont dans la case donnée.
Alors on va d'abord faire une copie de sauvegarde de ces tas, et les effacer de la position source.

		void CGame::teleportStacks(CVec2 mapPos)
		{
			CTile*  tile = map.getTile(mapPos);
			std::vector<CObjectStack>   copyStacks;

			// copie les tas
			for (int y = 0; y < 2; ++y)
				for (int x = 0; x < 2; ++x)
				{
					CVec2 objPos = mapPos * 2 + CVec2(x, y);
					CObjectStack*    stack = map.findObjectsStack(objPos, eWallSideMax);

					if (stack != NULL)
					{
						CObjectStack    copy = *stack;
						copy.mPos = CVec2(x, y);
						copyStacks.push_back(copy);
						map.removeObjectsStack(objPos, eWallSideMax);
					}
				}
				
Ensuite, si le téléporteur mène à un autre niveau, on le charge (après avoir sauvegardé quelques données).

			int     lastLevel = currentLevel;
			CVec2   lastPos = player.pos;
			uint8_t lastDir = player.dir;

			int     level = tile->getIntParam("Niveau");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));

			// charge le niveau de destination
			if (level != currentLevel)
				loadLevel(level, CVec2(), 0);
				
Après ça, on repose tous les objets des tas copiés à la position de destination.

			// mets les tas à la nouvelle position
			for (size_t i = 0; i < copyStacks.size(); ++i)
			{
				CVec2 objPos = newPos * 2 + copyStacks[i].mPos;
				CObjectStack*   stack = map.addObjectsStack(objPos, eWallSideMax);

				for (size_t j = 0; j < copyStacks[i].getSize(); ++j)
					stack->addObject(copyStacks[i].getObject(j));
			}
				
Et finalement, si on a changé de niveau, on revient à celui de départ.

			// recharge le dernier niveau
			if (level != lastLevel)
				loadLevel(lastLevel, lastPos, lastDir);
		}
				

Bugs corrigés dans l'éditeur

J'ai corrigé un bug ennuyeux dans l'éditeur.
Les outils drawTile et drawWall sauvegardaient seulement le type de l'élément qu'ils écrasaient.
Donc, si par exemple vous écrasiez par erreur une case de porte, avec tous ses paramètres remplis, si vous pressiez
Ctrl-Z, la case de porte réapparaissait, mais tous ses paramètres étaient réinitialisés.
Maintenant, je sauvegarde la case ou le mur complet, avec tous ses paramètres.

J'ai aussi un peu changé les graphismes des boutons et des objets pour que les maps soient plus lisibles.