Part 20: Game interface - Part 3: Cursors and arrows

Downloads

Source code
Executable for the map editor - exactly the same as in part 19 (Windows 32bits)
Executable for the game (Windows 32bits)

Before you try to compile the code go to the "Projects" tab on the left menu, select the "Run" settings for your kit,
and set the "Working directory" to the path of "editor\data" for the editor or "game\data" for the game.

The cursors

There are 2 types of cursors in the game: an arrow and a hand.


In fact there is also a 3rd type, as when you hold an object it replaces the cursor, but we will see that later.

Roughly, the hand appears when the mouse is in the main game window - the "3D" view - and in the character's sheet.
The arrow appears when the mouse is over the right interface part - the spells, the weapons and the movement arrows.

For the champions boxes at the top of the screen, it is a little bit more complex:

The arrow is also displayed on areas that we don't have yet: i.e. in the "save game" screen.

In the original game, there was also a strange behavior, that could be considered like a bug:
When we entered the the character's sheet to resurrect a champion, the cursor was an arrow. But if we took an object of the
character, the cursor turned to a hand, and stayed a hand even when we dropped the object in a frame of the inventory.

First we need to hide the system cursor, as we will draw the cursor ourselves.
With Qt we can do that simply by adding a mouse area to "main.qml" file:

		MouseArea {
			anchors.fill: parent
			enabled: false
			cursorShape: Qt.BlankCursor
		}
				
It is not enabled, so it won't intercept the mouse clicks, but when the mouse is inside it, the cursor is blank.

Cursors areas

So we need to define areas where the cursor will change. But we already have areas for the mouse - the ones we use to define
the "clicks".
We will just add a parameter to SMouseArea to tell which cursor we want.

		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);
				
Then we have to modify CMouse::clickedArea(). We won't check the areas only when we the mouse is clicked. We will first
loop through all the areas to see if the mouse is inside it, then display the cursor, and finally, test if we are pressing the
button to return the pointer to the area.

		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;
		}
				
You can see that by default we display the arrow cursor.

Now the problem is that we are missing some areas. For example, in the main 3D window, the only areas we have for now
are the buttons on the door frames.
But we want to change the cursor on the whole 3D view. So we will need to add some areas with no action behind the other
"useful" areas.
You can see that at the beginning of 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);
				
The same thing appears in the case of the character's sheet, and in a few other places.

The movement arrows


Then let's have a look at the movement arrows on the right of the screen.
As for the other elements, they need mouse areas so that we can click on them. But because these rectangles will be used for
another purpose, I chose to store them in the CInterface structure and initialize their coordinate in it's constructor:

		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));
		}
				
In the game, we move not only by clicking on these arrows but by presing keys too.
As we told before, the keyboard is handled asynchronously at the moment. So to ease the handling of these inputs, we need to sync it
with the mouse.
I modified the "keyboard.cpp" so that every key press is stored in a boolean array.

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

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

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

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

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

					[...]
				
When we will need to check the key, we will call an update function that makes a copy of this array, to avoid that it is modified while we
process the keys:

		void CKeyboard::update()
		{
			for (int i = 0; i < eKeyCount; ++i)
			{
				keys[i] = tempKeys[i];
				tempKeys[i] = false;
			}
		}
				
Then we'll check both the mouse clicks and the keyboard in an updateArrows() function:

		//------------------------------------------------------------
		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();
			}
		}
				
Gathering both mouse and keyboard at the same place will be useful later to limit the movement when characters will be overweighting.

Highlighting arrows

Now let's explain the meaning of the currentArrow and arrowHighlightTime variables.
When we click on an arrow or press the correspnding key, it will be highlighted for a short amount of time:


So currentArrow holds the arrow we clicked, and arrowHighlightTime is a "counter" that holds the number of frames this
arrow will be highlighted.

There is no special sprite for this highlighting, the colors are simply inverted so I needed to write an "Xor" function
to display this inversion. It is based on a property of the "exclusive or" boolean function that can invert bits of a value.
A similar technique was used on some games on Amstrad CPC and other 8 bits computer to animate sprites without having to
save the background before.

		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());
				}
		}
				
Finally, to sum it up, let's look at the drawArrows() function, where we can see the use of our coordinates array,
and the highlighting of the current arrow.

		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);
				}
			}
		}