Part 29: Stairs and map snapshot

Downloads

Source code
Executable for the map editor (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.

Stairs in the editor

Stairs are a little bit like doors. They both sit in the middle of a tile and their orientations are important.
So, as for the doors, we will use both a tile and a wall to define them.

The tile will hold the datas: the destination level, the position where we will appear in the new level, and the
direction we will head for.

		<tile name="Stairs">
			<image>Stairs.png</image>
			<param type="int">Level</param>
			<param type="int">X</param>
			<param type="int">Y</param>
			<param type="enum" values="Up;Left;Down;Right">Side</param>
		</tile>
				
And the wall will give the orientation of the graphics. I chose to use the "back" wall of the stairs - the one that
is highlighted in red in this image:


And that's all we have to do in the editor.
I also created a "level 2" map which is just a big room to see the stairs graphics from every position.

Stairs in the game

You will find the stairs graphics in "data/gfx/3DView/stairs".
There are 6 images for the stairs going down that are facing us.
I named them following the indexes in the walls' table.


You can see that 3 of the images are for stairs that are before us, and the others are when the stairs are one tile on
the left.
Of course, the graphs are flipped for the right side.

There are the same number of images for stairs going up and facing us.


When the stairs are perpendicular to us, we can only see a small part of them:


And finally, when we are on the same tile as the stairs, we only see a small part of the handrails.


The code to display the stairs is in "tiles.cpp". It's not too complex. As for other graphic elements we use
coordinates tables that follow the walls' table. Note that I added a variable CGame::currentLevel to keep the currently loaded level. We use this variable to
tell if the stairs are going up or down.

		struct SStairsData
		{
			char        file[32];
			uint16_t    x, y;
		};

		// stairs going down, facing table
		static const SStairsData   gDownFacingStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			{{"", 0, 0}, {"Down_13_F.png", 8, 63}, {"Down_23_F.png", 75, 58}, {"Down_13_F.png", 141, 63}, {"", 0, 0}}, // Row 3
			{{"", 0, 0}, {"Down_12_F.png", 0, 57}, {"Down_22_F.png", 63, 57}, {"Down_12_F.png", 163, 57}, {"", 0, 0}}, // Row 2
			{{"", 0, 0}, {"Down_11_F.png", 0, 51}, {"Down_21_F.png", 35, 50}, {"Down_11_F.png", 192, 51}, {"", 0, 0}}, // Row 1
			{{"", 0, 0}, {"", 0, 0},               {"", 0, 0},                {"", 0, 0},                 {"", 0, 0}}  // Row 0
		};

		// stairs going up, facing table
		static const SStairsData   gUpFacingStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			[...]
		};

		// stairs going down, side table
		static const SStairsData   gDownSideStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			{{"", 0, 0}, {"", 0, 0},                      {"", 0, 0}, {"", 0, 0},                       {"", 0, 0}}, // Row 3
			{{"", 0, 0}, {"Side_12_F.png",      60,  89}, {"", 0, 0}, {"Side_12_F.png",      156,  89}, {"", 0, 0}}, // Row 2
			{{"", 0, 0}, {"Down_Side_11_F.png", 32,  95}, {"", 0, 0}, {"Down_Side_11_F.png", 172,  95}, {"", 0, 0}}, // Row 1
			{{"", 0, 0}, {"Side_10_F.png",       0, 106}, {"", 0, 0}, {"Side_10_F.png",      208, 106}, {"", 0, 0}}  // Row 0
		};

		// stairs going up, side table
		static const SStairsData   gUpSideStairs[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH] =
		{
			[...]
		};

		void CTiles::drawStairs(QImage* image, CTile* tile, CVec2 tablePos)
		{
			const SStairsData*  data;

			// find the side of the stairs
			EWallSide   side;
			int i;

			for (i = 0; i < 4; ++i)
				if (tile->mWalls[i].getType() == eWallStairs)
					break;
			side = (EWallSide)i;
			bool isDown = (tile->getIntParam("Level") > game.currentLevel);

			if (side != player.getWallSide(eWallSideDown))
			{
				// get the table data
				if (side == player.getWallSide(eWallSideUp))
				{
					// facing stairs
					if (isDown)
						data = &gDownFacingStairs[tablePos.y][tablePos.x];
					else
						data = &gUpFacingStairs[tablePos.y][tablePos.x];
				}
				else
				{
					// side stairs
					if (isDown)
						data = &gDownSideStairs[tablePos.y][tablePos.x];
					else
						data = &gUpSideStairs[tablePos.y][tablePos.x];
				}

				// display
				if (data->file[0] != 0)
				{
					std::string fileName = std::string("gfx/3DView/stairs/") + std::string(data->file);
					CVec2   pos = CVec2(data->x, data->y);
					bool    flip = (tablePos.x > 2);
					QImage  stairsImage = fileCache.getImage(fileName);
					graph2D.drawImage(image, pos, stairsImage, 0, flip);
				}
			}
			else if (tablePos == CVec2(2, 3))
			{
				// special case when we are on the stairs' tile
				if (isDown)
				{
					QImage  stairsImage = fileCache.getImage("gfx/3DView/stairs/Down_20_L.png");
					graph2D.drawImage(image, CVec2(0, 109), stairsImage);
					graph2D.drawImage(image, CVec2(194, 109), stairsImage, 0, true);
				}
				else
				{
					QImage  stairsImage = fileCache.getImage("gfx/3DView/stairs/Up_20_L.png");
					graph2D.drawImage(image, CVec2(0, 91), stairsImage);
					graph2D.drawImage(image, CVec2(194, 91), stairsImage, 0, true);
				}
			}
		}
				

Taking the stairs

There are several ways to "take" the stairs and go to the next level:
The latter case is to avoid showing the player that the sides of the stairs are not drawn when you are on their tile.

The function to change the level and position according to the datas of the current tile is pretty straightforward.

		void CPlayer::takeStairs()
		{
			CTile*  tile = map.getTile(pos);
			int     level = tile->getIntParam("Level");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));
			int     side = tile->getEnumParam("Side");
			game.loadLevel(level, newPos, side);
		}
				
The CGame::loadLevel() function obviously loads the new level and set the position of the player in the new map.

But calling this function directly in the player's movements we enumerated would be dangerous.
Loading a new level in the middle of a game frame while some processing of the old level are not
finished could lead to weird bugs.
Instead, we will set a flag to true when we make these movements:

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnLeft()
		{
			shouldTakeStairs = false;
			[...]
			CTile*  tile = map.getTile(pos);

			if (tile->getType() == eTileStairs)
				shouldTakeStairs = true;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnRight()
		{
			shouldTakeStairs = false;
			[...]
			CTile*  tile = map.getTile(pos);

			if (tile->getType() == eTileStairs)
				shouldTakeStairs = true;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveLeft()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveRight()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveForward()
		{
			shouldTakeStairs = false;
			[...]
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveBackward()
		{
			shouldTakeStairs = false;

			if (interface.isInInventory() == false)
			{
				CTile*  tile = map.getTile(pos);

				if (tile->getType() == eTileStairs)
				{
					shouldTakeStairs = true;
				}
				else
				{
					[...]
				}
			}
		}

		//-----------------------------------------------------------------------------------------
		void CPlayer::moveIfPossible(EWallSide side)
		{
			[...]

			if (canMove == true)
			{
				[...]

				if (destTile->getType() == eTileStairs)
					shouldTakeStairs = true;
			}
		}
				
And at the end of the game loop, we test this flag to call our function:

		QImage& CGame::mainLoop()
		{
			[...]

			if (player.shouldTakeStairs)
				player.takeStairs();

			return image;
		}
				
And we don't forget to set this flag back to false in takeStairs().

Map snapshot

Now if we run the game like this, once we get downstairs to the level 2, then get back up to level 1, we will
face a problem.
All the objects that we picked up reappears on the ground, and the first door of the level is closed, so we
can't get back to the champions' area and to the starting point of the game.

We need to take a snapshot of the map's state before we leave it, and restore it when we re-enter this level.
So we will store the datas of all the maps we visited in a list in CGame:

		class CSnapshot
		{
		public:
			CSnapshot();
			CSnapshot(const CSnapshot& rhs);
			virtual ~CSnapshot();

			CSnapshot&  operator=(const CSnapshot& rhs);
			void    copyPressPlates(bool* src);

			int     level;
			CMap    map;
			bool*   pressPlateStates;

		private:
			void    freePressPlates();
		};

		class CGame
		{
			[...]
			std::vector<CSnapshot>   mVisitedLevels;
		};
				
The datas we will store for each map are:
I added some utility functions to handle the pressPlateStates too.

Now, before going further, I wanted to talk about an bug I already had in a previous part and forgot to tell you.
When you use STL containers like std::vector they don't always work as you would think.
Especially when you add an object to them using the push_back() function, it does not simply store your object in
the container.
Instead it makes a kind of temporary copy then, destroys it, calling the destrucor of your class.
But the problem is that is does not call the standard constructor to create this copy, so your destructor could
crash because some variables are not initialized properly.
In fact, it calls a copy constructor to create its object, i.e:

		CMap::CMap(const CMap& rhs)
		{
			mTiles = NULL;
			*this = rhs;
		}
				
So I had to write copy constructors for a couple of classes, and by the way I also wrote proper "=" operators for
these classes because it was easier to define the copy constructors this way.

Another remark: when we copy the map, we don't have to copy all the datas. So I chose to change to "static" the
tables that contains the parameters types and names - mTilesParams, mWallsParams and mObjectsParams - that are
common to all the maps.

Now, to actually take the snapshot of the level, I wrote a snapshotLevel() function:

		void CGame::snapshotLevel()
		{
			if (currentLevel != 0)
			{
				// if there is an entry for this level, delete it
				std::vector<CSnapshot>::iterator it;

				for (it = mVisitedLevels.begin(); it != mVisitedLevels.end(); ++it)
					if (it->level == currentLevel)
					{
						mVisitedLevels.erase(it);
						break;
					}

				// add a new entry
				CSnapshot   s;
				s.level = currentLevel;
				s.map = map;
				s.copyPressPlates(tiles.getPressPlates());
				mVisitedLevels.push_back(s);
			}
		}
				
This function is called every time we load a new level in the loadLevel() function:

		void CGame::loadLevel(int level, CVec2 pos, int side)
		{
			// snapshot the current level
			snapshotLevel();
			currentLevel = level;

			// search if the new level was snapshoted...
			for (size_t i = 0; i < mVisitedLevels.size(); ++i)
			{
				CSnapshot*  s = &mVisitedLevels[i];

				if (s->level == level)
				{
					map.freeMemory();
					map = s->map;
					tiles.setPressPlates(s->pressPlateStates);
					player.pos = pos;
					player.dir = side;
					return;
				}
			}

			// ...else load the new level
			static char fileName[256];
			sprintf(fileName, "maps/level%02d.map", level);
			map.load(fileName);
			player.pos = pos;
			player.dir = side;
			tiles.initMap();
			walls.init();
		}
				
And that's it. Now, every time we take the stairs to get back to a level we previously visited,
we will find it in the same state as it was when we leave it.