Partie 18: Portes - Partie 2: Collisions, ouverture et décors

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 17 (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.

Collisions

La dernière fois, on a dessiné les portes mais on pouvait les traverser même si elles étaient fermées.
Alors maintenant voyons comment on peut bloquer le joueur.
Comme on a choisi une représentation basée sur les murs pour notre map, jusqu'à maintenant on avait juste à tester
les murs dans CPlayer::moveIfPossible() pour restreindre le mouvement du joueur.
En suivant cette logique, on pourrait ajouter un type de mur invisible pour empêcher le joueur d'aller sur la case
d'une porte quand elle est fermée.
Mais à ce moment là, à chaque fois qu'on ajouterait une porte dans l'éditeur, il faudrait qu'on mette 1 case et 4
murs. Ca fait beaucoup de travail.

Heureusement, il y a une solution qui ne necessitera pas qu'on ajoute autre chose à l'éditeur et qui ne sera pas
difficile à programmer dans le jeu.
Comme on l'a dit, on veut seulement empêcher le joueur d'aller sur la case de la porte, et si vous vous rappelez,
on connait déjà la case où le joueur veut aller dans moveIfPossible().
Alors c'est facile de modifier cette fonction pour tester si on va vers une porte fermée:

		void CPlayer::moveIfPossible(EWallSide side)
		{
			CVec2   newPos = pos;

			[...]

			CTile*  tile = map.getTile(pos);
			CTile*  destTile = map.getTile(newPos);

			bool canMove = true;

			if (tile->mWalls[side].getType() != 0)
			{
				canMove = false;
			}
			else if (destTile == NULL)
			{
				canMove = false;
			}
			else if (destTile->getType() == eTileDoor && doors.isOpened(destTile) == false)
			{
				canMove = false;
			}

			if (canMove == true)
				pos = newPos;
		}
				

Le bouton

La dernière fois on a oublié un élément graphique de la porte: le bouton.
Dans ce jeu, les portes peuvent être ouvertes de beaucoup de façons. Mais la plus simple est un bouton qui apparait
sur l'encadrement de la porte.


Le bouton est une simple image que l'on va redimensionner et assombrir en fonction de la distance de la porte.
Dans "doors.cpp" vous pouvez voir que j'ai écrit un tableau avec la position et la taille du bouton, car il
n'apparaitra pas dans toutes les position des la porte:

		struct SButtonElem
		{
			int16_t x, y;
			float   scale;
		};

		SButtonElem   tabButton[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			// Rang 3 (le plus loin)
			{
				{0, 0, 0.0},  // 03
				{0, 0, 0.0},  // 13
				{137, 74, 0.43},  // 23
				{197, 72, 0.43},  // 33
				{0, 0, 0.0}   // 43
			},

			// Rang 2
			{
				{0, 0, 0.0},  // 02
				{0, 0, 0.0},  // 12
				{150, 75, 0.66}, // 22
				{0, 0, 0.0},  // 32
				{0, 0, 0.0}   // 42
			},

			// Rang 1
			{
				{0, 0, 0.0},  // 01
				{0, 0, 0.0},  // 11
				{167, 76,  1.0}, // 21
				{0, 0, 0.0},  // 31
				{0, 0, 0.0}   // 41
			},

			// Rang 0 (le plus proche)
			{
				{0, 0, 0.0},  // 00
				{0, 0, 0.0},  // 10
				{0, 0, 0.0},  // 20
				{0, 0, 0.0},  // 30
				{0, 0, 0.0}   // 40
			}
		};
				
Ensuite, dans CDoors::draw() on fait le dessin et on ajoute une zone souris quand la porte est devant nous.
Remarquez que dans la zone souris on ajoute un paramètre pour retrouver où est la porte.

		void CDoors::draw(QImage* image, CTile* tile, CVec2 tablePos)
		{
			if (isFacing(tile) == true)
			{
				//-------------------------------------------------------------------------------------
				// dessine l'encadrement
				drawFrameElement(image, tablePos, tabTopFrame, false);
				drawFrameElement(image, tablePos, tabLeftFrame, false);
				drawFrameElement(image, tablePos, tabRightFrame, true);

				//-------------------------------------------------------------------------------------
				// dessine le bouton
				if (tile->getBoolParam("hasButton") == true)
				{
					SButtonElem& cell = tabButton[tablePos.y][tablePos.x];
					CVec2 pos(cell.x, cell.y);
					float scale = cell.scale;

					if (scale != 0.0f)
					{
						QImage  button = fileCache.getImage("gfx/3DView/doors/Button.png");
						static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
						float shadow = shadowLevels[WALL_TABLE_HEIGHT - 2 - tablePos.y];

						graph2D.darken(&button, shadow);
						graph2D.drawImageScaled(image, pos, button, scale);

						if (tablePos == CVec2(2, 2))
						{
							CRect   rect(pos,
							             CVec2(pos.x + button.width() - 1, pos.y + button.height() - 1));
							mouse.addArea(eMouseAreaG_DoorButton, rect, (void*)tile);
						}
					}
				}

				//-------------------------------------------------------------------------------------
				// dessine la porte avec le décor
				drawDoor(image, tablePos, tile);
			}
		}
				
Finalement dans CGame::update() où on teste les zones souris, on teste si le bouton est cliqué.
Si c'est le cas, on récupère la case de la porte et on inverse son flag "estOuverte":

		void CGame::update(SMouseArea* clickedArea)
		{
			doors.update();

			if (clickedArea != NULL)
			{
				if (mouse.mButtonPressing == true)
				{
					if (clickedArea->type == eMouseAreaG_Champion)
					{
						[...]
					}
					else if (clickedArea->type == eMouseAreaG_DoorButton)
					{
						CTile*  tile = (CTile*)clickedArea->param1;
						bool isOpened = tile->getBoolParam("estOuverte");
						tile->setBoolParam("estOuverte", !isOpened);
					}
				}
			}
		}
				
A des fins de test, j'ai ajouté un bouton à chaque porte dans le premier niveau, comme on n'a pas encore vu les
autres façons de les ouvrir.

Ouvrir les portes

Les portes s'ouvrent de 2 manières différentes.
Soit toute la porte glisse à l'intérieur du plafond:


Ou les deux moitiés de la porte glissent à l'intérieur des murs de coté:


Les animations dans le jeu d'origine étaient un peu saccadées car elles n'affichaient que 4 ou 5 étapes pendant
toute l'animation.
C'était probablement parce qu'elle nécessitait le précalcul des images sur certains ordinateurs.
Nous, on va faire des animations beaucoup plus fluides. D'abord on va avoir besoin d'une fonction de mise à jour
qui sera appelée à chaque frame.
Cette fonction va boucler sur toutes les portes de la map, et pour chaque porte elle mettra à jour sa position et
le flag "estEnMouvement":

		// Met à jour les animations des portes
		void CDoors::update()
		{
			for (int y = 0; y < map.mSize.y; ++y)
				for (int x = 0; x < map.mSize.x; ++x)
				{
					CTile*  tile = map.getTile(CVec2(x, y));

					if (tile->getType() == eTileDoor)
					{
						bool isHorizontal = tile->getBoolParam("estHorizontale");
						int animLenght = (isHorizontal == false ? 88 : 48) * DOOR_POS_FACTOR;
						int pos = tile->getIntParam("pos");
						bool isOpened = tile->getBoolParam("estOuverte");

						if (isOpened == false)
						{
							if (pos < 0)
							{
								tile->setBoolParam("estEnMouvement", true);
								pos += animLenght / DOOR_ANIMATION_TIME;
								tile->setIntParam("pos", pos);
							}

							if (pos >= 0)
							{
								tile->setBoolParam("estEnMouvement", false);
								tile->setIntParam("pos", 0);
							}
						}
						else
						{
							if (pos > -animLenght)
							{
								tile->setBoolParam("estEnMouvement", true);
								pos -= animLenght / DOOR_ANIMATION_TIME;
								tile->setIntParam("pos", pos);
							}

							if (pos <= -animLenght)
							{
								tile->setBoolParam("estEnMouvement", false);
								tile->setIntParam("pos", -animLenght);
							}
						}
					}
				}
		}
				
Maintenant, les constantes ont besoin d'être un peu expliquées.
Les animations durent 1 seconde. Donc DOOR_ANIMATION_TIME est mis à 60, comme cette fonction est appelée 60 fois
par seconde.
Mais à quoi sert DOOR_POS_FACTOR ?

Si vous prenez une porte verticale, elle doit bouger de 88 pixels en 60 frames.
Donc sa vitesse est de 88/60 = 1.466... pixels par frame.
Mais comme on utilise des variables entières, cette vitesse sera arrondie à 1.
La porte bougera alors à 1 pixel par frame, et l'animation durera 88 frames, et non 60.

Pour une porte horizontale c'est encore pire, car chaque moitié de porte doit bouger de 48 pixels.
Sa vitesse serait de 48/60 = 0.8 pixels par frame, qui sera arrondi à 0.
Et la porte ne bougera pas du tout!

Alors pour avoir plus de précision dans les calculs, je multiplie la longueur de l'animation et la position de la
porte par DOOR_POS_FACTOR.
Et plus tard je divise la position par la même valeur au moment où je dessine la porte.
J'ai trouvé que 50 est une bonne valeur pour avoir des mouvements fluides, mais vous pouvez essayer de la changer si
vous voulez.

Le flag "estEnMouvement" est utilisé en conjonction avec "estOuverte" pour empêcher que le joueur traverse la porte
pendant qu'elle s'ouvre ou se ferme (cherchez CDoors::isOpened).

Les portes verticales

Pour dessiner les portes verticales pendant leurs animations, on ajoute simplement le paramètre "pos" à la position
du sprite dans CDoors::drawDoor():

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// récupère l'image de la porte
				int doorType = tile->getDoorParam("Type");
				std::string fileName = std::string("gfx/3DView/doors/") + doorsDatas[doorType].files[doorNum].toUtf8().constData();
				QImage  doorImage = fileCache.getImage(fileName);

				[...]

				//----------------------------------------------------------------------------
				// dessin
				bool isHorizontal = tile->getBoolParam("estHorizontale");
				int doorPos = tile->getIntParam("pos") / DOOR_POS_FACTOR;

				if (isHorizontal == false)
				{
					//----------------------------------------------------------------------------
					// porte verticale
					static const int doorScalesY[] = {88, 61, 38};
					float scale = doorScalesY[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					doorPos = (doorPos * scale) / 88;

					CVec2 pos(cell.x, cell.y + doorPos);
					QRect clip(cell.cx1, cell.cy1,
					           cell.cx2 - cell.cx1 + 1, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip);
				}
				else
				{
					//----------------------------------------------------------------------------
					// porte horizontale
					[...]
				}
			}
		}
				

Les portes horizontales

Les portes horizontales sont un peu plus complexes à dessiner.
D'abord il faut qu'on dessine les 2 moitiés indépendamment.
Les rectangles de clipping sont utilisés pour n'afficher qu'une moitié du sprite.
Et ils doivent être modifiés en fonction de la position de l'animation.

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// récupère l'image de la porte
				[...]

				//----------------------------------------------------------------------------
				// dessin
				bool isHorizontal = tile->getBoolParam("estHorizontale");
				int doorPos = tile->getIntParam("pos") / DOOR_POS_FACTOR;

				if (isHorizontal == false)
				{
					//----------------------------------------------------------------------------
					// porte verticale
					[...]
				}
				else
				{
					//----------------------------------------------------------------------------
					// porte horizontale
					static const int doorScalesX[] = {96, 64, 44};
					float scale = doorScalesX[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					doorPos = (doorPos * scale) / 96;

					CVec2 pos(cell.x + doorPos, cell.y);
					int halfwidth = (cell.cx2 - cell.cx1 + 1) / 2;

					QRect clip1(cell.cx1, cell.cy1,
					            halfwidth + doorPos, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip1);

					pos.x = cell.x - doorPos;
					QRect clip2(cell.cx1 + halfwidth - doorPos, cell.cy1,
					            halfwidth + doorPos, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip2);
				}
			}
		}
				

Les décors des portes

Tant qu'on est dans la fonction drawDoor(), dessinons une dernière chose: le décor de la porte.
La seule porte qui a un décor dans le premier niveau est celle que l'on a prise pour entrer dans le donjon.


Alors remplissons le fichier "door_ornates.xml" avec le nom de l'image et sa position relative par rapport à la
porte:

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<door_ornates>
			<door_ornate name="Aucun">
			</door_ornate>

			<door_ornate name="Entree Donjon">
				<image>Dungeon_Entrance.png</image>
				<pos x="0" y="0"/>
			</door_ornate>
		</door_ornates>
				
Le chargement de cette base de données est la même chose que ce que nous avons déjà fait de nombreuses fois...

Ensuite, pour le dessin c'est pratiquement la même chose que pour les décors des murs.
L'image est redimensionnée, assombrie et positionnée relativement à sa porte respective.
Comme le dessin de la porte est déjà complexe, j'ai préfére ajouter le décor directement à l'image de la porte
avant de dessiner tout l'ensemble à l'écran

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// récupère l'image de la porte
				[...]

				//----------------------------------------------------------------------------
				// récupère l'image du décor
				int ornateType = tile->getDoorOrnateParam("Decor");
				std::string ornateName = doorOrnatesDatas[ornateType].file.toUtf8().constData();

				QImage  ornateImage;
				CVec2   ornatePos;
				if (ornateName.empty() == false)
				{
					// réduit le décor, l'assombrit, et le dessine sur l'image de la porte
					ornateImage = fileCache.getImage(std::string("gfx/3DView/door_ornates/") + ornateName);
					static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
					float shadow = shadowLevels[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					graph2D.darken(&ornateImage, shadow);

					ornatePos = doorOrnatesDatas[ornateType].pos;
					float scale = doorImage.width() / 96.0;
					ornatePos *= scale;
					graph2D.drawImageScaled(&doorImage, ornatePos, ornateImage, scale);
				}

				//----------------------------------------------------------------------------
				// dessin
				[...]
			}
		}
				
On verra plus tard que ces décors réservent encore bien des surprises...