Part 19: Pressure plates and scripts

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.

Interacting with the map

An important part of the game is the ability to act on the map. Last time we saw the simplest way to do that: opening a door
by pressing a button on it.
But the game also uses pressure plates on the ground, switches, levers, keyholes, and other devices to act on an element that can
sometime be distant.
So we need to find a way to create a "link" between these elements.

The original game used "actuators" - a kind of invisible switch that acted on a particular element.
The problem is that there was a lot of different actuators, depending on how they were activated - by the player, a monster, or an object -
and on which element they acted - a tile, a wall, a door, etc. And these actuators could have quite a lot of parameters to modify the way
they acted on their target element.

But we will try to set up simpler technique.
In the first level there is a pressure plate that opens a door only if we added at least one champion to our party.
So let's define precisely what we will need to do.


The pressure plate will act on it's target only in 2 cases: either when it is pressed - i.e. when our party walk on it - or when it is released.
The other kinds of "devices" that we will meet later will also react in a few cases.
For a switch or a lever it will be when it is turned on, or when it is turned off.
For a keyhole it will react when we put the right key on it.
Perhabs we will see later some elements that reacts when a timer is elapsed. But it is not the case right now, so let's keep it simple.

In the case of out pressure plate, there could be different ways to press it.
It can be when our party walk on it, as the one in the first level.
Some other plates can be activated when a monster walks on them.
And some can react when we put an object on them.
Or it could be a combination of several of these methods.
To define all these cases we will simply add boolean parameters to the tile where the plate is in the editor.

Now what are the targets that can be affected by these switches and plates?
At the moment in the game, we can either act on a wall, on a tile, or on a door - which is only a special type of tile.
In the future we will probably have to act on some other elements, but in most of the cases it will fall back to one of those elements.
For example, a pit will be a special type of tile. A teleporter too, as it lies on a particular tile...

Then what will we modify on those target elements?
We will either change the type of the element. I.e. we can make a wall disappear.
Or we can change the value of one of its parameters. Which will be simple as we can retrieve them by name.
In fact, in some case we will have to change several parameters, and perhabs act on several elements with the same action.

So, as there could be many possibilities of actions, and that we want to be able to modify them easily, the simplest way to define them
is to write simple script files.
We will see what form they will take later. For now, let's begin to set up our pressure plate in the editor.

The pressure plate

The pressure plate in the editor will look like that:


The type defines the graphics we want. It can be either "None" - for an invisible plate - "Square", "Round" or "Tiny", as the graphics we saw
in the floor ornates.
The following checkboxes define if the pressure plate can be activated by our champions, the monsters or an object.
playSound will be used later, when we add sounds to the game. Because in some cases we could need to have plates that make no sounds.
Then, we define the scripts that will be called when we press or release the plate. It's simply the name of a text file.
The buttons next to these lines opens a file dialog to chose the script we want.

So here is what it looks like in "tiles.cpp":

		<tile name="Pressure Plate">
			<image>PressurePlate.png</image>
			<param type="enum" values="None;Square;Round;Tiny">Type</param>
			<param type="bool">byChampions</param>
			<param type="bool">byMonsters</param>
			<param type="bool">byObjects</param>
			<param type="bool">playSound</param>
			<param type="script">onPressed</param>
			<param type="script">onReleased</param>
		</tile>
				
You can see that I added two new types of parameters.
"script" is only a string that holds the name of the text file.

"enum" is a custom combo box. The different types of pressure plates are only a subset of the floor ornates.
As we already have the graphics in "floor_ornates.xml", we don't need to create a new database.
By the way, the editor already reads quite a lot of databases, and the more databases we add, the less flexibility it will have to be used for
other games.
So we just need a custom type to convert a name to an integer.

The values this enum can take are stored in mTilesParams - the list that hold the names and types of each parameters for each type of tile.

		struct CParamType
		{
			EParamType  mType;
			QString     mName;
			QStringList mValues;
		};

		[...]

		std::vector<std::vector<CParamType>>  mTilesParams; // list of the params for each type of tile
				
In the game, the pressure plate is simply drawn in CGame::displayMainView() at the same place we draw the other floor ornates:

		if (tile->getType() == eTileGround)
		{
			//------------------------------------------------------------------------------
			// ground tile
			int ornate = tile->getOrnateParam("Ornate");
			tiles.drawFloorOrnate(image, tablePos, ornate);
		}
		else if (tile->getType() == eTilePressPlate)
		{
			//------------------------------------------------------------------------------
			// pressure plate
			int ornate = tile->getEnumParam("Type");

			if (ornate != 0)
				tiles.drawFloorOrnate(image, tablePos, ornate + 6);
		}
				
The graphics are in the same order in the floor ornates as the order we chose for our combo box, so we only have to add "6" to get to the
square plate graphics.

The script

Finally, let's look at the script. I'll write only the code needed to open the door with the pressure plate in the first level.
So here is the script:

		Target Tile 5 9
		SetBool isOpened true
				
The first line defines the target: the tile at coordinates (5, 9). For a wall we would only have to add one more parameter with its side.
The second line changes a bool parameter on the target we defined - in our case, the "isOpened" parameter is set to "true".
As we saw in the last part, setting this parameter is the only thing needed to open the door, as the animation is based only on this value.

In the future we will add other functions to change the values of the different types of parameters: SetInt, SetString, and so on... With this method you can see that it is easy to change several parameters of the same target, or to act on multiples targets, as the other
functions use only the last target defined.

Now let's have a look at how this script is executed in the code. I added a "scripts.cpp" file where we can find the main execute() function:

		void CScripts::execute(std::string fileName)
		{
			if (fileName.empty() == false)
			{
				static char cFileName[256];
				static char line[256];

				sprintf(cFileName, "scripts/%s", fileName.c_str());
				FILE*   f = fopen(cFileName, "rt");

				while (fgets(line, 256, f) != NULL)
				{
					// split the line into words
					static std::vector<std::string>    words;
					words.clear();
					char* ptr;
					ptr = strtok(line," \t\r\n");

					while (ptr != NULL)
					{
						words.push_back(ptr);
						ptr = strtok(NULL, " \t\r\n");
					}

					// execute the line
					executeLine(words);
				}
				fclose (f);
			}
		}
				
This function opens the file, reads each line, split them into a list words, and calls the executeLine() function to execute the given line.
Now let's look at this function:

		void CScripts::executeLine(std::vector<std::string>& words)
		{
			if (words[0] == "Target")
				executeTarget(words);
			else if (words[0] == "SetBool")
				executeSetBool(words);
		}
				
It's pretty simple. It only calls the executeTarget() or executeSetBool() functions depending on the instruction of this line.
So let's go on with the executeTarget() function:

		void CScripts::executeTarget(std::vector<std::string>& words)
		{
			int x = std::stoi(words[2]);
			int y = std::stoi(words[3]);
			CTile*  tile = map.getTile(CVec2(x, y));

			if (words[1] == "Tile")
			{
				mTargetType = eTargetType_Tile;
				mTarget = (void*)tile;
			}
			else
			{
				mTargetType = eTargetType_Wall;
				// mTarget = ...
			}
		}
				
It stores a pointer to our target, that can be either a tile or a wall.
The code for a wall is not complete, as we only need a tile for now.
And finally let's see the SetBool function:

		void CScripts::executeSetBool(std::vector<std::string>& words)
		{
			QString param = QString::fromStdString(words[1]);
			bool    value = (words[2] == "true" ? true : false);

			if (mTargetType == eTargetType_Tile)
				((CTile*)mTarget)->setBoolParam(param, value);
			else
				((CWall*)mTarget)->setBoolParam(param, value);
		}
				
Here we call the setBoolParam() function either for a tile or for a wall, with the given parameters.

Calling the script

Finally let's talk about one last important point: when do we call this script.
In the case of a pressure plate, as it can be pressed by various means - either by our party, an enemy, or an object -
it is simpler to to check all these events in the same place - if not, we would have to add code in the player's movement
function, in the enemy movement, and in the code where we drop an object, and that would lead to synchronisation problems with
the others elements in our code.

So I wrote an update function like the one we used for the doors, that is called every frame:

		void CTiles::updatePressPlates()
		{
			for (int y = 0; y < map.mSize.y; ++y)
				for (int x = 0; x < map.mSize.x; ++x)
				{
					CVec2   pos(x, y);
					CTile*  tile = map.getTile(pos);

					bool&   state = pressPlateStates[y * map.mSize.x + x];
					bool    lastState = state;
					state = false;

					if (tile->getType() == eTilePressPlate)
					{
						if (player.pos == pos)
						{
							if (tile->getBoolParam("byChampions") == true && interface.isChampionEmpty(0) == false)
								state = true;
						}

						// walk on pressure plate
						if (lastState == false && state == true)
							scripts.execute(tile->getScriptParam("onPressed"));

						// leave pressure plate
						if (lastState == true && state == false)
							scripts.execute(tile->getScriptParam("onReleased"));
					}
				}
		}
				
Here we loop through every plate tile, we ony check if our party is on it - as we don't have ennemies or objects yet- and
we execute the corresponding script if it's state changed - either if it is pressed or released.