Part 35: Teleporters

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.

Parameters of the teleporters

Teleporters are a new type of tiles.
Here are the parameters they use:

		<tile name="Teleporter">
			<image>Teleporter.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;None">Side</param>
			<param type="bool">forChampions</param>
			<param type="bool">forObjects</param>
		</tile>
				
Like the stairs, teleporters can bring you to another level at a (X,Y) position.
The side parameter has 5 values because a teleporter can turn your party to a given direction, but some of them
don't change your initial direction.
Finally 2 booleans tells if the teleporter acts on your party or on objects.

Although they look like stairs, you will see that teleporters are a little bit more complicated.

Graphics

In the original graphics there is only a 32*32 image with random points to draw the teleporters.
Here I will leave the pink background for clarity in this web page.


But this is what they should look like in the game:



The sides of the teleporter have the same shape as the walls they are replacing.
We will have to take several stage to draw that.
First, we create a temporary image that is the same size of the wall that should be replaced by a teleporter,
and we fill it with our teleporter "pattern" by repeating it as many times as needed.


Then we only have to draw our teleporter image where there are opaque pixels in the wall image.
The original game used black and white masks to do that, but the Qt graphical functions allow us to use the wall
image itself as a mask.


And this image will be darken with the distance too.
So this is what a part of the code will look like:

		QImage  wallGfx = fileCache.getImage(fileName);
		CVec2   wallPos(tileInfo->x, tileInfo->y);
		QImage  teleportGfx = fileCache.getImage("gfx/3DView/Teleporter.png");

		// create the teleporter image
		QImage  tempImg(wallGfx.size(), QImage::Format_ARGB32);
		tempImg.fill(TRANSPARENT);

		int subImgX = (wallGfx.size().width() + teleportGfx.size().width() - 1) / teleportGfx.size().width();
		int subImgY = (wallGfx.size().height() + teleportGfx.size().height() - 1) / teleportGfx.size().height();
		[...]

		for (int y = 0; y < subImgY; ++y)
			for (int x = 0; x < subImgX; ++x)
			{
				CVec2   subImgPos(x * teleportGfx.size().width(),
				                  y * teleportGfx.size().height());
				[...]
				graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
			}

		// darken
		int shadow = WALL_TABLE_HEIGHT - 2 - tablePos.y;
		if (shadow < 0)
			shadow = 0;
		graph2D.darken(&tempImg, (float)shadow * 0.2f);

		// draw the image
		graph2D.drawImageWithMask(image, wallPos, tempImg, wallGfx);
				

Animation

In the game, teleporters are animated. The dots seems to move randomly every 20 frames.
To draw this, when we create the teleporter image from the 32*32 "tiles", we will rotate and flip them randomly.
But as we want to draw the same image during 20 frames, we will need a random number generator with a seed that we
can control. So that it produces the same series of numbers when we set the same starting seed.
I used a simple congruential generator.

To make sure the seed only changes every 20 frames, I use a frame counter that is incremented every frame and
divide it by 20.
I also could have used a timer, but we don't really mind if the animation stops when the game is paused or if it
goes faster when we sleep as we don't see it anyways. And this does not affect the gameplay
So here is our image creation now:

		int random = game.frameCounter / 20;
		[...]

			// create the teleporter image
			[...]
			random = (random * 31415821 + 1) % 100000000;

			for (int y = 0; y < subImgY; ++y)
				for (int x = 0; x < subImgX; ++x)
				{
					[...]
					bool flip = ((random & 1) != 0);
					int angle = ((random >> 1) % 4) * 90;
					graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
				}
				

The whole tile

Like for the stairs, we will draw the tiles that contain a teleporter in "tiles.cpp".
But as we draw the whole tile, we'll see the outer sides of the teleporter walls - unless we are inside it.
So we will need a table that is the opposite of the one we used for the walls:

		struct STelepTile
		{
			char        file[16];
			uint16_t    x, y;
		};

		static const STelepTile   gTelepTiles[WALL_TABLE_HEIGHT][WALL_TABLE_WIDTH][3] =
		{
			// Row 3 (farthest)
			{
				{{"Wall02_F.png",   0, 58}, {"", 0, 0},                {"Wall13_L.png",  8, 58}}, // 02
				{{"Wall12_F.png",   7, 58}, {"", 0, 0},                {"Wall23_L.png", 78, 59}}, // 12
				{{"Wall22_F.png",  77, 58}, {"", 0, 0},                {"", 0, 0}              }, // 22
				{{"Wall32_F.png", 146, 58}, {"Wall23_R.png", 134, 59}, {"", 0, 0}              }, // 32
				{{"Wall42_F.png", 216, 58}, {"Wall33_R.png", 180, 58}, {"", 0, 0}              }  // 42
			},

			// Row 2
			{
				{{"", 0, 0},                {"", 0, 0},                {"Wall12_L.png",  0, 57}}, // 01
				{{"Wall11_F.png",   0, 52}, {"", 0, 0},                {"Wall22_L.png", 60, 52}}, // 11
				{{"Wall21_F.png",  59, 52}, {"", 0, 0},                {"", 0, 0}              }, // 21
				{{"Wall31_F.png", 164, 52}, {"Wall22_R.png", 146, 52}, {"", 0, 0}              }, // 31
				{{"", 0, 0},                {"Wall32_R.png", 216, 57}, {"", 0, 0}              }  // 41
			},

			// Row 1
			{
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}              }, // 00
				{{"Wall10_F.png",   0, 42}, {"", 0, 0},                {"Wall21_L.png", 33, 42}}, // 10
				{{"Wall20_F.png",  32, 42}, {"", 0, 0},                {"", 0, 0}              }, // 20
				{{"Wall30_F.png", 191, 42}, {"Wall21_R.png", 164, 42}, {"", 0, 0}              }, // 30
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}              }  // 40
			},

			// Row 0 (nearest)
			{
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}             }, // 00
				{{"", 0, 0},                {"", 0, 0},                {"Wall20_L.png", 0, 33}}, // 10
				{{"Wall20_F.png",  32, 42}, {"Wall20_R.png", 191, 33}, {"Wall20_L.png", 0, 33}}, // 20
				{{"", 0, 0},                {"Wall20_R.png", 191, 33}, {"", 0, 0}             }, // 30
				{{"", 0, 0},                {"", 0, 0},                {"", 0, 0}             }  // 40
			}
		};
				
Note that this table handles the case when we are inside the teleporter - line 20.
In this case we draw the 3 walls that surround us.
And finally the complete function to draw our teleporter is:

		void CTiles::drawTeleporter(QImage* image, CVec2 tablePos)
		{
			for (int i = 0; i < 3; ++i)
			{
				int random = game.frameCounter / 20;
				const STelepTile* tileInfo = &gTelepTiles[tablePos.y][tablePos.x][i];

				if (tileInfo->file[0] != 0)
				{
					static char fileName[256];
					sprintf(fileName, "gfx/3DView/walls/%s", tileInfo->file);
					QImage  wallGfx = fileCache.getImage(fileName);
					CVec2   wallPos(tileInfo->x, tileInfo->y);
					QImage  teleportGfx = fileCache.getImage("gfx/3DView/Teleporter.png");

					// create the teleporter image
					QImage  tempImg(wallGfx.size(), QImage::Format_ARGB32);
					tempImg.fill(TRANSPARENT);

					int subImgX = (wallGfx.size().width() + teleportGfx.size().width() - 1) / teleportGfx.size().width();
					int subImgY = (wallGfx.size().height() + teleportGfx.size().height() - 1) / teleportGfx.size().height();
					random = (random * 31415821 + 1) % 100000000;

					for (int y = 0; y < subImgY; ++y)
						for (int x = 0; x < subImgX; ++x)
						{
							CVec2   subImgPos(x * teleportGfx.size().width(),
							                  y * teleportGfx.size().height());
							bool flip = ((random & 1) != 0);
							int angle = ((random >> 1) % 4) * 90;
							graph2D.drawImage(&tempImg, subImgPos, teleportGfx, angle, flip);
						}

					// darken
					int shadow = WALL_TABLE_HEIGHT - 2 - tablePos.y;
					if (shadow < 0)
						shadow = 0;
					graph2D.darken(&tempImg, (float)shadow * 0.2f);

					// draw the image
					graph2D.drawImageWithMask(image, wallPos, tempImg, wallGfx);
				}
			}
		}
				

Stepping inside it

When we walk on a teleporter tile and it's a teleporter the acts on the champions, it's pretty much like the
stairs.
We set a boolean named shouldTeleport that will be handled in CGame::mainLoop().
When this boolean is true, we call teleport():

		void CPlayer::teleport()
		{
			CTile*  tile = map.getTile(pos);
			int     level = tile->getIntParam("Level");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));
			int     side = tile->getEnumParam("Side");

			if (side == eWallSideMax)
				side = dir;

			if (level != game.currentLevel)
			{
				game.loadLevel(level, newPos, side);
			}
			else
			{
				player.pos = newPos;
				player.dir = side;
			}
			sound->play(player.pos, "sound/Buzz.wav");
			shouldTeleport = false;
		}
				
Here it's pretty much the same as for the stairs. Except that we don't alway go to another level, and we don't
always turn to a new direction either.

Dropping objects in it

When we drop an object on the ground, we will test if we are on the tile of a teleporter that acts on objects.

            else if (clickedArea->type == eMouseAreaG_DropObject)
            {
                CVec2   pos = CVec2((int)clickedArea->param1, (int)clickedArea->param2);
                int side = (int)clickedArea->param3;

			    [... drop the object]

                if (side == eWallSideMax)
                {
                    CVec2   mapPos = pos / 2;
                    CTile*  tile = map.getTile(mapPos);

                    if (tile->getType() == eTileTeleporter && tile->getBoolParam("forObjects") == true)
                        teleportStacks(mapPos);
                }
            }
				
The teleportStacks() function will teleport all the object stack on the 4 possible positions of the given tile.
So first, we will make a backup copy of these stacks and remove them from the source position.

		void CGame::teleportStacks(CVec2 mapPos)
		{
			CTile*  tile = map.getTile(mapPos);
			std::vector<CObjectStack>   copyStacks;

			// copies the stacks
			for (int y = 0; y < 2; ++y)
				for (int x = 0; x < 2; ++x)
				{
					CVec2 objPos = mapPos * 2 + CVec2(x, y);
					CObjectStack*    stack = map.findObjectsStack(objPos, eWallSideMax);

					if (stack != NULL)
					{
						CObjectStack    copy = *stack;
						copy.mPos = CVec2(x, y);
						copyStacks.push_back(copy);
						map.removeObjectsStack(objPos, eWallSideMax);
					}
				}
				
Then if the teleporter leads to another level we load it - after backuping some datas.

			int     lastLevel = currentLevel;
			CVec2   lastPos = player.pos;
			uint8_t lastDir = player.dir;

			int     level = tile->getIntParam("Level");
			CVec2   newPos = CVec2(tile->getIntParam("X"), tile->getIntParam("Y"));

			// load destination level
			if (level != currentLevel)
				loadLevel(level, CVec2(), 0);
				
After that we put back all of the objects in the copied stack to the destination position.

			// set the stacks at the new pos
			for (size_t i = 0; i < copyStacks.size(); ++i)
			{
				CVec2 objPos = newPos * 2 + copyStacks[i].mPos;
				CObjectStack*   stack = map.addObjectsStack(objPos, eWallSideMax);

				for (size_t j = 0; j < copyStacks[i].getSize(); ++j)
					stack->addObject(copyStacks[i].getObject(j));
			}
				
And finally, if we changed level, we go back to the starting one.

			// reload the last level
			if (level != lastLevel)
				loadLevel(lastLevel, lastPos, lastDir);
		}
				

Bugs fixed in the editor

I fixed an annoying bug in the editor.
The drawTile and drawWall tools only backed up the type of the element they were overwriting.
So, if by mistake you overwrote, say a door tile with all it's parameters set, if you pressed Ctrl-Z, the door tile
went back but all its parameters were reseted.
Now I backup the whole Tile/Wall with all its parameters.

I also changed a little bit the switches and objects tiles graphics so that maps are more readable.