Partie 20: Interface du jeu - Partie 3: Curseurs et flèches

Téléchargements

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

Il y a 2 types de curseurs dans le jeu: une flèche et une main.


En fait, il y a aussi un 3ième type, car quand vous tenez un objet, il remplace le curseur, mais on verra ça plus
tard.

En gros, la main apparait quand la souris est dans la fenêtre de jeu principale (la vue "3D") et dans la feuille de
personnage.
La flèche apparait quand la souris est au-dessus de la partie de droite de l'interface (les sorts, les armes et les
flèches de déplacement).

Pour les "boites" des champions en haut de l'écran, c'est un peu plus compliqué:

La flèche est aussi affichée dans des zones que l'on n'a pas encore: par ex. l'écran de sauvegarde.

Dans le jeu d'origine il y avait aussi un comportement étrange, qui pourrait être consideré comme un bug:
Quand on entrait dans la feuille de personnage pour ressusciter un champion, le curseur était une flèche. Mais si
on prenait un objet du personnage, le curseur se transformait en main et le restait même quand on relachait l'objet
dans une case de l'inventaire.

D'abord il faut qu'on cache le curseur système, comme on va dessiner le curseur nous-même.
Avec Qt on peut faire ça simplement en rajoutant une MouseArea dans le fichier "main.qml":

		MouseArea {
			anchors.fill: parent
			enabled: false
			cursorShape: Qt.BlankCursor
		}
				
Elle n'est pas activée, donc elle n'interceptera pas les clics, mais quand la souris sera à l'intérieur, le curseur
sera invisible.

Les zones curseur

Donc on doit définir des zones où le curseur va changer. Mais on a déjà des zones souris (celles qu'on utilise pour
les zones de "clic").
On va juste ajouter un paramètre à SMouseArea pour dire quel curseur on veut.

		enum ECursorTypes
		{
			eCursor_None = 0,
			eCursor_Arrow,
			eCursor_Hand
		};

		struct SMouseArea
		{
			EMouseAreaTypes type;
			CRect           rect;
			void*           param1;
			void*           param2;
			ECursorTypes    cursor;
		};

		[...]

		void        addArea(EMouseAreaTypes type, CRect rect, ECursorTypes cursor = eCursor_Arrow, void* param1 = NULL, void* param2 = NULL);
				
Ensuite, il faut qu'on modifie CMouse::clickedArea(). On ne testera plus les zones seulement quand les bouton de la
souris est enfoncé. On va d'abord boucler sur toutes les zones pour voir si la souris est à l'intérieur, ensuite
afficher le curseur, et finalement, tester si le bouton est enfoncé pour renvoyer le pointeur sur la zone.

		SMouseArea* CMouse::clickedArea(QImage* image)
		{
			SMouseArea*  area = NULL;

			for (int i = mAreaList.size() - 1; i >= 0; --i)
			{
				CRect*  rect = &mAreaList[i].rect;

				if (mPos.x >= rect->tl.x &&
					mPos.y >= rect->tl.y &&
					mPos.x <= rect->br.x &&
					mPos.y <= rect->br.y)
				{
					area = &mAreaList[i];
					break;
				}
			}

			if (area == NULL || area->cursor != eCursor_None)
			{
				std::string fileName;

				if (area == NULL)
					fileName = "gfx/interface/CursorArrow.png";
				else if (area->cursor == eCursor_Arrow)
					fileName = "gfx/interface/CursorArrow.png";
				else
					fileName = "gfx/interface/CursorHand.png";

				QImage  cursor = fileCache.getImage(fileName);
				graph2D.drawImage(image, mPos, cursor);
			}

			if (mButtonPressed == true)
			{
				return area;
			}
			return NULL;
		}
				
Vous pouvez voir que par défaut on affiche le curseur flèche.

Maintenant le problème c'est qu'il nous manque des zones. Par exemple, dans la fenêtre 3D principale, les seules
zones que l'on a pour l'instant sont les boutons sur les encadrements de portes.
Mais on veut changer le curseur sur toute la fenêtre 3D. Alors on va devoir ajouter des zones sans action derrière
les autres zones "utiles".
Vous pouvez voir ça au début de displayMainView():

		void CGame::displayMainView(QImage* image)
		{
			if (interface.isInInventory() == false)
			{
				CRect   mainRect(CVec2(0, 33), CVec2(MAINVIEW_WIDTH - 1, 33 + MAINVIEW_HEIGHT - 1));
				mouse.addArea(eMouseArea_None, mainRect, eCursor_Hand);
				
La même chose se passe dans le cas de la feuille de personnage et à quelques autres endroits.

Les flèches de déplacement


Maintenant, jetons un oeil aux flèches de déplacement à droite de l'écran.
Comme pour les autres éléments, elles ont besoin de zones souris pour qu'on puisse cliquer dessus. Mais comme ces
rectangles vont être utilisés pour autre chose, j'ai choisi de les stocker dans la classe CInterface et d'initialiser
leurs coordonnées dans son constructeur:

		CInterface::CInterface()
		{
			[...]

			arrowsRects[eArrowTurnLeft]  = CRect(CVec2(234, 125), CVec2(261, 145));
			arrowsRects[eArrowForward]   = CRect(CVec2(263, 125), CVec2(289, 145));
			arrowsRects[eArrowTurnRight] = CRect(CVec2(291, 125), CVec2(318, 145));

			arrowsRects[eArrowLeft]      = CRect(CVec2(234, 147), CVec2(261, 167));
			arrowsRects[eArrowBackward]  = CRect(CVec2(263, 147), CVec2(289, 167));
			arrowsRects[eArrowRight]     = CRect(CVec2(291, 147), CVec2(318, 167));
		}
				
Dans le jeu on ne se déplace pas seulement en cliquant sur les flèche, mais aussi en utilisant le clavier.
Comme on l'a dit avant, le clavier est géré de manière asynchrone pour le moment. Alors pour faciliter la gestion
de ces entrées, on a besoin de le synchroniser avec la souris.
J'ai modifié le fichier "keyboard.cpp" de façon a ce que chaque appui sur une touche soit stocké dans un tableau
de booléens.

		bool CKeyboard::eventFilter(QObject */*object*/, QEvent *event){

			if (event->type() == QEvent::KeyPress)
			{
				[...]

				if (keyEvent->isAutoRepeat() == false)
				{
					[...]

					switch (nativeKey)
					{
					case 16:    // 'Q' en QWERTY, 'A' en AZERTY
						tempKeys[eTurnLeft] = true;
						break;

					case 17:    // 'W' en QWERTY, 'Z' en AZERTY
						tempKeys[eForward] = true;

					[...]
				
Quand on aura besoin de tester les touches, on appellera une fonction de mise à jour qui fera une copie de ce
tableau, pour éviter qu'il soit modifié pendant qu'on traite les touches:

		void CKeyboard::update()
		{
			for (int i = 0; i < eKeyCount; ++i)
			{
				keys[i] = tempKeys[i];
				tempKeys[i] = false;
			}
		}
				
Ensuite on va tester à la fois les clics souris et le clavier dans une fonction updateArrows():

		//------------------------------------------------------------
		bool   CInterface::isClickingOnArrow(SMouseArea* clickedArea, EMouseAreaTypes type)
		{
			return (mouse.mButtonPressing && clickedArea != NULL && clickedArea->type == type);
		}

		//------------------------------------------------------------
		void   CInterface::updateArrows(SMouseArea* clickedArea)
		{
			if (isClickingOnArrow(clickedArea, eMouseArea_ArrowForward) == true || keyboard.keys[CKeyboard::eForward] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowForward;
				player.moveForward();
			}
			else if (isClickingOnArrow(clickedArea, eMouseArea_ArrowBackward) == true || keyboard.keys[CKeyboard::eBackward] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowBackward;
				player.moveBackward();
			}
			else if (isClickingOnArrow(clickedArea, eMouseArea_ArrowLeft) == true || keyboard.keys[CKeyboard::eLeft] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowLeft;
				player.moveLeft();
			}
			else if (isClickingOnArrow(clickedArea, eMouseArea_ArrowRight) == true || keyboard.keys[CKeyboard::eRight] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowRight;
				player.moveRight();
			}
			else if (isClickingOnArrow(clickedArea, eMouseArea_ArrowTurnLeft) == true || keyboard.keys[CKeyboard::eTurnLeft] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowTurnLeft;
				player.turnLeft();
			}
			else if (isClickingOnArrow(clickedArea, eMouseArea_ArrowTurnRight) == true || keyboard.keys[CKeyboard::eTurnRight] == true)
			{
				arrowHighlightTime = ARROWS_HIGHLIGHT_TIME;
				currentArrow = eArrowTurnRight;
				player.turnRight();
			}
		}
				
Rassembler la souris et le clavier au même endroit sera utile plus tard pour limiter le mouvement quand les
personnages seront en surpoids.

Surligner les flèches

Maintenant expliquons la signification des variables currentArrow et arrowHighlightTime.
Quand on va cliquer sur une flèche ou qu'on appuiera sur la touche corrspondante, elle sera surlignée pendant un
court instant:


Donc currentArrow contient le numéro de la flèche qu'on a cliqué, et arrowHighlightTime est un "compteur" qui
contient le nombre de frames pendant lequel cette flèche sera surligné.

Il n'y a pas de sprite spécial pour ce surlignage, les couleurs sont simplement inversées donc il faut qu'on écrive
une fonction "Xor" pour afficher cette inversion. Elle se base sur une propriété de la fonction booléenne "ou
exclusif" qui peut inverser les bits d'une valeur.
Une technique similaire était utilisées dans certains jeux sur Amstrad CPC et d'autres ordis 8 bits pour animer des
sprites sans avoir à sauver le fond avant.

		void    CGraphics2D::Xor(QImage* image, CRect rect, QColor color)
		{
			for (int y = rect.tl.y; y <= rect.br.y; ++y)
				for (int x = rect.tl.x; x <= rect.br.x; ++x)
				{
					QColor  newColor;
					QRgb pixel = image->pixel(QPoint(x, y));
					newColor.setRed(qRed(pixel) ^ color.red());
					newColor.setGreen(qGreen(pixel) ^ color.green());
					newColor.setBlue(qBlue(pixel) ^ color.blue());
					image->setPixel(QPoint(x, y), newColor.rgba());
				}
		}
				
Enfin, pour finir, jetons un oeil à la fonction drawArrows() où on peut voir l'utilisation de notre tableau de
coordonnées, et le surlignage de la flèche courante.

		void    CInterface::drawArrows(QImage* image)
		{
			QImage  arrowsBg = fileCache.getImage("gfx/interface/MovementArrows.png");
			CVec2   pos(233, 124);
			graph2D.drawImage(image, pos, arrowsBg);

			if (isArrowsGreyed() == true)
			{
				CRect   rect(CVec2(233, 124),
							 CVec2(319, 168));
				graph2D.patternRectangle(image, rect);
			}
			else
			{
				static const EMouseAreaTypes arrowsMouseAreas[] =
				{
					eMouseArea_ArrowForward,
					eMouseArea_ArrowBackward,
					eMouseArea_ArrowLeft,
					eMouseArea_ArrowRight,
					eMouseArea_ArrowTurnLeft,
					eMouseArea_ArrowTurnRight
				};

				for (int i = 0; i < eArrowsCount; ++i)
				{
					mouse.addArea(arrowsMouseAreas[i], arrowsRects[i]);
				}

				if (arrowHighlightTime > 0)
				{
					arrowHighlightTime--;
					graph2D.Xor(image, arrowsRects[currentArrow], MAIN_INTERFACE_COLOR);
				}
			}
		}