Partie 17: Portes - Partie 1: Editeur 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.

Les graphismes

Les portes sont des objets complexes dans ce jeu, et le fait que l'on utilise une représentation basée sur les murs
pour nos maps va apporter encore un peu plus de complexité.


Comme le jeu d'origine était basé sur des cases, une porte se trouve au milieu d'une case.
Elle est composée de la porte elle même, d'un encadrement et parfois d'un bouton sur l'encadrement.
Mais quand on regarde les graphismes, on peut voir que l'encadrement est en 3 parties.


Las images Frame_Left_* et Frame_Far_Left_0 sont utilisées pour la partie gauche de l'encadrement.
La partie droite utilise ces images retournées horizontalement.
Et il y a 2 version de la partie haute de l'encadrement. Cette partie n'est pas visible sur les portes les plus
éloignées.

Frame_Face_2 est affichée quand on est sur la même case que la porte et qu'on regarde vers les murs de coté.

Dans l'éditeur

Commençons par créer un fichier "doors.xml" avec les types de portes qu'on a vus dans les graphismes:

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<doors>
			<door name="Herse">
				<image1>Porticullis_1.png</image1>
				<image2>Porticullis_2.png</image2>
				<image3>Porticullis_3.png</image3>
			</door>

			<door name="Porte en Bois">
				<image1>Wooden_Door_1.png</image1>
				<image2>Wooden_Door_2.png</image2>
				<image3>Wooden_Door_3.png</image3>
			</door>


			<door name="Porte en Fer">
				<image1>Iron_Door_1.png</image1>
				<image2>Iron_Door_2.png</image2>
				<image3>Iron_Door_3.png</image3>
			</door>

			<door name="Porte Ra">
				<image1>Ra_Door_1.png</image1>
				<image2>Ra_Door_2.png</image2>
				<image3>Ra_Door_3.png</image3>
			</door>
		</doors>
				
Dans l'éditeur, on voudrait voir les portes dans différentes orientations.


Vous pouvez penser que la façon la plus facile de faire ça est de dessiner la porte dans les cases et de lui ajouter
un paramètre de "rotation".
Mais si on veut faire ça sans faire un cas particulier dans l'éditeur, toutes les cases devraient avoir ce paramètre
de rotation.
Et on devrait le sauver dans le fichier map. Mais pour 99% des cases ce paramètre serait inutile, et on se
retrouverait avec des fichier map plus gros.

Une autre façon de faire ça serait de créer 2 types de cases "porte", un pour les horizontales et un pour les
verticales.
Mais ça serait inélégant, car on devrait ajouter les mêmes paramètres aux 2 types de portes. Et comme vous allez le
voir plus tard, les portes ont besoin de beaucoup de paramètres.
En règle générale, dupliquer les données comme ça entraîne plus de bugs (par ex. si on ajoute un paramètre à un type
porte et qu'on oublie de l'ajouter à l'autre type).

De plus, quand on dessine un mur, ça serait utile de savoir directement si c'est un coté de porte sans avoir à
tester la case correspondante, comme à un moment il va être dessiné différemment.
Et il serait aussi important de stocker la plupart des données du mur dans la case où il est, à cause du même
problème de duplication des données. Si chaque mur autour de la porte contient un booléen qui dit si la porte est
ouverte, ça mènera évidemment à la confusion si leurs états ne changent pas exactement au même moment, et encore
une fois ça prendra plus de place dans le fichier map.

Alors j'ai opté pour une 3ième façon de représenter les portes. La case ne sera pas dessinée avec une image spéciale
(ce sera la même qu'un sol classique). Elle contiendra toutes les données de la porte, mais pas d'orientation
spécifique.
Et les murs de coté auront un type spécial avec un graphisme qui correspond à la moitié de la porte, mais pas
d'autre donnée.



		<?xml version="1.0" encoding="ISO-8859-1"?>
		<walls>
			<wall name="Nothing">
			</wall>

			<wall name="Simple">
				[...]
			</wall>

			<wall name="Portrait">
				[...]
			</wall>

			<wall name="Door Side">
				<image>DoorSide.png</image>
			</wall>
		</walls>
				
De cette façon, c'est facile de dessiner n'importe quel type de porte dans l'éditeur en positionnant simplement ses
murs de coté.
Et dans le code, il sera facile de retrouver l'orientation d'une porte à partir de sa case en testant si un de ses
murs est un mur de coté.

Paramètres d'une porte

Les paramètres qu'on utilisera pour une porte ressemblent à ça dans l'éditeur:


Type est le type de porte tel qu'on l'a défini dans "doors.xml" (en fer, en bois, etc...).

Comme pour les murs et les sols, il y a aussi des décors pour les portes. De cette façon, en combinant les 4 types
de portes avec les différents décors, on obtient un grand nombre de variétés de portes. Malheureusement, on n'aura
pas le temps de parler de ça dans cette partie.

aUnBouton dit si la porte a un bouton sur son encadrement pour l'ouvrir.

estHorizontale dit si la porte s'ouvre verticalement en glissant à travers le plafond, ou horizontalement en se
séparant en 2, chaque partie glissant à l'intérieur des murs de coté.

estCassable: on verra plus tard que certaines portes n'ont pas de moyen de s'ouvrir autrement qu'en les "découpant"
avec une hache ou un épée.

estOuverte dit si la porte est ouverte ou fermée. Ca sera principalement modifié par le code car la plupart des
portes seront fermées au départ.

estEnMouvement et pos seront utilisés dans le code pour animer la porte.

Et voici "tiles.xml":

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

			<tile name="Sol">
				[...]
			</tile>

			<tile name="Porte">
				<image>Tile.png</image>
				<param type="door">Type</param>
				<param type="door_ornate">Decor</param>
				<param type="bool">aUnBouton</param>
				<param type="bool">estHorizontale</param>
				<param type="bool">estCassable</param>
				<param type="bool">estOuverte</param>
				<param type="bool">estEnMouvement</param>
				<param type="int">pos</param>
			</tile>
		</tiles>
				
Tous ces types de paramètres ont déjà ata utilisés pour les murs, c'est simplement un travail de copier/coller de
les ajouter aux cases.
Mais comme on commence à avoir beaucoup de paramètres, j'ai utilisé des macros pour réduire la taille du code et le
rendre plus facile à lire.
Vous trouverez différentes macros dans "Map.cpp". Voici par exemple le chargement des paramètres d'un mur:

		#define LOAD_PARAM(_enum,_struct,_type,_expr) \
			case _enum: \
				{ \
					_type value; \
					fread(&value, sizeof(value), 1, handle); \
					((_struct*)mParams[i])->mValue = (_expr); \
				} \
				break;

		void  CWall::load(FILE* handle)
		{
			uint8_t type;
			fread(&type, 1, 1, handle);
			setType(type);

			std::vector<CParamType>& paramTypes = map.mWallsParams[mType];

			for (size_t i = 0; i < paramTypes.size(); ++i)
			{
				switch (paramTypes[i].mType)
				{
					LOAD_PARAM(eParamOrnate, CParamOrnate, uint8_t, value)
					LOAD_PARAM(eParamInt, CParamInt, int32_t, value)
					LOAD_PARAM(eParamBool, CParamBool, uint8_t, (value == 0 ? false : true))

					default:
						break;
				}
			}
		}
				
Et le chargement d'une case ressemble à ça:

		void  CTile::load(FILE* handle)
		{
			uint8_t type;
			fread(&type, 1, 1, handle);
			setType(type);

			std::vector<CParamType>& paramTypes = map.mTilesParams[mType];

			for (size_t i = 0; i < paramTypes.size(); ++i)
			{
				switch (paramTypes[i].mType)
				{
					LOAD_PARAM(eParamOrnate, CParamOrnate, uint8_t, value)
					LOAD_PARAM(eParamDoor, CParamDoor, uint8_t, value)
					LOAD_PARAM(eParamDoorOrnate, CParamDoorOrnate, uint8_t, value)
					LOAD_PARAM(eParamInt, CParamInt, int32_t, value)
					LOAD_PARAM(eParamBool, CParamBool, uint8_t, (value == 0 ? false : true))

					default:
						break;
				}
			}
		}
				

Affichage dans le jeu

Ca m'a demandé un peu de temps de prendre des screenshots pour avoir les différentes positions des portes dans le
premier niveau.
Et aussi pour trouver les rectangles de clipping, car parfois seule une partie de l'image est affichée.
Vous pouvez voir le résultat dans le fichier "test_doors.xcf". Les rectangles rouges sont les rectangles de clipping.

Les graphismes étaient clippés pour éviter des erreurs de perspective, car les encadrements ont seulement été
dessinés pour une porte en face de nous.
Alors quelques astuces étaient utilisées. Par exemple, dans cette position, une partie de l'encadrement devrait être
visible, mais la porte a été décalée pour masquer ça.


Pour les portes les plus éloignées, comme dans le screenshot ci-dessous, une partie de l'encadrement était clippé
(celle de gauche dans ce cas) également pour éviter les erreurs de perspective.


Alors j'ai créé un fichier "doors.cpp" pour stocker toutes ces coordonnées. Vous pourrez voir que la plus grande
partie de ce fichier contient de grosses tables pour les graphismes de chaque partie de l'encadrement et de la
porte:

		// table pour la partie gauche de l'encadrement
		SDoorElem   tabLeftFrame[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			// Rang 3 (le plus loin)
			{
				{"", 0, 0, 0, 0, 0, 0},  // 03
				{"Frame_Far_Left_0.png",  16, 59,  16, 59,  31, 101}, // 13
				{"Frame_Left_0.png",      82, 60,  82, 60,  91, 101}, // 23
				{"Frame_Far_Left_0.png", 141, 60, 147, 60, 150, 102}, // 33
				{"", 0, 0, 0, 0, 0, 0}   // 43
			},

			[...]
		}
				
Le dessin est fait dans la fonction draw():

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

				SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
				if (cell.file[0] != 0)
				{
					int doorType = tile->getDoorParam("Type");
					int doorNum = cell.file[0] - '0';
					std::string fileName = doorsDatas[doorType].files[doorNum].toUtf8().constData();
					drawDoor(image, tablePos, fileName);
				}
			}
		}
				
Je ne vais pas expliquer les fonctions drawFrameElement() et drawDoor() car elles sont faciles à comprendre. Elles
lisent seulement les coordonnées dans les tableaux et affichent l'image.

Cette fonction draw() est appelée dans CGame::displayMainView(), où vous pouvez voir une autre future complication.
Comme on dessine les graphismes case par case dans cette partie, et comme les portes sont au milieu d'une case, ça
va interférer avec le dessin des objets sur le sol.
Certains objets apparaitront derrière la porte et les autres seront dessinés "devant" elle. Alors on aura besoin de
2 fonction pour dessiner tous les objets sur cette case.

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

				for (int y = 0; y < WALL_TABLE_HEIGHT; ++y)
					for (int x = 0; x < WALL_TABLE_WIDTH; ++x)
					{
						[...]

						if (tile != NULL)
						{
							// dessine les murs
							[...]

							// dessine les décors au sol
							if (tile->getType() == eTileGround)
							{
								[...]
							}

							// dessine les objets de la ligne arrière

							// dessine les portes
							if (tile->getType() == eTileDoor)
							{
								doors.draw(image, tile, tablePos);
							}

							// dessine les objets de la ligne avant
							// dessine les ennemis
						}
					}
			}
		}
				

Le dernier graphisme

Il y a encore une dernière image que l'on doit dessiner pour l'encadrement, c'est le cas où l'on est sur la même
case que la porte et qu'on regarde en direction d'un mur de coté.


Elle est simplement dessinée dans CGame::drawWall(). C'est à peu près la même chose que le portrait qu'on dessine
sur les miroirs:

            CWall*  wall = &tile->mWalls[playerSide];

            if (wall->getType() == eWallSimple)
            {
                //------------------------------------------------------------------------------
                // mur simple
                int ornate = wall->getOrnateParam("Decor");
                walls.drawOrnate(image, tablePos, side, ornate);
            }
            else if (wall->getType() == eWallPortrait)
            {
                //------------------------------------------------------------------------------
                // mur simple
                // miroir de champion
                walls.drawOrnate(image, tablePos, side, ORNATE_MIRROR);

                if (tablePos == CVec2(2, 3) && side == eWallSideUp)
                {
                    // si le miroir est juste en facede nous, on dessine la tête du champion
                    int champion = wall->getIntParam("Champion");
                    bool isEmpty = wall->getBoolParam("estVide");

                    if (isEmpty == false)
                    {
                        CRect   rect = interface.getChampionPortraitRect(champion);
                        QImage  portraits = fileCache.getImage("gfx/interface/ChampPortraits.png");
                        CVec2   pos(96, 68);
                        graph2D.drawImageAtlas(image, pos, portraits, rect);

                        int lastChampChosed;
                        for (lastChampChosed = 0; lastChampChosed < 4; ++lastChampChosed)
                            if (characters[lastChampChosed].portrait == -1)
                                break;

                        if (lastChampChosed != 4)
                        {
                            CRect   mouseRect(pos, CVec2(pos.x + CHAMP_PORTRAIT_WIDTH - 1, pos.y + CHAMP_PORTRAIT_HEIGHT - 1));
                            mouse.addArea(eMouseAreaG_Champion, mouseRect, (void*)wall);
                        }
                    }
                }
            }
            else if (wall->getType() == eWallDoorSide)
            {
                //------------------------------------------------------------------------------
		void CGame::drawWall(QImage* image, CTile* tile, CVec2 tablePos, EWallSide side, bool flip)
		{
			[...]

					// dessine les décors
					CWall*  wall = &tile->mWalls[playerSide];

					if (wall->getType() == eWallSimple)
					{
						//------------------------------------------------------------------------------
						// mur simple

						[...]
					}
					else if (wall->getType() == eWallPortrait)
					{
						//------------------------------------------------------------------------------
						// miroir de champion

						[...]
					}
					else if (wall->getType() == eWallDoorSide)
					{
						//------------------------------------------------------------------------------
						// coté d'une porte
						if (tablePos == CVec2(2, 3) && side == eWallSideUp)
						{
							QImage  doorFrame = fileCache.getImage("gfx/3DView/doors/Frame_Face_2.png");
							graph2D.drawImage(image, CVec2(96, 33), doorFrame);
						}
					}

			[...]
		}
				
Maintenant on a dessiné toutes les portes du premier niveau si vous testez le jeu, vous verrez qu'on peut les
traverser même si elles sont fermées.
La prochaine fois on ajoutera des collisions, et on verra comment les ouvrir.