Partie 22: Objets - Partie 2: Sur le sol

Téléchargements

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

La base de données des objets

On va commencer par créer le fichier "items.xml" qu'on va utiliser dans le jeu.
Il va être assez différent de celui que j'ai écrit pour l'éditeur dans la dernière partie, qui contenait seulement
des noms.
Il faudra probablement qu'on change ça plus tard pour utiliser le même fichier aux 2 endroits, mais pour l'instant
on utilisera 2 fichiers différents pendant un petit moment.
Ce sera surement la plus grosse base de données du jeu, car elle va définir toutes les caractéristiques des 170
objets.
Mais on va commencer par une version simple et on ajoutera les différentes caractéristiques quand on en aura besoin
dans les parties futures.

Alors voyons de quelles données on a besoin pour afficher nos objets sur le sol:

		<?xml version="1.0" encoding="utf-8"?>
		<items>
			<!-- ======================== Armes ======================== -->
			<!-- OEIL DU TEMPS -->
			<item name="ITEM001">
				<category>Weapon</category>
				<floor_image>00W_Ring.png</floor_image>
			</item>

			<!-- ROND D'ECLAIRS -->
			<item name="ITEM002">
				<category>Weapon</category>
				<floor_image>00W_Ring.png</floor_image>
			</item>

			[...]
		</items>
				
D'abord chaque objet a un nom. Je les ai ajoutés à "texts.xml" car ils apparaitront dans le jeu, mais on ne les
utilisera pas dans cette partie.

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<texts>
			[...]
			<text id="ITEM001">OEIL DU TEMPS</text>
			<text id="ITEM002">ROND D'ECLAIRS</text>
			<text id="ITEM003">TORCHE</text>
			<text id="ITEM004">FLAMITAINE</text>
			[...]
		</texts>
				
Ensuite, on trouve la catégorie de l'objet. Chaque objet appartient à une des 6 catégories suivantes:
Ce champ catégorie ne sera pas utilisé dans cette partie, mais je l'ai ajouté parce que c'est plus clair pour trier
les objets.
A l'avenir, il pourrait être utile pour définir les paramètres additionnels. Par ex. une arme aura une valeur
d'attaque, une armure aura une valeur de défense, un parchemin aura un texte, etc...

Enfin, chaque objet dans la base de données contient le nom de l'image utilisée pour le dessiner sur le sol.
Vous pouvez les trouver dans "data/gfx/3DView/items_floor/", je les ai numérotées et triés par catégorie:


Les graphistes n'ont pas dessiné une image pour chaque objet, donc pour les 170 objets, il y a seulement 86 images
différentes.
Certains objets partagent la même image (par ex. les armures de plaque, la plupart des épées, la plupart des
potions, ...).
Ce fichier est lu par readObjectsDB() dans "objects.cpp", un nouveau fichier source pour toutes les fonctions
relatives aux objets.
Les données sont stockées dans le vecteur mObjectInfos.

Positions des objets

Dans le jeu d'origine, les objets étaient placés bizarrement sur le sol:


Ils utilisaient probablement une grosse table de coordonnées pour chaque position possible.
Je ne voulais pas écrire une autre grosse table dans le code, alors j'ai trouvé une formule pour obtenir à peu près
les mêmes positions:


Ca m'a pris plus de temps que je le pensais.
Normalement, pour avoir un effet de perspective comme ça, vous devez diviser les coodonnées par la "profondeur" et
ajouter quelques coefficients pour redimensionner le tout.
Mais comme je l'ai dit auparavant, ce jeu est plein d'erreurs de perspective, alors j'ai fini avec une formule bizarre
avec un facteur de correction de 0.9 sorti de nulle part.
La voilà:

		CVec2 CObjects::getObjectPos(CVec2 tablePos)
		{
			CVec2   pos;
			pos.x = 250 * (tablePos.x - 4.5) / (8.5 - tablePos.y) + 112;
			pos.y = 310 / (8.5 - 0.9 * tablePos.y) + 66;
			return pos;
		}
				
Néanmoins ça donne un résultat assez proche du jeu d'origine. Et cette fonction sera utile plus tard quand on
ajoutera les monstres.

Les rangées avant et arrière

Comme je l'ai dit quand on parlait des portes, les objets sur une case doivent être dessinés en 2 rangées séparées
parce que certains peuvent être cachés derrière une porte.
Donc on a 2 fonctions, une pour la rangée arrière (la plus loin de nous) et une pour la rangée avant (la plus
proche).
Voici la fonction pour la rangée arrière:

		void CObjects::drawBackRow(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			CVec2   pos1, pos2;

			switch (player.dir)
			{
				case 0: // haut
					pos1 = CVec2(0, 0);
					pos2 = CVec2(1, 0);
					break;

				case 1: // gauche
					pos1 = CVec2(0, 1);
					pos2 = CVec2(0, 0);
					break;

				case 2: // bas
					pos1 = CVec2(1, 1);
					pos2 = CVec2(0, 1);
					break;

				case 3: // droite
					pos1 = CVec2(1, 0);
					pos2 = CVec2(1, 1);
					break;
			}

			drawObjectsStack(image, mapPos * 2 + pos1, tablePos * 2 + CVec2(0, 0));
			drawObjectsStack(image, mapPos * 2 + pos2, tablePos * 2 + CVec2(1, 0));
		}
				
Vous pouvez voir que les coordonnées de la case sont multipliées par 2 parce qu'il y a 4 endroits pour poser les
tas d'objets dans une case.


Dans cette foncton, il y a 4 cas différents où on positionne les coordonnées en fonction de la direction dans
laquelle le joueur regarde.
Cette image va vous expliquer d'où viennent ces coordonnées:

La fonction pour la rangée de devant est la même à part pour les coordonnées.

Dessiner les objets

Dessiner les objets est assez similaire aux décors des murs.

		void CObjects::drawObjectsStack(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			if (tablePos.y < 1 ||
				tablePos.y > (WALL_TABLE_HEIGHT - 1) * 2)
				return;

			CObjectStack*   stack = map.findObjectsStack(mapPos);

			if (stack != NULL)
			{
				for (size_t i = 0; i < stack->getSize(); ++i)
				{
					int type = stack->getObject(i).getType();

					if (type != 0)
					{
						// récupère l'image de l'objet
						CObjectInfo object = mObjectInfos[type - 1];
						QImage  objectImage = fileCache.getImage(object.floorImage.toLocal8Bit().constData());

						// récupère la position de l'objet et ajoute une valeur pseudo-aléatoire
						CVec2   pos = getObjectPos(tablePos);
						pos.x += ((mapPos.x - mapPos.y + i) * 53) % 7 - 3;
						pos.y += (i < 4 ? i : 3);

						// calcule la taille par rapport à la position de référence (la plus proche)
						CVec2   pos0 = getObjectPos(CVec2(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2));
						CVec2   pos1 = getObjectPos(CVec2(WALL_TABLE_WIDTH, tablePos.y));
						float   scale = (float)(pos1.x - 112) / (float)(pos0.x - 112);

						// redimensionne l'objet dans une image temporaire
						QImage  scaledObject(objectImage.size() * scale, QImage::Format_ARGB32);
						scaledObject.fill(QColor(0, 0, 0, 0));
						graph2D.drawImageScaled(&scaledObject, CVec2(), objectImage, scale);

						// assombrit l'objet en fonction de sa distance
						float shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePos.y) * 0.13f;
						graph2D.darken(&scaledObject, shadow);

						// dessine l'objet à l'écran
						pos -= CVec2(scaledObject.size().width() / 2, scaledObject.size().height() - 1);
						graph2D.drawImage(image, pos, scaledObject, 0, false, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
					}
				}
			}
		}
				
On redimensionne l'image et on l'assombrit en fonction de la distance de l'objet.

Avant d'afficher l'objet, il on doit soustraire à la position la moitié de la largeur et toute le hauteur de l'image,
pour que la position qu'on a calculée dans getObjectPos() corresponde au milieu/bas de l'objet.


On fait ça parce que comme les objets sont posés sur le sol, 2 objets différents doivent être alignés par le bas.


Enfin, il y a encore une petite astuce que vous pouvez voir au début de la boucle: on ajoute un peu d'aléa à la
position pour éviter que les objets se superposent complètement quand il y a plusieurs fois le même objet dans un
tas.