Partie 13: Les décors des murs - Partie 2

Téléchargements

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

Charger les bases de données

La dernière fois on a modifié l'éditeur pour ajouter des décors aux murs, maintenant on va voir comment les afficher dans le jeu.
Le jeu a besoin d'avoir les informations qui sont dans "walls.xml" et "ornates.xml". Alors maintenant il les lit dans "walls.cpp".
Le code est assez similaire à l'éditeur, a part que nous lisons les noms images et les coordonnées qui sont dans "ornates.xml":

		//--------------------------------------------------------------------------------------------------------
		void    CWalls::readOrnatesDB()
		{
			QDomDocument doc;

			QFile f("databases/ornates.xml");
			f.open(QIODevice::ReadOnly);
			doc.setContent(&f);
			f.close();

			QDomElement root = doc.documentElement();
			QDomElement ornate = root.firstChild().toElement();

			while(!ornate.isNull())
			{
				if (ornate.tagName() == "ornate")
				{
					COrnateData   newOrnate;

					newOrnate.name = ornate.attribute("name");

					QDomElement ornateInfo = ornate.firstChild().toElement();

					while(!ornateInfo.isNull())
					{
						if (ornateInfo.tagName() == "image_front")
						{
							newOrnate.imageFront = QString("gfx/3DView/ornates/") + ornateInfo.text();
						}
						else if (ornateInfo.tagName() == "image_side")
						{
							newOrnate.imageSide = QString("gfx/3DView/ornates/") + ornateInfo.text();
						}
						else if (ornateInfo.tagName() == "pos_front")
						{
							newOrnate.posFront.x = ornateInfo.attribute("x").toInt();
							newOrnate.posFront.y = ornateInfo.attribute("y").toInt();
						}
						else if (ornateInfo.tagName() == "pos_side")
						{
							newOrnate.posSide.x = ornateInfo.attribute("x").toInt();
							newOrnate.posSide.y = ornateInfo.attribute("y").toInt();
						}

						ornateInfo = ornateInfo.nextSibling().toElement();
					}
					ornatesDatas.push_back(newOrnate);
				}
				ornate = ornate.nextSibling().toElement();
			}
		}

		//---------------------------------------------------------------------------------------------
		void    CWalls::readWallsDB()
		{
			QDomDocument doc;

			QFile f("databases/walls.xml");
			f.open(QIODevice::ReadOnly);
			doc.setContent(&f);
			f.close();

			QDomElement root = doc.documentElement();
			QDomElement wall = root.firstChild().toElement();

			while(!wall.isNull())
			{
				std::vector<CParamType> paramsTypesList;

				if (wall.tagName() == "wall")
				{
					CWallData   newWall;

					newWall.name = wall.attribute("name");

					QDomElement wallInfo = wall.firstChild().toElement();

					while(!wallInfo.isNull())
					{
						if (wallInfo.tagName() == "param")
						{
							QString paramType = wallInfo.attribute("type");

							if (paramType == "ornate")
							{
								CParamType  newParamType;
								newParamType.mType = eParamOrnate;
								newParamType.mName = wallInfo.text();
								paramsTypesList.push_back(newParamType);
							}
						}

						wallInfo = wallInfo.nextSibling().toElement();
					}
					wallsDatas.push_back(newWall);
					map.mWallsParams.push_back(paramsTypesList);
				}
				wall = wall.nextSibling().toElement();
			}
		}
				

Une autre table de murs

Le dessin des décors est fait dans "game.cpp" (je le déplacerai probablement dans un fichier "ornates.cpp" plus tard).
Les positions des décors seront calculées par rapport aux murs où ils apparaissent. Donc on a besoin d'une table avec
les positions et tailles des murs, pareille à la table que l'on a utilisée pour afficher les murs eux-mêmes:

		// table avec les positions et tailles de chaque mur pour les décors
		struct SWallInfos
		{
			CVec2   pos;
			CVec2   size;
		};

		static const SWallInfos   gWallsInfos[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH][3] =
		{
			// Rang 3 (le plus loin)
			{
				{{{0, 0}, {0, 0}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }, // 03
				{{{0, 0}, {0, 0}}, {{ 8, 58}, {36, 48}}, {{0, 0}, {0, 0}}     }, // 13
				{{{0, 0}, {0, 0}}, {{78, 59}, {12, 48}}, {{134, 59}, {12, 48}}}, // 23
				{{{0, 0}, {0, 0}}, {{0, 0}, {0, 0}},     {{180, 58}, {36, 48}}}, // 33
				{{{0, 0}, {0, 0}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }  // 43
			},

			// Rang 2
			{
				{{{-77, 58}, {70, 48}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }, // 02
				{{{  7, 58}, {70, 48}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }, // 12
				{{{ 77, 58}, {70, 48}}, {{60, 52}, {18, 74}}, {{146, 52}, {18, 74}}}, // 22
				{{{146, 58}, {70, 48}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }, // 32
				{{{216, 58}, {70, 48}}, {{0, 0}, {0, 0}},     {{0, 0}, {0, 0}}     }  // 42
			},

			// Rang 1
			{
				{{{0, 0}, {0, 0}},       {{0, 0}, {0, 0}},      {{0, 0}, {0, 0}}      }, // 01
				{{{-46, 52}, {105, 74}}, {{0, 0}, {0, 0}},      {{0, 0}, {0, 0}}      }, // 11
				{{{ 59, 52}, {105, 74}}, {{33, 42}, {27, 111}}, {{164, 42}, {27, 111}}}, // 21
				{{{164, 52}, {105, 74}}, {{0, 0}, {0, 0}},      {{0, 0}, {0, 0}}      }, // 31
				{{{0, 0}, {0, 0}},       {{0, 0}, {0, 0}},      {{0, 0}, {0, 0}}      }  // 41
			},

			// Rang 0 (le plus proche)
			{
				{{{0, 0}, {0, 0}},         {{0, 0}, {0, 0}}, {{0, 0}, {0, 0}}}, // 00
				{{{-127, 42}, {159, 111}}, {{0, 0}, {0, 0}}, {{0, 0}, {0, 0}}}, // 10
				{{{  32, 42}, {159, 111}}, {{0, 0}, {0, 0}}, {{0, 0}, {0, 0}}}, // 20
				{{{ 191, 42}, {159, 111}}, {{0, 0}, {0, 0}}, {{0, 0}, {0, 0}}}, // 30
				{{{0, 0}, {0, 0}},         {{0, 0}, {0, 0}}, {{0, 0}, {0, 0}}}  // 40
			}
		};
				
Maintenant vous pouvez vous demander pourquoi on a besoin d'une autre table alors que celle qui est dans "walls.cpp" nous
donne déjà leurs positions et les noms de fichiers qu'on peut utiliser pour connaitre leur taille.
Hé bien il y a plusieurs raisons à cela:

Mise en place des variables

Le dessin lui-même prend place dans la fonction drawWall().
Après quelques tests pour savoir si on a un décor à afficher, on initialise les principales variables dont on aura besoin:

		CWalls::COrnateData& ornateData = walls.ornatesDatas[ornate];
		const SWallInfos*   ornateTabData = &gWallsInfos[tablePos.y][tablePos.x][tableSide];
		const SWallInfos*   ornate0 = NULL;
		QString ornateFileName;
		CVec2   ornatePos;
		bool    ornateFlip = false;

		switch(side)
		{
			case eWallSideUp:
				ornate0 = &gWallsInfos[3][2][tableSide];
				ornateFileName = ornateData.imageFront;
				ornatePos = ornateData.posFront;
			break;

			case eWallSideLeft:
				ornate0 = &gWallsInfos[2][2][1];
				ornateFileName = walls.ornatesDatas[ornate].imageSide;
				ornatePos = ornateData.posSide;
			break;

			case eWallSideRight:
				ornate0 = &gWallsInfos[2][2][1];
				ornateFileName = walls.ornatesDatas[ornate].imageSide;
				ornatePos = ornateData.posSide;
				ornateFlip = true;
			break;

			default:
				break;
		}
				

Calculer la position et redimensionner l'image


		// calcule la position et le facteur d'échelle et dessine le décor dans une image temporaire
		float   scale = (float)ornateTabData->size.y / 111.0f;
		QImage  ornateImage = fileCache.getImage(ornateFileName.toLocal8Bit().constData());
		QSize   ornateSize = ornateImage.size() * scale;
		ornatePos = (ornatePos - ornate0->pos);
		ornatePos.x = ornatePos.x * ornateTabData->size.x / ornate0->size.x;
		ornatePos.y = ornatePos.y * ornateTabData->size.y / ornate0->size.y;

		if (ornateFlip == true)
			ornatePos.x = ornateTabData->size.x - ornatePos.x - ornateSize.width();

		ornatePos += ornateTabData->pos;

		QImage  tempImage(ornateSize, QImage::Format_ARGB32);
		tempImage.fill(QColor(0, 0, 0, 0));
		graph2D.drawImageScaled(&tempImage, CVec2(), ornateImage, scale, ornateFlip);
				
Dans les 3 premières lignes, on calcule le facteur d'échelle de l'image et sa taille finale.

Ensuite, on récupère la position du décor par rapport au mur de référence. Et on met à l'échelle cette position en fonction
du rapport entre les tailles du mur de destination et de celui de référence, à la fois en x et en y.
On aurait pu simplement multiplier cette position par le facteur d'échelle, mais j'ai constaté que c'était imprécis et que le
décor "bougeait" sur le mur.

Ensuite, il y a une correction si le décor est retourné, car on a calculé sa position par rapport à la gauche du mur, mais une
fois retourné ça devient une position par rapport à la droite.

Et enfin on ajoute cette position à celle du mur de destination, pour avoir la position du décor à l'écran.

Dans les 3 dernières lignes, on dessine le décor redimensionné dans une image temporaire. Je vais expliquer pourquoi tout de suite.

Assombrir l'image

On n'a pas dessiné le décor directement à l'écran parce qu'on doit l'assombrir avant.
Comme je l'ai dit dans la dernière partie, les décors apparaissent plus sombres quand ils sont plus loin de nous.

		// assombrit le décor en fonction de sa distance
		static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
		float shadow;

		if (side == eWallSideUp)
			shadow = shadowLevels[WALL_TABLE_HEIGHT - 1 - tablePos.y];
		else
			shadow = shadowLevels[WALL_TABLE_HEIGHT - 2 - tablePos.y];

		QPainter painter(&tempImage);
		painter.setOpacity(shadow);
		painter.setCompositionMode(QPainter::CompositionMode_SourceAtop);
		painter.drawImage(QPoint(0, 0), *shadowImage);
		painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
		painter.setOpacity(1.0f);
		painter.end();

		// dessine les décors à l'écran.
		graph2D.drawImage(image, ornatePos, tempImage, 0, false, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
				
Pour réaliser ça, on change l'opacité de dessin et on dessine par dessus notre image temporaire une "image d'ombre".
C'est simplement une image remplie de noir initialisée auparavant.

Le mode de composition de Qt est choisi pour éviter d'assombrir les pixels transparents de notre décor.

Finalement on dessine notre décor à l'écran. Le dernier paramètre est un rectangle de "clipping" pour éviter de dessiner en
dehors de la vue 3D.
Un décor au milieu de la zone des armes ne serait pas beau.

De meilleurs graphismes

Comme je l'ai dit dans la dernière partie, dans le jeu d'origine, quand les décors étaient redimensionnés et assombris ils devaient
se contraindre à une palette de couleurs donnée et le résultat était parfois bizarre.
On n'est pas contraints à utiliser une palette fixe, et nos décors sont redimensionnés avec un filtrage bilinéraire, ce qui donne des
images qui sont plus proches de l'image à sa taille d'origine.

Regardez la différence sur les images ci-dessous. A gauche c'est des screenshots du jeu d'origine qui tourne sur un émulateur, et à
droite on a des screenshots de notre jeu:


Et voici encore quelques exemples: