Partie 27: Parchemins et textes sur les murs

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.

Textes multi-lignes

Dans cette partie on va parler des textes sur les parchemins et sur les murs.



Ces textes sont particuliers. Quand ils sont affichés, ils sont découpés en plusieurs lignes.
Au départ, je pensais que j'utiliserais un paramètre "largeur", et que je découperais le texte quand il serait plus
large que cette largeur en utilisant un algorithme de word-wrapping.
Mais j'ai changé d'avis quand j'ai vu ce texte dans un émulateur:


Ici on voit que dans le jeu d'origine ils n'utilisaient pas un algorithme pour limiter la largeur du texte.
S'ils l'avaient fait, la 2ième ligne du texte aurait au moins contenu "FOR OLD" parce que c'est plus court que la
1ère ligne.
Ils ont du insérer des "retours à la ligne" à la main dans le texte de départ.
Alors c'est ce que j'ai fait. J'ai choisi d'utiliser le caractère "_" comme retour à la ligne et voici à quoi
ressemblent les textes des deux parchemins du premier niveau:

		<text id="SCROLL00">INVOQUE LE_FUL ET TU_OBTIENDRAS_UNE TOUCHE_MAGIQUE.</text>
		<text id="SCROLL01">LES VIEUX OS_AURONT_NOUVELLE VIE</text>
				
Maintenant il faut qu'on écrive une fonction pour découper ces textes:

		std::vector<std::string> CInterface::getTextLines(std::string text)
		{
			std::vector<std::string>    lines;
			size_t  start = 0;
			size_t  end = 0;

			while (true)
			{
				end = text.find('_', start);

				if (end != std::string::npos)
				{
					lines.push_back(text.substr(start, end - start));
					start = end + 1;
				}
				else
				{
					lines.push_back(text.substr(start, end));
					break;
				}
			}
			return lines;
		}
				
Ici on utilise les fonctions de std::string pour couper les lignes et on les stocke dans un vector de std::string.

Avant d'écrire une fonction pour afficher ce vecteur de strings, je voulais nettoyer un peu la fonction drawText().
Jusqu'à maintenant, elle utilisait une valeur de "8" qui correspondait à la largeur des caractères dans l'image de
la police. J'ai remplacé cette valeur par un define "CHAR_WIDTH".
Elle utilisait aussi une variable bizarre appelée "offset" qui était soustraite de CHAR_WIDTH pour obtenir
l'espacement entre deux caractères à l'écran. J'ai remplacé ça par une fonction.
Alors voila le code "nettoyé":

		//------------------------------------------------------------
		int CInterface::getCharSpacing(EFonts font)
		{
			if (font == eFontStandard)
				return 6;
			else if (font == eFontScroll)
				return 6;
			else
				return 8;
		}

		//------------------------------------------------------------
		#define CHAR_WIDTH  8

		void   CInterface::drawText(QImage* image, CVec2 pos, EFonts font, const char* text, QColor color)
		{
			[...]

			int     spacing = getCharSpacing(font);

			[...]

			int i = 0;

			while (text[i] != 0)
			{
				int c = codes[text[i] - 32];

				CRect   rect(CVec2(c * CHAR_WIDTH, 0),
				             CVec2((c + 1) * CHAR_WIDTH - 1, file.height() - 1));
				CVec2   cPos(pos.x + i * spacing, pos.y);

				graph2D.drawImageAtlas(image, cPos, file, rect, (font != eFontWalls ? color : QColor(0, 0, 0, 0)));
				i++;
			}
		}
				
Maintenant, avant d'écrire notre fonction multi-lignes, regardons encore le résultat qu'on veut obtenir.
Vous aurez remarqué que chaque ligne de texte est centrée horizontalement.
De plus, pour les parchemins, le texte est centré verticalement dans l'image du parchemin.
Alors notre fonction aura besoin d'un paramètre "height" pour connaitre la hauteur du parchemin. Dans le cas d'un
mur, on mettra ce paramètre à 0.
De plus, on aura besoin d'un paramètre pour la hauteur d'une ligne (la valeur qu'on ajoutera à la coordonnée y
entre chaque ligne sera différente pour les polices des mur et des parchemins).
En dehors de ces paramètres, notre fonction aura les même paramètres que drawText(): position, police, couleur...
Mais comme les lignes sont centrées horizontalement, la coordonnée x de la position nous donnera la position de la
ligne centrale du texte.


Alors, voici le code de notre fonction:

		void   CInterface::drawTextMultiLines(QImage* image, CVec2 pos, EFonts font, std::string text, QColor color, int height, int lineHeight)
		{
			std::vector<std::string> lines = getTextLines(text);

			// centre le texte verticalement
			if (height != 0)
				pos.y += (height - lines.size() * lineHeight) / 2;

			// dessine chaque ligne de texte
			for (size_t i = 0; i < lines.size(); ++i)
			{
				CVec2   pos2 = pos;
				pos2.x -= lines[i].size() * getCharSpacing(font) / 2;
				drawText(image, pos2, font, lines[i].c_str(), color);
				pos.y += lineHeight;
			}
		}
				
On verra que ce n'est pas la version finale de cette fonction, car on va devoir faire quelques modifications pour
les murs.

Les paramètres des objets

Avant qu'on puisse afficher les textes des parchemins on a besoin de les définir dans l'éditeur.
Jusqu'à maintenant le fichier "items.xml" de l'éditeur ne contenait que les noms des objets.
On va maintenant utiliser le même fichier que dans le jeu, et les noms d'objets seront dans un fichier "texts.xml"
(on n'en avait pas encore dans l'éditeur).
Donc ces 2 fichiers fonctionneront exactement comme dans le jeu.

Ensuite, on va devoir ajouter un paramètre "Texte" aux parchemins dans "items.xml".

		<!-- PARCHEMIN -->
		<item name="ITEM105">
			[...]
			<param type="string">Texte</param>
		</item>
				
Les paramètres des objets fonctionnent exactement de la même façon que ceux des cases et des murs:

Paramètres des objets dans l'éditeur

Les paramètres vont apparaitre en dessous de chaque nom d'objet:


La routine pour créer ces boutons était dupliquée pour les tas d'objets sur le sol et dans les murs. Alors je l'ai
mise dans une nouvelle fonction:

		void    CEditor::addStackParamsItems(CObjectStack* stack, int startId, QQuickItem* parent, std::vector<QQuickItem *> *list)
		{
			size_t          size = 0;

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

					addButton(parent, "effacer", list, i);
					addComboBox(parent, bridge.itemsList, list, startId + i, objType);

					if (objType != 0)
					{
						std::vector<CParamType>& sourceList = map.mObjectsParams[objType - 1];

						for (size_t j = 0; j < sourceList.size(); ++j)
						{
							CParam* param = obj.mParams[j];

							if (sourceList[j].mType == eParamString)
							{
								CParamString*   par = (CParamString*)param;
								addLabel(parent, sourceList[j].mName + ":", list);
								addTextField(parent, QString::fromStdString(par->mValue), list, (j + 1) * 0x10000 + i);
							}
						}
					}
				}
				size = stack->getSize();
			}
			addButton(parent, "ajouter", list, size);
		}
				
Vous pouvez voir qu'on utilise une petite astuce pour l'identifiant qu'on donne au champ de texte.
On va avoir besoin de retrouver à la fois l'index de l'objet dans le tas et l'index du paramètre dans cet objet.
Alors l'identifiant a cette forme:

	<index du paramètre + 1> * 0x10000 + <index de l'objet>
				
Ensuite, dans CBridge::setSelParamText() on peut retrouver le bon paramètre quand l'utilisateur change le texte
d'un parchemin:

		void    CBridge::setSelParamText(qint32 id, QString value)
		{
			[...]

			if (mTabIndex == eTabTiles)
			{
				[...]
			}
			else if (mTabIndex == eTabWalls)
			{
				CVec2       pos = editor.mSelectStart / TILE_SIZE;
				EWallSide   side = editor.getWallSideAbs(editor.mSelectStart);

				if (id >= 0x10000)
				{
					CObjectStack*   stack = map.findObjectsStack(pos, side);
					int objNum = id & 0xFFFF;
					int paramNum = (id / 0x10000) - 1;
					CObject&    obj = stack->getObject(objNum);
					CParamType  paramInfos = map.mObjectsParams[obj.getType() - 1][paramNum];

					if (paramInfos.mType == eParamString)
					{
						obj.setStringParam(paramInfos.mName, value.toLocal8Bit().constData());
					}
				}
				else
				{
					[...]
				}
			}
			else if (mTabIndex == eTabObjects)
			{
				[même code pour les tas sur le sol]
			}
		}
				

Afficher les parchemins

Dessiner les parchemins est simple. On a juste à dessiner l'image de fond et à appeler notre fonction multi-lignes.
C'est ce qu'on fait dans la fonction drawScroll():

		void    CInterface::drawScroll(QImage* image, CObject& object)
		{
			QImage  scrollBg = fileCache.getImage("gfx/interface/Scroll.png");
			graph2D.drawImage(image, CVec2(80, 85), scrollBg);

			if (isPressingEye == true)
			{
				QImage  eye = fileCache.getImage("gfx/interface/Eye.png");
				graph2D.drawImage(image, CVec2(83, 90), eye);
			}
			else
			{
				QImage  arrow = fileCache.getImage("gfx/interface/Arrow.png");
				graph2D.drawImage(image, CVec2(83, 90), arrow);
			}

			std::string text = object.getStringParam("Texte");
			drawTextMultiLines(image, CVec2(162, 91), eFontScroll, getText(text), SCROLL_TEXT_COLOR, 59, 7);
		}
				
Vous devez avoir remarqué qu'il y a un test où on affiche soit un oeil ou une flèche.
C'est parce qu'il y a 2 façon de lire un parchemin:
Dans le premier cas, drawScroll() est appelée par drawInfos().
Et dans le 2ième cas, elle est appelée par drawInventory().
Dans ce cas, on doit aussi ajouter un test dans drawBodyPart() pour afficher un parchemin ouvert dans la main:

		void    CInterface::drawBodyPart(QImage* image, CVec2 pos, int championNum, CCharacter::EBodyParts part, bool enableArea)
		{
			CCharacter* c = &game.characters[championNum];
			int     objType = c->bodyObjects[part].getType();

			if (objType != 0)
			{
				// dessine l'objet dans ce slot
				CObjects::CObjectInfo  object = objects.mObjectInfos[objType - 1];
				QImage  objImage = fileCache.getImage(object.imageFile.toLocal8Bit().constData());
				int imageNum = object.imageNum;

				// objets ouverts dans la main
				if (part == CCharacter::eBodyPartRightHand)
				{
					if (objType == OBJECT_TYPE_SCROLL)
						imageNum = 30;
				}

				CRect   rect = getItemRect(imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, rect);
			}
			[...]
		}
				

Les textes des murs

Pour les murs avec texte on crée un nouveau type de mur dans "walls.xml":

		<wall name="Texte">
			<image>WallText.png</image>
			<param type="string">Texte</param>
		</wall>
				
Voici les textes des 2 murs du premier niveau:

		<text id="WALL_TEXT00">HALL DES_CHAMPIONS</text>
		<text id="WALL_TEXT01">L'AUTEL_VIM_DE LA_RENAISSANCE</text>
				
Comme pour les autres types de murs, celui là est affiché dans CGame::drawWall().
Remarquez qu'on ne l'affiche que quand on est juste en face du mur:

		else if (wall->getType() == eWallText)
		{
			//------------------------------------------------------------------------------
			// mur avec texte
			std::string text = wall->getStringParam("Texte");
			text = interface.getText(text);

			if (tablePos == CVec2(2, 3) && side == eWallSideUp)
			{
				interface.drawTextMultiLines(image, CVec2(112, 74), CInterface::eFontWalls, text, QColor(), 0, 11);
			}
		}
				
Mais si vous lancez le code comme ça, voici ce que vous verrez:


Evidemment, le graphisme du mur est génant pour lire le texte correctement.
Spécialement pour les 2 dernières lignes.
Dans le jeu d'origine il y avait une astuce: Une partie du mur était copiée sur la ligne verticale entre les
briques.
Je n'ai pas trouvé d'image pour ça dans les graphismes d'origine, donc jen ai créé une moi-même (c'est
"TextPatch.png" dans "gfx/3DView").
Une fois qu'on la dessine, voila le résultat:

		else if (wall->getType() == eWallText)
		{
			[...]

			if (tablePos == CVec2(2, 3) && side == eWallSideUp)
			{
				QImage  patch = fileCache.getImage("gfx/3DView/TextPatch.png");
				graph2D.drawImage(image, CVec2(111, 97), patch);

				interface.drawTextMultiLines(image, CVec2(112, 74), CInterface::eFontWalls, text, QColor(), 0, 11);
			}
		}
				


La 3ième ligne est encore un peu mal positionnée, alors modifions drawTextMultiLines() pour l'abaisser:

		void   CInterface::drawTextMultiLines(QImage* image, CVec2 pos, EFonts font, std::string text, QColor color, int height, int lineHeight)
		{
			[...]

			// dessine chaque ligne de texte
			for (size_t i = 0; i < lines.size(); ++i)
			{
				[...]
				drawText(image, pos2, font, lines[i].c_str(), color);
				pos.y += lineHeight;

				if (font == eFontWalls && i == 1)
					pos.y += 3;
			}
		}
				
Et voila ce qu'on obtient:


Décor "Texte"

Quand on n'est pas juste en face du texte, il apparaît comme une image de décor illisible.


		else if (wall->getType() == eWallText)
		{
			[...]

			if (tablePos == CVec2(2, 3) && side == eWallSideUp)
			{
				[...]
			}
			else
			{
				std::vector<std::string>    lines = interface.getTextLines(text);
				walls.drawOrnate(image, tablePos, side, ORNATE_TEXT, lines.size());
			}
		}
				
Et il y a une autre petite astuce ici.
Dans le jeu d'origine, la taille de ce décor était basée sur le nombre de lignes de texte.


Alors on va devoir modifier drawOrnate pour effectuer ça:

		CRect CWalls::drawOrnate(QImage* image, CVec2 tablePos, EWallSide side, int ornate, int textLines)
		{
			if (ornate != 0)
			{
				[...]

				if (gWallsInfos[tablePos.y][tablePos.x][tableSide].size.x != 0)
				{
					[...]

					// 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());

					if (ornate == ORNATE_TEXT)
					{
						CRect   rect;
						rect.tl = CVec2(0, 0);
						rect.br.x = ornateImage.width() - 1;

						if (side == eWallSideUp)
						{
							static int heights[] = {0, 14, 27, 41, 55};
							rect.br.y = heights[textLines];
						}
						else
						{
							static int heights[] = {0, 10, 20, 31, 41};
							rect.br.y = heights[textLines];
						}
						ornateImage = graph2D.cropImage(ornateImage, rect);
					}
					[...]
				
Ici on utilise une nouvelle fonction de graph2D qui coupe une image suivant un rectangle donné:

		QImage CGraphics2D::cropImage(const QImage& srcImage, CRect rect)
		{
			QRect   qrect(rect.tl.x, rect.tl.y, rect.br.x - rect.tl.x + 1, rect.br.y - rect.tl.y + 1);
			return srcImage.copy(qrect);
		}