Partie 16: Les décors au sol

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.

Les graphismes

Les décors au sol sont utilisés pour la décoration (mousse, grilles...), pour les plaques de pression, et pour un sort spécial
"traces de pas".
Dans cette partie, on va inclure les graphismes de tous les décors, mais on n'utilisera que les décoratifs. Les plaques de
pression seront utilisées plus tard dans un autre type de cases.


Les graphismes consistent en 6 sprites pour chaque type. 3 pour les cases en face, et 3 pour celles de gauche.
Les cases de droite utilisent les images de gauche retournées.


La plupart des décors sont symétriques, et cette façon de les afficher marche bien pour eux. Mais pour les autres, beaucoup de
bugs apparaissaient dans le jeu d'origine.

Bugs

La première chose étrange est que les décors apparaissent toujours de la même façon, quelle que soit la direction.
Par exemple, voici une flaque du début du niveau vue de 2 directions opposées:


C'est moins un bug qu'une façon d'économiser de l'espace disque, car pour les décors sans symétrie comme cette flaque, les
graphistes auraient du dessiner 3 fois plus de sprites pour couvrir tous les cas.

On a vu dans une partie précédente que le jeu utilisait une astuce pour améliorer la sensation de mouvement, qui consistait
à retourner l'image du sol chaque fois qu'on se déplaçait d'une case ou qu'on tournait à 90 degrés.
Mais même quand le sol était retourné, les décors restaient les mêmes. C'est problématique pour les décors qui suivent le
contour des pierres, comme la mousse:


Comme la plupart des décors sont symétriques, le fait de les retourner pour afficher les cases de droite ne pose pas de problème.
Mais vous pouvez voir que pour la flaque ça ne marche pas du tout:


Vous pouvez aussi voir que la flaque du milieu en face n'est pas alignée avec les autres. C'est sa vraie position dans le jeu d'origine.

Enfin, excepté pour quelques types de décors, les graphismes ne sont pas vraiment plus sombres avec la distance. Comme pour les décors
des murs, ça vient d'une limitation de la palette de couleurs utilisée dans le jeu.

J'aurais pu corriger certains de ces bugs, mais pour garder le code simple, j'ai décidé de les afficher comme ils étaient dans le jeu
d'origine.
Si vous voulez améliorer la qualité graphique par vous même, vous pouvez facilement corriger le problème d'assombrissement et le
retournement de la mousse avec le sol en utilisant des fonctions que j'ai déjà écrites.

La base de données des décors de sol

Comme on a l'habitude de le faire, commençons par créer un fichier xml pour stocker les données dont on aura besoin.
Vous pourrez trouver "floor_ornates.xml" dans le répertoire "databases":

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

			<floor_ornate name="Mousse">
				<image_10>01_Moss_10.png</image_10>
				<image_20>01_Moss_20.png</image_20>
				<image_11>01_Moss_11.png</image_11>
				<image_21>01_Moss_21.png</image_21>
				<image_12>01_Moss_12.png</image_12>
				<image_22>01_Moss_22.png</image_22>
				<pos_10 x="44" y="102"/>
				<pos_20 x="108" y="102"/>
				<pos_11 x="11" y="113"/>
				<pos_21 x="96" y="112"/>
				<pos_12 x="0" y="131"/>
				<pos_22 x="84" y="129"/>
			</floor_ornate>

			[...]
		</floor_ornates>
				
Ici on peut voir que pour chaque type on a les noms de fichiers des 6 images et leurs coordonnées à l'écran.
Les coordonnées des cases retournées seront calculées dans le code.

Les nombres qui suivent le nom de fichier (10, 20, 11, etc.) se basent sur les coordonnées de la table qu'on a utilisée pour
définir les murs.
C'est les mêmes qu'on a utilisés pour nommer les fichiers PNG des murs.

Vous remarquerez que certains types ne contiennent pas 6 images parce que certaines d'entre elles sont vides. Par exemple la
petite plaque de pression est seulement visible sur la case juste devant nous.

Les décors de sol dans l'éditeur

Comme on l'a fait pour les murs, on va ajouter une "combo box" pour ajouter un décor à la case "Sol". Donc on commence par
modifier "tiles.xml":

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<tiles>
			<tile name="Vide">
			</tile>

			<tile name="Sol">
				<image>Tile.png</image>
				<param type="ornate">Decor</param>
			</tile>
		</tiles>
				
Ensuite c'est plus ou moins la même chose que ce que l'on a fait pour les décors des murs. La plupart des changements apparaissant dans
la classe CTile:

		class CTile
		{
		public:
			CTile();
			virtual ~CTile();

			void    setType(uint8_t type);
			uint8_t getType();
			void    copyWithoutWalls(CTile& rhs);
			CTile&  operator=(CTile& rhs);
			void    load(FILE* handle);
			void    save(FILE* handle);

			CParam* findParamByName(QString name);
			uint8_t getOrnateParam(QString name);

			std::vector    mParams;
			CWall   mWalls[eWallSideMax];

		private:
			void    deleteParams();

			uint8_t mType;
		};
				
Le setType() et le getType() s'occupent d'allouer les paramètres quand on change le type de la case.
copyWithoutWalls() et l'opérateur "=" sont utilisées pour les outils du clipboard.
load() et save() sauvent les valeurs des paramètres en même temps que le type de case dans le fichier map.
Et il y a des fonctions pour retrouver un paramètre par son nom qui marchent de la même façon que celles utilisées pour les murs.

Les décors de sol dans le jeu

Jusqu'à maintenant, le jeu n'a jamais eu besoin de charger "tiles.xml", mais maintenant on a besoin de le faire pour avoir les paramètres
des cases.

On a aussi besoin de charger "floor_ornates.xml" mais regardons d'abord comment il sera stocké. Il y a un nouveau fichier "tiles.h" où
vous pouvez trouver ceci:

		class CTiles
		{
		public:
			struct COrnateElement
			{
				QString image;
				CVec2   pos;
			};

			struct COrnateData
			{
				QString name;
				COrnateElement table[WALL_TABLE_HEIGHT][(WALL_TABLE_WIDTH + 1) / 2];
			};

			CTiles();

			void    drawFloorOrnate(QImage* image, CVec2 tablePos, int ornate);
			void    readFloorOrnatesDB();
			void    readTilesDB();

			std::vector<COrnateData>  ornatesDatas;
		};
				
Comme il y a beaucoup d'images pour un décor, on les stocke dans un tableau qui fait la moitié de la taille de celui qu'on a utilisé
pour les murs.
On a seulement besoin de la moitié de la taille parce que les images pour les cases de droite sont retournées.
Bien sur, ce tableau n'est jamais complètement rempli car par ex. il n'y a pas d'images dans la colonne 0. Mais ça évitera des calculs
dans le code.

Chaque élément de ce tableau est une structure qui contient le nom de l'image et sa position.

Ensuite, on lit la base de données dans "tiles.cpp" comme ça:

		void    CTiles::readFloorOrnatesDB()
		{
			QDomDocument doc;

			QFile f("databases/floor_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() == "floor_ornate")
				{
					COrnateData   newOrnate;

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

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

					while(!ornateInfo.isNull())
					{
						QString tag = ornateInfo.tagName();

						if (tag.startsWith("image_") == true)
						{
							std::string str = tag.toUtf8().constData();
							int x = str.at(str.size() - 2) - '0';
							int y = str.at(str.size() - 1) - '0';
							COrnateElement* elem = &newOrnate.table[y][x];
							elem->image = QString("gfx/3DView/floor_ornates/") + ornateInfo.text();
						}
						else if (tag.startsWith("pos_") == true)
						{
							std::string str = tag.toUtf8().constData();
							int x = str.at(str.size() - 2) - '0';
							int y = str.at(str.size() - 1) - '0';
							COrnateElement* elem = &newOrnate.table[y][x];
							elem->pos.x = ornateInfo.attribute("x").toInt();
							elem->pos.y = ornateInfo.attribute("y").toInt();
						}

						ornateInfo = ornateInfo.nextSibling().toElement();
					}
					ornatesDatas.push_back(newOrnate);
				}
				ornate = ornate.nextSibling().toElement();
			}
		}
				
Comme vous le voyez, on décode la position dans la table à partir du nom du champ en soustrayant le code ASCII de "0" aux
derniers caractères.

Les décors sont ensuite affichés dans displayMainView():

		void CGame::displayMainView(QImage* image)
		{
			[...]

			for (int y = 0; y < WALL_TABLE_HEIGHT; ++y)
				for (int x = 0; x < WALL_TABLE_WIDTH; ++x)
				{
					tablePos = walls.getDrawSequence(CVec2(x, y));
					localPos = walls.getMapPos(tablePos);
					mapPos = player.getPosFromLocal(localPos);
					CTile*  tile = map.getTile(mapPos);

					if (tile != NULL)
					{
						// dessine les murs
						drawWall(image, tile, tablePos, eWallSideUp, flip);
						drawWall(image, tile, tablePos, eWallSideLeft, flip);
						drawWall(image, tile, tablePos, eWallSideRight, flip);

						// dessine les décors au sol
						if (tile->getType() == eTileGround)
						{
							//------------------------------------------------------------------------------
							// case de sol
							int ornate = tile->getOrnateParam("Decor");
							tiles.drawFloorOrnate(image, tablePos, ornate);
						}

						// dessine les objets
						// dessine les ennemis
					}
				}
		}