Partie 40: Les monstres partie 2: Paramètres et affichage

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

Tous les groupes de monstres vont avoir les mêmes paramètres:



		<!-- 01 Scorpion Géant -->
		<monster name = "MONSTER01">
			[...]
			<param type="int">nbMonstres</param>
			<param type="enum" values="Haut;Bas;Gauche;Droite">Dir</param>
			<param type="stack"/>
		</monster>

		<!-- 02 Démon Baveux -->
		<monster name = "MONSTER02">
			[...]
			<param type="int">nbMonstres</param>
			<param type="enum" values="Haut;Bas;Gauche;Droite">Dir</param>
			<param type="stack"/>
		</monster>
				
"nbMonstres" est le nombre de monstres dans ce groupe.
"Dir" est la direction dans laquelle le groupe est tourné au départ.
Et le tas d'objets contient tous les objets qui tomberont sur le sol quand tous les monstres du groupe seront
morts.
Ces tas seront stockés au même endroit que les autres.
Comme on a utilisé WallSideMax pour les tas dans les murs, on va utiliser une autre valeur pour différencier les
tas des monstres.

		enum EWallSide
		{
			eWallSideUp = 0,
			eWallSideDown,
			eWallSideLeft,
			eWallSideRight,
			eWallSideMax
		};

		#define eWallSideMonster (EWallSide)(eWallSideMax + 1)
				
Bien sur ces paramètres impliquent des changements dans les outils de l'éditeur, mais je n'expliquerai pas ça en
détail.

Charger et initialiser les monstres

Dans le jeu, la base de données des monstres est chargée dans un nouveau fichier "monsters.cpp":

		void CMonsters::readMonstersDB()
		{
			QDomDocument doc;

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

			QDomElement root = doc.documentElement();
			QDomNode    monsterNode = root.firstChild();

			while(!monsterNode.isNull())
			{
				if (monsterNode.isElement())
				{
					QDomElement monster = monsterNode.toElement();
					std::vector<CParamType> paramsTypesList;

					if (monster.tagName() == "monster")
					{
						QDomElement monsterInfo = monster.firstChild().toElement();
						SMonsterData    monsterData;

						while(!monsterInfo.isNull())
						{
							if (monsterInfo.tagName() == "img_front")
							{
								monsterData.frontImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_side")
							{
								monsterData.sideImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_back")
							{
								monsterData.backImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_attack")
							{
								monsterData.attackImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "param")
							{
								QString paramType = monsterInfo.attribute("type");

								CParamType  newParamType;

								if (paramType == "int")
								{
									newParamType.mType = eParamInt;
									newParamType.mName = monsterInfo.text();
									paramsTypesList.push_back(newParamType);
								}
								else if (paramType == "enum")
								{
									newParamType.mType = eParamEnum;
									newParamType.mName = monsterInfo.text();
									newParamType.mValues = monsterInfo.attribute("values").split(";");
									paramsTypesList.push_back(newParamType);
								}
								else if (paramType == "stack")
								{
									newParamType.mType = eParamStack;
									paramsTypesList.push_back(newParamType);
								}
							}

							monsterInfo = monsterInfo.nextSibling().toElement();
						}

						CMap::mMonstersParams.push_back(paramsTypesList);
						monstersDatas.push_back(monsterData);
					}
				}
				monsterNode = monsterNode.nextSibling();
			}
		}
				
Vous pouvez voir que les données des groupes sont stockées dans difféentes listes.
Les paramètres sont stockés dans la classe CMap, comme pour les autres éléments (murs, cases, etc.).
Et les images des monstres sont stockées dans la liste monstersDatas:

		class CMonsters
		{
		public:
			[...]

			struct SMonsterData
			{
				QString frontImage;
				QString sideImage;
				QString backImage;
				QString attackImage;
			};
			[...]

		private:
			[...]

			std::vector<SMonsterData>   monstersDatas;
		};
				
Une fois qu'on a chargé la base de données, les groupes chargés dans le fichier map sont stockés dans la liste
monsterGroups.

		class CMonsters
		{
		public:
			[...]

			struct SMonsterGroup
			{
				SMonster    monsters[4];
				EMonsterDir dir;
			};
			[...]

		private:
			[...]

			std::vector<SMonsterGroup>  monsterGroups;
		};
				
Vous pouvez voir que le groupe peut contenir 4 monstres. Après avoir chargé la map, on appelle la fonction init()
qui va initialiser chacun d'eux.

		class CMonsters
		{
		public:
			enum EMonsterPos
			{
				eMonsterPosNone,    // monstre vide
				eMonsterPosNorthWest,
				eMonsterPosNorthEast,
				eMonsterPosSouthWest,
				eMonsterPosSouthEast,
				eMonsterPosCenter
			};

			enum EMonsterDir
			{
				eMonsterDirUp,
				eMonsterDirDown,
				eMonsterDirLeft,
				eMonsterDirRight
			};

			struct SMonsterData
			{
				QString frontImage;
				QString sideImage;
				QString backImage;
				QString attackImage;
			};

			struct SMonster
			{
				EMonsterPos pos;
				EMonsterDir dir;
				int life;
			};
				
Comme vous pouvez le voir, chaque monstre a une position dans le groupe, sa propre direction et un compteur de vie.
Voici la fonction init():

		void CMonsters::init()
		{
			for (size_t i = 0; i < map.mMonsterGroups.size(); ++i)
			{
				CMonsterGroup&  mapGroup = map.mMonsterGroups[i];
				SMonsterGroup   group;
				int nbMonsters = mapGroup.getIntParam("nbMonstres");

				group.dir = (EMonsterDir)mapGroup.getEnumParam("Dir");

				for (int j = 0; j < nbMonsters; j++)
					group.monsters[j].dir = group.dir;

				if (nbMonsters == 1)
				{
					group.monsters[0].pos = eMonsterPosCenter;
					group.monsters[0].life = 3;

					for (int j = 1; j < 4; j++)
					{
						group.monsters[j].pos = eMonsterPosNone;
						group.monsters[j].life = 0;
					}
				}
				else
				{
					// positions des monstres pour une direction donnée
					static const EMonsterPos startingPos[4][4] =
					{
						{eMonsterPosNorthWest, eMonsterPosNorthEast, eMonsterPosSouthWest, eMonsterPosSouthEast},   // up
						{eMonsterPosSouthEast, eMonsterPosSouthWest, eMonsterPosNorthEast, eMonsterPosNorthWest},   // down
						{eMonsterPosNorthWest, eMonsterPosSouthWest, eMonsterPosNorthEast, eMonsterPosSouthEast},   // left
						{eMonsterPosSouthEast, eMonsterPosNorthEast, eMonsterPosSouthWest, eMonsterPosNorthWest}    // right
					};

					for (int j = 0; j < 4; j++)
					{
						if (j < nbMonsters)
						{
							group.monsters[j].pos = startingPos[group.dir][j];
							group.monsters[j].life = 3;
						}
						else
						{
							group.monsters[j].pos = eMonsterPosNone;
							group.monsters[j].life = 0;
						}
					}
				}

				monsterGroups.push_back(group);
			}
		}
				

Dessiner les monstres



Les monstres sont affichés dans la fonction CMonsters::draw().
D'abord on a une boucle for parce que l'on doit faire 3 passes pour dessiner les monstres en suivant la perspective.
On va d'abord dessiner les monstres sur la ligne de derrière, puis ceux au centre, et enfin ceux de devant.

		void CMonsters::draw(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			if (tablePos.y == WALL_TABLE_HEIGHT - 1)
				return;

			int groupIndex = map.findMonsterGroupIndex(mapPos);

			if (groupIndex != -1)
			{
				CMonsterGroup&  mapGroup = map.mMonsterGroups[groupIndex];
				SMonsterGroup&  group = monsterGroups[groupIndex];

				// position des monstres en fonction de la position du joueur
				static const float monsterPosDir[4][5][2] =
				{
					//   NW          NE          SW          SE           C
					{{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0},  {0.5, 0.5}},   // haut
					{{1.0, 0.0}, {1.0, 1.0}, {0.0, 0.0}, {0.0, 1.0},  {0.5, 0.5}},   // gauche
					{{1.0, 1.0}, {0.0, 1.0}, {1.0, 0.0}, {0.0, 0.0},  {0.5, 0.5}},   // bas
					{{0.0, 1.0}, {0.0, 0.0}, {1.0, 1.0}, {1.0, 0.0},  {0.5, 0.5}}    // droite
				};

				// direction des monstres en fonction de la direction du joueur
				static const EMonsterDir monsterDirDir[4][4] =
				{
					{eMonsterDirUp,    eMonsterDirDown,  eMonsterDirLeft,  eMonsterDirRight}, // haut
					{eMonsterDirRight, eMonsterDirLeft,  eMonsterDirUp,    eMonsterDirDown},  // gauche
					{eMonsterDirDown,  eMonsterDirUp,    eMonsterDirRight, eMonsterDirLeft},  // bas
					{eMonsterDirLeft,  eMonsterDirRight, eMonsterDirDown,  eMonsterDirUp}     // droite
				};

				// on dessine la ligne arrière des monstres avant ceux de devant
				for (int row = 0; row < 3; ++row)
				{
				
Ensuite, on récupère la direction du monstre par rapport au joueur et on trouve son sprite.

					for (int i = 0; i < 4; ++i)
					{
						SMonster&   monster = group.monsters[i];

						if (monster.pos != eMonsterPosNone)
						{
							// direction relative
							EMonsterDir tableDir = monsterDirDir[player.dir][monster.dir];

							// sprite
							int type = mapGroup.getType();
							QString spriteName;
							bool    flip = false;

							if (tableDir == eMonsterDirDown)
							{
								spriteName = monstersDatas[type].frontImage;
							}
							else if (tableDir == eMonsterDirUp)
							{
								spriteName = monstersDatas[type].backImage;
								if (spriteName.isEmpty() == true)
									spriteName = monstersDatas[type].frontImage;
							}
							else
							{
								spriteName = monstersDatas[type].sideImage;
								if (spriteName.isEmpty() == true)
									spriteName = monstersDatas[type].frontImage;
								else if (tableDir == eMonsterDirLeft)
									flip = true;
							}
				
Si le monstre est dans le rang qu'on veut dessiner, on calcule sa position à l'écran.

							if (monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][1] == (float)row / 2.0)
							{
								// position dans la table
								float tablePosX = tablePos.x * 2 + monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][0];
								float tablePosY = tablePos.y * 2 + monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][1];

								// position à l'écran
								QImage  sprite = fileCache.getImage(spriteName.toStdString());
								CVec2   pos = getMonsterPos(tablePosX, tablePosY);
				
On le redimensionne et on l'assombrit comme la plupart des éléments du jeu

								// calcule la taille par rapport à la position de référence (la plus proche)
								CVec2   pos0, pos1;
								float   scale;

								pos0 = getMonsterPos(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2 + 1);
								pos1 = getMonsterPos(WALL_TABLE_WIDTH, tablePosY);
								scale = 2.37 * (float)(pos1.x - 112) / (float)(pos0.x - 112);

								// redimensionne le monstre dans une image temporaire
								QImage  scaledMonster(sprite.size() * scale, QImage::Format_ARGB32);
								scaledMonster.fill(TRANSPARENT);
								graph2D.drawImageScaled(&scaledMonster, CVec2(), sprite, scale);

								// assombrit le monstre en fonction de la distance
								float shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePosY - 1) * 0.13f;
								graph2D.darken(&scaledMonster, shadow);
				
Et enfin on l'affiche en se basant sur la position de ses pieds.

								// dessine le monstre (position relative à ses pieds)
								pos.x -= scaledMonster.width() / 2;
								pos.y -= scaledMonster.height() - 9 * scale;
								graph2D.drawImage(image, pos, scaledMonster, 0, flip, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
							}
						}
					}
				}
			}
		}