Partie 28: Interface du jeu - Partie 4: Messages, sorts et armes

Téléchargements

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

Messages

Le bas de l'écran est utilisé pour les messages qui apparaissent quand divers évènements se déclenchent.


Il peut y avoir jusqu'à 4 lignes de texte.
Les messages apparaissent du bas de l'écran et scrollent vers le haut, en même temps que ceux au-dessus d'eux.
Chaque message disparait après environ 18 secondes.
Et ils sont écrits avec la couleur d'un champion ou la couleur bleue de l'interface.

Alors regardons comment on va les implémenter dans la classe CInterface.

		struct SMessage
		{
			std::string text;
			int champion;
			int time;
		};

		std::list<SMessage>   mMessages;
		int     messagePos;
				
On stockera les structures des messages dans une liste.
La structure du message contient:
messagePos contiendra la position du dernier message ajouté. On n'a besoin que d'une coordonnée car les autres
messages seront affichés juste au-dessus de celui-ci

Ajouter un message à la liste

Pour ajouter un message à la liste, on va écrire une fonction qui prendra comme paramètres la chaine du texte et le
numéro du champion utilisé pour la couleur:

		void CInterface::addMessage(std::string text, int champion)
		{
				
D'abord on doit tester si la liste contient 4 lignes. Si c'est le cas on retire le plus vieux message:

			if (mMessages.size() == 4)
				mMessages.pop_back();
				
Ensuite on remplit juste une nouvelle structure de message avec nos valeurs et on l'ajoute à la liste.

			SMessage    newMessage;
			newMessage.text = text;
			newMessage.champion = champion;
			newMessage.time = MSG_TIME;
			mMessages.push_front(newMessage);
				
Enfin on remplit la position du dernier message, qui est celui qu'on vient d'ajouter.

			messagePos = MSG_START_POS;
		}
				
MSG_START_POS est défini comme "200 * MSG_POS_FACTOR".
MSG_POS_FACTOR est simplement un facteur d'échelle comme on en a utilisé pour les portes pour avoir un scrolling
fluide.
Donc la coordonnée y de ce message est de 200 pixels. C'est juste en-dehors de l'écran.
Plus tard, on le fera scroller vers le haut pour le faire apparaître à l'écran.

Ecrire les messages

Pour afficher les messages, on boucle simplement à travers la liste du message le plus bas vers le plus haut et on
les écrit les uns au-dessus des autres, en utilisant la bonne couleur

		void CInterface::drawMessages(QImage* image)
		{
			std::list<SMessage>::iterator it;
			int pos = messagePos;

			for (it = mMessages.begin(); it != mMessages.end(); ++it)
			{
				QColor  color;

				if (it->champion < 4)
				{
					color = QColor(championsColors[it->champion][0],
					               championsColors[it->champion][1],
					               championsColors[it->champion][2]);
				}
				else
				{
					color = MAIN_INTERFACE_COLOR;
				}

				drawText(image, CVec2(0, pos / MSG_POS_FACTOR), eFontStandard, (it->text).c_str(), color);
				pos -= 7 * MSG_POS_FACTOR;
			}
		}
				

Mettre à jour les messages

Suivant la logique qu'on a utilisé pour les flèches, pour gérer le scrolling et la temporisation, on va écrire une
fonction de mise à jour qui sera appelée à chaque frame.
Notez que c'est une bonne habitude d'appeler ces fonctions d'update au même endroit, parce que ça simplifiera les
choses quand on implémentera des fonctions comme la pause.

		void CInterface::updateMessages()
		{
			// scrolling
			if (messagePos > MSG_DEST_POS)
				messagePos -= MSG_SCROLL_SPEED;

			if (messagePos < MSG_DEST_POS)
				messagePos = MSG_DEST_POS;

			// décrémente les temps
			std::list<SMessage>::iterator it;

			for (it = mMessages.begin(); it != mMessages.end();)
			{
				it->time--;

				if (it->time == 0)
					it = mMessages.erase(it);
				else
					++it;
			}
		}
				

Le message de résurrection


Le message de résurrection, comme la plupart des messages que l'on verra plus tard, contient le nom d'un champion.
On ne va pas ajouter un message à la base de données pour chaque champion. Alors on va utiliser un caractère "joker"
qui sera remplacé par le nom du champion.

		<text id="MESSAGE00"># RESSUSCITE.</text>
				
Voici le code pour remplacer le caractère "#":

		std::string CInterface::setChampionNameInString(int champion, std::string stringId)
		{
			std::string result = getText(stringId);
			size_t  pos = result.find('#');

			if (pos != std::string::npos)
				result.replace(pos, 1, game.characters[champion].firstName);

			return result;
		}
				
Avant d'ajouter le texte à la liste, j'ai du écrire une autre fonction pour retrouver le dernier champion ajouté à
l'équipe (en fait ce code était déjà utilisé ailleurs).

		int CInterface::findLastChampion()
		{
			for (int i = 3; i >= 0; --i)
				if (game.characters[i].portrait != -1)
					return i;

			return -1;
		}
				
Maintenant on peut ajouter le message quand on ressuscite un personnage dans CInterface::update():

		// bouton ressusciter
		case eMouseArea_Resurrect:
		{
			game.lastMirrorClicked->setBoolParam("estVide", true);
			isResurrecting = false;
			mainState = eMainGame;
			int champ = findLastChampion();
			std::string message = setChampionNameInString(champ, "MESSAGE00");
			addMessage(message, champ);
		}
		break;
				

La zone des sorts


La zone des sorts est assez complexe, avec beaucoup de boutons où on peut cliquer.
On va la mettre en place en plusieurs étapes. A chaque fois je mettrai en surbrillance en rouge la partie dont on
parle.

Mais avant ça, quelques mots à propos des variables qu'on va utiliser.
Le lanceur de sort courant sera stocké dans une variable appelée "currentSpellCaster" dans CInterface.
Le sort courant de chaque champion sera stocké dans une chaîne de caractères "spell" dans CCharacter (souvenez-vous
que quand j'ai écrit la routine de texte, j'ai choisi d'associer les symboles des sorts aux lettres minuscules).

Le dessin de tous les éléments de cette zone prendra place dans la fonction CInterface::drawSpells().

La zone de sélection du lanceur


On va la dessiner en 3 temps.
D'abord on dessine les petits boutons à gauche du lanceur sélectionné.

		CCharacter* c = &game.characters[currentSpellCaster];

		for (int i = 0; i < currentSpellCaster; ++i)
		{
			if (isChampionEmpty(i) == false && isChampionDead(i) == false)
			{
				CRect   rect(CVec2(233 + 14 * i, 42),
				             CVec2(244 + 14 * i, 48));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				if (isSpellsGreyed() == false)
					mouse.addArea(eMouseArea_SpellCaster, rect, eCursor_Arrow, (void*)i);
			}
		}
				
Puis le gros onglet du lanceur avec son nom.

		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			CRect   rect(CVec2(233 + 14 * currentSpellCaster, 42),
			             CVec2(277 + 14 * currentSpellCaster, 49));
			graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

			drawText(image, rect.tl + CVec2(2, 2), eFontStandard, c->firstName.c_str(), BLACK);
		}
				
Et finalement une boucle qui ressemble à la première pour les boutons à droite du lanceur.

		for (int i = currentSpellCaster + 1; i < 4; ++i)
		{
			if (isChampionEmpty(i) == false && isChampionDead(i) == false)
			{
				CRect   rect(CVec2(266 + 14 * i, 42),
				             CVec2(277 + 14 * i, 48));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);

				if (isSpellsGreyed() == false)
					mouse.addArea(eMouseArea_SpellCaster, rect, eCursor_Arrow, (void*)i);
			}
		}
				
Dans CInterface::update(), on gère les zones souris qu'on a ajoutés en changeant simplement le lanceur courant.

		// change le jeteur de sort
		case eMouseArea_SpellCaster:
			currentSpellCaster = (int)clickedArea->param1;
		break;
				

Les boutons des symboles


Il y a 4 différentes pages de symboles.
On sait dans laquelle on se trouve en comptant les caractères du sort courant.

		int currentPage = 0;

		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
			currentPage = strlen(c->spell) % 4;
				
Ensuite, on dessine chaque symbole comme on l'a fait dans la dernière partie qui traitait de l'interface.
Remarquez que les symboles changent en fonction du numéro de page.

		char word[2] = "a";

		for (int i = 0; i < 6; ++i)
		{
			CVec2 pos(239 + i * 14, 54);
			word[0] = 'a' + currentPage * 6 + i;
			drawText(image, pos, eFontStandard, word, MAIN_INTERFACE_COLOR);

			if (isChampionEmpty(currentSpellCaster) == false &&
				isChampionDead(currentSpellCaster) == false &&
				isSpellsGreyed() == false)
			{
				CRect   rect(CVec2(235 + 14 * i, 51),
				             CVec2(247 + 14 * i, 61));
				mouse.addArea(eMouseArea_SpellLetter, rect, eCursor_Arrow, (void*)((int)word[0]));
			}
		}
				
Quand une de ces zones souris est pressée, on ajoute le symbole correspondant au sort.
Mais si le sort contient déjà 4 caractères, on l'efface avant.

		// ajoute un symbole au sort
		case eMouseArea_SpellLetter:
		{
			CCharacter* c = &game.characters[currentSpellCaster];
			if (strlen(c->spell) == 4)
				c->spell[0] = 0;

			int i = strlen(c->spell);
			c->spell[i] = (int)clickedArea->param1;
			c->spell[i + 1] = 0;
		}
		break;
				

Le bouton "effacer"


C'est seulement une zone souris qu'on doit définir.

		// flèche "effacer"
		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			CRect   rect(CVec2(305, 63), CVec2(318, 73));
			mouse.addArea(eMouseArea_SpellBack, rect, eCursor_Arrow);
		}
				
Quand elle est cliquée, on efface la dernière lettre du sort si il n'est pas vide.

		// efface le dernier symbole du sort
		case eMouseArea_SpellBack:
		{
			CCharacter* c = &game.characters[currentSpellCaster];
			int i = strlen(c->spell);

			if (i != 0)
				c->spell[i - 1] = 0;
		}
		break;
				

Le bouton "lancer le sort"


Dans cette zone, on écrit le sort courant.
J'ai ajouté un paramètre à la fonction drawText() pour augmenter l'espace entre les symboles parce qu'ils étaient
trop proches les uns des autres.
Notez que je n'ai pas vérifié si la position des symboles était exactement au même endroit que dans le jeu
d'origine.

		// sort courant
		if (isChampionEmpty(currentSpellCaster) == false && isChampionDead(currentSpellCaster) == false)
		{
			drawText(image, CVec2(237, 66), eFontStandard, c->spell, MAIN_INTERFACE_COLOR, 1);

			if (isSpellsGreyed() == false)
			{
				CRect   rect(CVec2(234, 63), CVec2(303, 73));
				mouse.addArea(eMouseArea_CastSpell, rect, eCursor_Arrow);
			}
		}
				
Quand on clique sur ce bouton, on lance le sort.
Ici, j'écris simplement un message. On ne vérifiera pas les sorts pour l'instant.

		// jette un sort
		case eMouseArea_CastSpell:
		{
			CCharacter* c = &game.characters[currentSpellCaster];

			if (strlen(c->spell) != 0)
			{
				std::string message = setChampionNameInString(currentSpellCaster, "MESSAGE01");
				addMessage(message, 4);
				c->spell[0] = 0;
			}
		}
		break;
				

La zone des armes


La zone des armes est composée des boutons d'armes, de la liste des attaques, et de la vignette des dégats.
Je vais les expliquer séparément, mais voici un aperçu rapide des variables qu'on peut trouver dans CInterface:

Les boutons d'armes

Ca fait déjà longtemps qu'on a les rectangles de fond.

		void    CInterface::drawWeapon(QImage* image, int num)
		{
			if (isWeaponEmpty(num) == false)
			{
				// dessine le rectangle de fond
				CRect   rect(CVec2(233 + 22 * num, 86),
				             CVec2(252 + 22 * num, 120));
				graph2D.rect(image, rect, MAIN_INTERFACE_COLOR, true);
				
Maintenant pour dessiner les armes en noir, on utilise la même fonction que pour dessiner les textes dans une
couleur donnée.
Vous pouvez voir qu'il y a un cas particulier quand la main du champion est vide.
On doit aussi éviter de dessiner certains objets. Par example, une pomme n'est pas une arme, mais on verra ça dans
une autre partie.

				// dessine l'arme
				CVec2       pos = CVec2(235 + 22 * num, 96);
				int         weapon = getWeapon(num).getType();
				std::string imageFile;
				int         imageNum;

				if (weapon != 0)
				{
					CObjects::CObjectInfo  object = objects.mObjectInfos[weapon - 1];
					imageFile = object.imageFile.toLocal8Bit().constData();
					imageNum = object.imageNum;
				}
				else
				{
					// main vide
					imageFile = "gfx/interface/Items6.png";
					imageNum = 9;
				}

				QImage  objImage = fileCache.getImage(imageFile);
				CRect   objRect = getItemRect(imageNum);
				graph2D.drawImageAtlas(image, pos, objImage, objRect, BLACK);
				
Enfin on dessine un tramage quand l'arme est grisée, ou on ajoute une zone souris quand elle ne l'est pas.

				// griser ou zone souris
				if (isWeaponGreyed(num) == true)
					graph2D.patternRectangle(image, rect);
				else
					mouse.addArea(eMouseArea_Weapon, rect, eCursor_Arrow, (void*)num);
			}
		}
				
Notez que la fonction isWeaponGreyed() dépend de la variable de cooldown de l'arme.
On parlera de ces variables plus tard.

Quand on teste la zone souris dans CInterface::update(), on va changer l'état de la zone d'armes pour afficher la
liste des attaques.

		// clique sur une arme
		case eMouseArea_Weapon:
			currentWeapon = (int)clickedArea->param1;
			weaponsAreaState = eWeaponsAreaAttacks;
		break;
				

La liste d'attaques


La liste d'attaques affiche jusqu'à 3 attaques pour l'arme qu'on vient de cliquer.
Les noms des attaques vont changer suivant l'arme. Pour l'instant j'ai seulement mis les noms des 3 attaques à main
nue.
Il n'y a pas toujours 3 attaques. Ca dépend des compétences du personnage. Pour simuler ça, j'ai utilisé une règle
simple: Le premier champion a 1 attaque, le 2ième en a 2, le 3ième en a 3 et le 4ième en a 1.

Maintenant, regardons la fonction d'affichage. D'abord on affiche l'image de fond qui contient les 3 cases
d'attaques.

		void    CInterface::drawAttacks(QImage* image, int num)
		{
			// dessine l'image de fond
			QImage  attacksBg = fileCache.getImage("gfx/interface/WeaponsActions.png");
			CVec2   pos(233, 77);
			graph2D.drawImage(image, pos, attacksBg);
				
On efface les slots inutiles en dessinant un rectangle noir par dessus.

			// cache les slots inutiles
			int nbAttacks = (num % 3) + 1;

			if (nbAttacks != 3)
			{
				CRect   rect(CVec2(233, 98 + (nbAttacks - 1) * 12),
							 CVec2(319, 121));
				graph2D.rect(image, rect, BLACK, true);
			}
				
On écrit le nom du champion en haut de la liste

			// écrit le nom du personnage
			CCharacter* c = &game.characters[num];
			drawText(image, CVec2(235, 79), eFontStandard, c->firstName.c_str(), BLACK);
				
On écrit le nom des attaques. Remarquez qu'une d'entre elles peut être en surbrillance (on verra ça après).
Pour dessiner celle qui est en surbrillance, on utilise la fonction pour inverser les couleurs qu'on a utilisée
pour les flèches.

			// écrit le nom des attaques
			for (int i = 0; i < nbAttacks; ++i)
			{
				static char attackName[16];
				sprintf(attackName, "ATTACK%02d", i);
				drawText(image, CVec2(241, 89 + i * 12), eFontStandard, getText(attackName).c_str(), MAIN_INTERFACE_COLOR);

				if (attackHighlightTime != 0 && currentAttack == i)
				{
					CRect   rect(CVec2(234, 86 + i * 12),
					             CVec2(318, 96 + i * 12));
					graph2D.Xor(image, rect, MAIN_INTERFACE_COLOR);
				}
			}
				
Enfin on positionne les zones souris

			// zones souris
			if (isWeaponGreyed(num) == false && attackHighlightTime == 0)
			{
				for (int i = 0; i < nbAttacks; ++i)
				{
					CRect   rect(CVec2(234, 86 + i * 12),
					             CVec2(318, 96 + i * 12));
					mouse.addArea(eMouseArea_Attack, rect, eCursor_Arrow, (void*)i);
				}

				CRect   rect(CVec2(290, 77), CVec2(314, 83));
				mouse.addArea(eMouseArea_CloseAttack, rect, eCursor_Arrow);
			}
		}
				
Quand on clique sur une de ces zones souris, on ne quitte pas la liste tout de suite, mais l'attaque cliquée
est mise en surbrillance pendant un court instant.

		// clique sur une attaque
		case eMouseArea_Attack:
			currentAttack = (int)clickedArea->param1;
			attackHighlightTime = ATTACK_HIGHLIGHT_TIME;
		break;
				

Comme pour les messages, on écrit une fonction de mise à jour pour décrémenter ce compteur. Quand il atteint 0,
deux choses se passent:

		void CInterface::updateWeapons()
		{
			// surbrillance de l'attaque
			if (attackHighlightTime > 0)
			{
				attackHighlightTime--;

				if (attackHighlightTime == 0)
				{
					weaponCoolDown[currentWeapon] = currentAttack * 60;

					if ((rand() % 2) == 0)
					{
						weaponsAreaState = eWeaponsAreaWeapons;
					}
					else
					{
						damages = rand() % 200 + 1;
						damagesDisplayTime = DAMAGES_DISPLAY_TIME;
						weaponsAreaState = eWeaponsAreaDamage;
					}
				}
			}
				

La vignette de dégats


La vignette de dégats est simplement affichée pendant un court instant.

		void CInterface::drawDamages(QImage *image)
		{
			QImage  damagesBg = fileCache.getImage("gfx/interface/DamageDone.png");
			static char damagesStr[8];
			sprintf(damagesStr, "%d", damages);

			int     length = strlen(damagesStr);
			float   scaleX = 0.44f + (length - 1) * 0.24f;
			CVec2   pos(258 - (length - 1) * 10, 81);

			graph2D.drawImageScaled(image, pos, damagesBg, scaleX, false, 0.82f);

			CVec2   textPos(274 - (length - 1) * 3, 97);
			drawText(image, textPos, eFontStandard, damagesStr, MAIN_INTERFACE_COLOR);
		}
				
Sa temporisation est aussi mise à jour dans la fonction updateWeapons(). A la fin, on revient aux boutons d'armes.

		// timer des dégats
		if (damagesDisplayTime > 0)
		{
			damagesDisplayTime--;

			if (damagesDisplayTime == 0)
				weaponsAreaState = eWeaponsAreaWeapons;
		}
				

Le cooldown

Quand on revient aux boutons d'armes, suivant sa valeur de cooldown, celle qu'on a cliqué est peut-être encore
grisée.


Le cooldown est aussi géré dans updateWeapons().

		// cooldowns des armes
		for (int i = 0; i < 4; ++i)
			if (weaponCoolDown[i] > 0)
				weaponCoolDown[i]--;
				
On doit seulement attendre que le timer finisse pour être capable de cliquer dessus à nouveau.