Part 18: Doors - Part 2: Collisions, opening and ornates

Downloads

Source code
Executable for the map editor - exactly the same as in part 17 (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.

Collisions

Last time we drew the doors but we could go through them even if they were closed. So now let's see how we can block the player.
As we chose a wall-based representation for our map, until now we only had to test the walls in CPlayer::moveIfPossible() to
restrict the movement of the player.
Following this logic, we could add a type of invisible wall to prevent the player from getting to the tile where the door is
when it's closed.
But then, every time we would add a door in the editor, we would have to set 1 tile and 4 walls. That's quite a lot of work.

Hopefully, there is a solution that won't require us to add something else to the editor an that won't be difficult to code in the game.
As we said, we only want to prevent the player from getting to the door's tile, and if you remember, we already know the tile where
the player wants to go in moveIfPossible().
So it's easy to modify this function to check if we are going to a closed door:

		void CPlayer::moveIfPossible(EWallSide side)
		{
			CVec2   newPos = pos;

			[...]

			CTile*  tile = map.getTile(pos);
			CTile*  destTile = map.getTile(newPos);

			bool canMove = true;

			if (tile->mWalls[side].getType() != 0)
			{
				canMove = false;
			}
			else if (destTile == NULL)
			{
				canMove = false;
			}
			else if (destTile->getType() == eTileDoor && doors.isOpened(destTile) == false)
			{
				canMove = false;
			}

			if (canMove == true)
				pos = newPos;
		}
				

The button

Last time we forgot a graphical element of the door: the button.
The doors in this game can be opened by a lot of means. But the simplest one is a button that appears on the frame of the door.


The button is a simple image that we will scale and darken according to the distance of the door.
In "doors.cpp" you can see that I wrote a table with the button's position and scale, as it doesn't appear for every position of the door:

		struct SButtonElem
		{
			int16_t x, y;
			float   scale;
		};

		SButtonElem   tabButton[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			// Row 3 (farthest)
			{
				{0, 0, 0.0},  // 03
				{0, 0, 0.0},  // 13
				{137, 74, 0.43},  // 23
				{197, 72, 0.43},  // 33
				{0, 0, 0.0}   // 43
			},

			// Row 2
			{
				{0, 0, 0.0},  // 02
				{0, 0, 0.0},  // 12
				{150, 75, 0.66}, // 22
				{0, 0, 0.0},  // 32
				{0, 0, 0.0}   // 42
			},

			// Row 1
			{
				{0, 0, 0.0},  // 01
				{0, 0, 0.0},  // 11
				{167, 76,  1.0}, // 21
				{0, 0, 0.0},  // 31
				{0, 0, 0.0}   // 41
			},

			// Row 0 (nearest)
			{
				{0, 0, 0.0},  // 00
				{0, 0, 0.0},  // 10
				{0, 0, 0.0},  // 20
				{0, 0, 0.0},  // 30
				{0, 0, 0.0}   // 40
			}
		};
				
Then in CDoors::draw() we do the drawing and we add a mouse area when the door is in front of us.
Note that in the mouse area we add a parameter to rememember the tile where this door is.

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

				//-------------------------------------------------------------------------------------
				// draw the button
				if (tile->getBoolParam("hasButton") == true)
				{
					SButtonElem& cell = tabButton[tablePos.y][tablePos.x];
					CVec2 pos(cell.x, cell.y);
					float scale = cell.scale;

					if (scale != 0.0f)
					{
						QImage  button = fileCache.getImage("gfx/3DView/doors/Button.png");
						static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
						float shadow = shadowLevels[WALL_TABLE_HEIGHT - 2 - tablePos.y];

						graph2D.darken(&button, shadow);
						graph2D.drawImageScaled(image, pos, button, scale);

						if (tablePos == CVec2(2, 2))
						{
							CRect   rect(pos,
										 CVec2(pos.x + button.width() - 1, pos.y + button.height() - 1));
							mouse.addArea(eMouseAreaG_DoorButton, rect, (void*)tile);
						}
					}
				}

				//-------------------------------------------------------------------------------------
				// draw the door with the ornate
				drawDoor(image, tablePos, tile);
			}
		}
				
Finally in CGame::update() where we check the mouse areas, we check if the button is clicked.
If that is the case, we get the door's tile and we flip its "isOpened" flag:

		void CGame::update(SMouseArea* clickedArea)
		{
			doors.update();

			if (clickedArea != NULL)
			{
				if (mouse.mButtonPressing == true)
				{
					if (clickedArea->type == eMouseAreaG_Champion)
					{
						[...]
					}
					else if (clickedArea->type == eMouseAreaG_DoorButton)
					{
						CTile*  tile = (CTile*)clickedArea->param1;
						bool isOpened = tile->getBoolParam("isOpened");
						tile->setBoolParam("isOpened", !isOpened);
					}
				}
			}
		}
				
For testing purpose I added a button to every door in the first level, as we have not seen the other ways to open them yet.

Opening the doors

Doors opens in 2 different fashions.
Either the whole door slides up inside the ceiling:


Or two halves of the door slide inside the side walls:


The animations in the original game were a little clumsy as it only displayed 4 or 5 stages during the whole animation.
It was probably because it needed precalculation of the images on some computers.
Now we will do far smoother animations. First we will need an update function that will be called at each frame.
This function will loop through all doors in the map and for each door it will update it's position and the "isMoving" flag:

		// Update doors animations
		void CDoors::update()
		{
			for (int y = 0; y < map.mSize.y; ++y)
				for (int x = 0; x < map.mSize.x; ++x)
				{
					CTile*  tile = map.getTile(CVec2(x, y));

					if (tile->getType() == eTileDoor)
					{
						bool isHorizontal = tile->getBoolParam("isHorizontal");
						int animLenght = (isHorizontal == false ? 88 : 48) * DOOR_POS_FACTOR;
						int pos = tile->getIntParam("pos");
						bool isOpened = tile->getBoolParam("isOpened");

						if (isOpened == false)
						{
							if (pos < 0)
							{
								tile->setBoolParam("isMoving", true);
								pos += animLenght / DOOR_ANIMATION_TIME;
								tile->setIntParam("pos", pos);
							}

							if (pos >= 0)
							{
								tile->setBoolParam("isMoving", false);
								tile->setIntParam("pos", 0);
							}
						}
						else
						{
							if (pos > -animLenght)
							{
								tile->setBoolParam("isMoving", true);
								pos -= animLenght / DOOR_ANIMATION_TIME;
								tile->setIntParam("pos", pos);
							}

							if (pos <= -animLenght)
							{
								tile->setBoolParam("isMoving", false);
								tile->setIntParam("pos", -animLenght);
							}
						}
					}
				}
		}
				
Now the constants need some explanations.
The animations last 1 second. So DOOR_ANIMATION_TIME holds 60 as this function is called 60 times per second.
But what DOOR_POS_FACTOR is used for ?

If you look at a vertical door it has to move by 88 pixels during 60 frames.
So it's speed is 88/60 = 1.466... pixels per second.
But as we use integer variables, this speed will be rounded to 1.
The door will then move at 1 pixel per frame, and the animation will last for 88 frames, not 60.

For an horizontal door it's even worse, as each half of the door as to move by 48 pixels.
It's speed would be 48/60 = 0.8 pixels per frame, which will be rounded to 0.
And the door won't move at all!

So to get more precision in the calculations, I multiply the animation length and the door position by DOOR_POS_FACTOR.
And later I divide the position by the same value at the moment I draw the door.
I found 50 is a good value to get smooth movements, but you can try to change it if you like.

The flag "isMoving" is used in conjunction with "isOpened" To avoid that the player go through the door while it is
opening or closing - see CDoors::isOpened().

The vertical doors

To draw vertical doors during animations, we simply add the "pos" parameter to the sprite position in CDoors::drawDoor():

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// get door image
				int doorType = tile->getDoorParam("Type");
				std::string fileName = std::string("gfx/3DView/doors/") + doorsDatas[doorType].files[doorNum].toUtf8().constData();
				QImage  doorImage = fileCache.getImage(fileName);

				[...]

				//----------------------------------------------------------------------------
				// draw
				bool isHorizontal = tile->getBoolParam("isHorizontal");
				int doorPos = tile->getIntParam("pos") / DOOR_POS_FACTOR;

				if (isHorizontal == false)
				{
					//----------------------------------------------------------------------------
					// vertical door
					static const int doorScalesY[] = {88, 61, 38};
					float scale = doorScalesY[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					doorPos = (doorPos * scale) / 88;

					CVec2 pos(cell.x, cell.y + doorPos);
					QRect clip(cell.cx1, cell.cy1,
							   cell.cx2 - cell.cx1 + 1, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip);
				}
				else
				{
					//----------------------------------------------------------------------------
					// horizontal door
					[...]
				}
			}
		}
				

The horizontal doors

The horizontal doors are a little more complex to draw.
First, we have to draw the 2 halves independently.
The clipping rects are used to display only one half of the sprite.
And they have to be modified according to the animation position.

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// get door image
				[...]

				//----------------------------------------------------------------------------
				// draw
				bool isHorizontal = tile->getBoolParam("isHorizontal");
				int doorPos = tile->getIntParam("pos") / DOOR_POS_FACTOR;

				if (isHorizontal == false)
				{
					//----------------------------------------------------------------------------
					// vertical door
					[...]
				}
				else
				{
					//----------------------------------------------------------------------------
					// horizontal door
					static const int doorScalesX[] = {96, 64, 44};
					float scale = doorScalesX[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					doorPos = (doorPos * scale) / 96;

					CVec2 pos(cell.x + doorPos, cell.y);
					int halfwidth = (cell.cx2 - cell.cx1 + 1) / 2;

					QRect clip1(cell.cx1, cell.cy1,
							   halfwidth + doorPos, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip1);

					pos.x = cell.x - doorPos;
					QRect clip2(cell.cx1 + halfwidth - doorPos, cell.cy1,
							   halfwidth + doorPos, cell.cy2 - cell.cy1 + 1);
					graph2D.drawImage(image, pos, doorImage, 0, false, clip2);
				}
			}
		}
				

The door ornates

As we are in the drawDoor() function, let's draw one last thing: the ornate of the door.
The only door that have an ornate in the fisrt level is the one we took to enter in the dungeon.


So let's fill the "door_ornates.xml" file with the name of the graphic and its position relatively to the door:

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<door_ornates>
			<door_ornate name="None">
			</door_ornate>

			<door_ornate name="Dungeon entrance">
				<image>Dungeon_Entrance.png</image>
				<pos x="0" y="0"/>
			</door_ornate>
		</door_ornates>
				
The loading of this database is the same thing as we already did many times...

Now for the drawing, it's nearly the same as for the walls ornates.
The graphic is scaled, darkened and positionned relatively to its respective door.
As the drawing of the door is already complex, I prefered to add the ornate directly to the door image before
drawing the whole thing to the screen.

		void    CDoors::drawDoor(QImage* image, CVec2 tablePos, CTile* tile)
		{
			SDoorElem&  cell = tabDoor[tablePos.y][tablePos.x];
			int doorNum = cell.file[0] - '0';

			if (doorNum >= 0)
			{
				//----------------------------------------------------------------------------
				// get door image
				[...]

				//----------------------------------------------------------------------------
				// get ornate image
				int ornateType = tile->getDoorOrnateParam("Ornate");
				std::string ornateName = doorOrnatesDatas[ornateType].file.toUtf8().constData();

				QImage  ornateImage;
				CVec2   ornatePos;
				if (ornateName.empty() == false)
				{
					// scale the ornate darken it, and draw it on the door graphics
					ornateImage = fileCache.getImage(std::string("gfx/3DView/door_ornates/") + ornateName);
					static const float shadowLevels[] = {0.0f, 0.2f, 0.4f};
					float shadow = shadowLevels[WALL_TABLE_HEIGHT - 2 - tablePos.y];
					graph2D.darken(&ornateImage, shadow);

					ornatePos = doorOrnatesDatas[ornateType].pos;
					float scale = doorImage.width() / 96.0;
					ornatePos *= scale;
					graph2D.drawImageScaled(&doorImage, ornatePos, ornateImage, scale);
				}

				//----------------------------------------------------------------------------
				// draw
				[...]
			}
		}
				
We will see later that these ornates hide even more surprises...