Part 40: Monsters part 2: Parameters and display

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

All the monsters groups will have the same parameters:



		<!-- 01 Giant Scorpion -->
		<monster name = "MONSTER01">
			[...]
			<param type="int">nbMonsters</param>
			<param type="enum" values="Up;Down;Left;Right">Dir</param>
			<param type="stack"/>
		</monster>

		<!-- 02 Swamp Slime -->
		<monster name = "MONSTER02">
			[...]
			<param type="int">nbMonsters</param>
			<param type="enum" values="Up;Down;Left;Right">Dir</param>
			<param type="stack"/>
		</monster>
				
"nbMonsters" is the number of monsters in this group.
"Dir" is the direction the group is facing at the beginning.
And the objects stack contains the objects that will be dropped on the ground when all the monsters of the group
are killed.
These stacks will be stored along with the others.
As we used WallSideMax for the stacks in the walls, we will use another value to differentiate the monsters' stacks.

		enum EWallSide
		{
			eWallSideUp = 0,
			eWallSideDown,
			eWallSideLeft,
			eWallSideRight,
			eWallSideMax
		};

		#define eWallSideMonster (EWallSide)(eWallSideMax + 1)
				
Of course these parameters implies a bunch of modifications of the tools in the editor. I won't go into details
with that.

Loading and initializing the monsters

In the game, the monsters database is loaded in a new file "monsters.cpp":

		void CMonsters::readMonstersDB()
		{
			QDomDocument doc;

			QFile f("databases/monsters.xml");
			f.open(QIODevice::ReadOnly);
			doc.setContent(&f);
			f.close();

			QDomElement root = doc.documentElement();
			QDomNode    monsterNode = root.firstChild();

			while(!monsterNode.isNull())
			{
				if (monsterNode.isElement())
				{
					QDomElement monster = monsterNode.toElement();
					std::vector<CParamType> paramsTypesList;

					if (monster.tagName() == "monster")
					{
						QDomElement monsterInfo = monster.firstChild().toElement();
						SMonsterData    monsterData;

						while(!monsterInfo.isNull())
						{
							if (monsterInfo.tagName() == "img_front")
							{
								monsterData.frontImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_side")
							{
								monsterData.sideImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_back")
							{
								monsterData.backImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "img_attack")
							{
								monsterData.attackImage = QString("gfx/3DView/monsters/") + monsterInfo.text();
							}
							else if (monsterInfo.tagName() == "param")
							{
								QString paramType = monsterInfo.attribute("type");

								CParamType  newParamType;

								if (paramType == "int")
								{
									newParamType.mType = eParamInt;
									newParamType.mName = monsterInfo.text();
									paramsTypesList.push_back(newParamType);
								}
								else if (paramType == "enum")
								{
									newParamType.mType = eParamEnum;
									newParamType.mName = monsterInfo.text();
									newParamType.mValues = monsterInfo.attribute("values").split(";");
									paramsTypesList.push_back(newParamType);
								}
								else if (paramType == "stack")
								{
									newParamType.mType = eParamStack;
									paramsTypesList.push_back(newParamType);
								}
							}

							monsterInfo = monsterInfo.nextSibling().toElement();
						}

						CMap::mMonstersParams.push_back(paramsTypesList);
						monstersDatas.push_back(monsterData);
					}
				}
				monsterNode = monsterNode.nextSibling();
			}
		}
				
You can see that the groups datas are stored in different lists.
The parameters are stored in the CMap class, as for the other elements - the tiles, the walls, etc.
And the group images for each type of monsters are stored in the monstersDatas list:

		class CMonsters
		{
		public:
			[...]

			struct SMonsterData
			{
				QString frontImage;
				QString sideImage;
				QString backImage;
				QString attackImage;
			};
			[...]

		private:
			[...]

			std::vector<SMonsterData>   monstersDatas;
		};
				
After we loaded the database, the groups loaded from the map file are stored in the monsterGroups list.

		class CMonsters
		{
		public:
			[...]

			struct SMonsterGroup
			{
				SMonster    monsters[4];
				EMonsterDir dir;
			};
			[...]

		private:
			[...]

			std::vector<SMonsterGroup>  monsterGroups;
		};
				
You can see that the groups can hold 4 monsters. After we loaded the map, we will call the init() function that
will initialize all monsters separately.

		class CMonsters
		{
		public:
			enum EMonsterPos
			{
				eMonsterPosNone,    // empty monster
				eMonsterPosNorthWest,
				eMonsterPosNorthEast,
				eMonsterPosSouthWest,
				eMonsterPosSouthEast,
				eMonsterPosCenter
			};

			enum EMonsterDir
			{
				eMonsterDirUp,
				eMonsterDirDown,
				eMonsterDirLeft,
				eMonsterDirRight
			};

			struct SMonsterData
			{
				QString frontImage;
				QString sideImage;
				QString backImage;
				QString attackImage;
			};

			struct SMonster
			{
				EMonsterPos pos;
				EMonsterDir dir;
				int life;
			};
				
As you can see, each monster has a position inside the group, its own direction and a life value.
Here is the init() function:

		void CMonsters::init()
		{
			for (size_t i = 0; i < map.mMonsterGroups.size(); ++i)
			{
				CMonsterGroup&  mapGroup = map.mMonsterGroups[i];
				SMonsterGroup   group;
				int nbMonsters = mapGroup.getIntParam("nbMonsters");

				group.dir = (EMonsterDir)mapGroup.getEnumParam("Dir");

				for (int j = 0; j < nbMonsters; j++)
					group.monsters[j].dir = group.dir;

				if (nbMonsters == 1)
				{
					group.monsters[0].pos = eMonsterPosCenter;
					group.monsters[0].life = 3;

					for (int j = 1; j < 4; j++)
					{
						group.monsters[j].pos = eMonsterPosNone;
						group.monsters[j].life = 0;
					}
				}
				else
				{
					// monster positions for a given direction
					static const EMonsterPos startingPos[4][4] =
					{
						{eMonsterPosNorthWest, eMonsterPosNorthEast, eMonsterPosSouthWest, eMonsterPosSouthEast},   // up
						{eMonsterPosSouthEast, eMonsterPosSouthWest, eMonsterPosNorthEast, eMonsterPosNorthWest},   // down
						{eMonsterPosNorthWest, eMonsterPosSouthWest, eMonsterPosNorthEast, eMonsterPosSouthEast},   // left
						{eMonsterPosSouthEast, eMonsterPosNorthEast, eMonsterPosSouthWest, eMonsterPosNorthWest}    // right
					};

					for (int j = 0; j < 4; j++)
					{
						if (j < nbMonsters)
						{
							group.monsters[j].pos = startingPos[group.dir][j];
							group.monsters[j].life = 3;
						}
						else
						{
							group.monsters[j].pos = eMonsterPosNone;
							group.monsters[j].life = 0;
						}
					}
				}

				monsterGroups.push_back(group);
			}
		}
				

Drawing the monsters



Monsters are drawn in the CMonsters::draw() function.
First we will have a for loop because we need to do 3 passes to draw the monsters following the perspective.
We will first draw the back row monsters, then the center ones, and finally the front row.

		void CMonsters::draw(QImage* image, CVec2 mapPos, CVec2 tablePos)
		{
			if (tablePos.y == WALL_TABLE_HEIGHT - 1)
				return;

			int groupIndex = map.findMonsterGroupIndex(mapPos);

			if (groupIndex != -1)
			{
				CMonsterGroup&  mapGroup = map.mMonsterGroups[groupIndex];
				SMonsterGroup&  group = monsterGroups[groupIndex];

				// monster positions depending on the player direction
				static const float monsterPosDir[4][5][2] =
				{
					//   NW          NE          SW          SE           C
					{{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0},  {0.5, 0.5}},   // up
					{{1.0, 0.0}, {1.0, 1.0}, {0.0, 0.0}, {0.0, 1.0},  {0.5, 0.5}},   // left
					{{1.0, 1.0}, {0.0, 1.0}, {1.0, 0.0}, {0.0, 0.0},  {0.5, 0.5}},   // down
					{{0.0, 1.0}, {0.0, 0.0}, {1.0, 1.0}, {1.0, 0.0},  {0.5, 0.5}}    // right
				};

				// monster dir depending on the player direction
				static const EMonsterDir monsterDirDir[4][4] =
				{

					{eMonsterDirUp,    eMonsterDirDown,  eMonsterDirLeft,  eMonsterDirRight}, // up
					{eMonsterDirRight, eMonsterDirLeft,  eMonsterDirUp,    eMonsterDirDown},  // left
					{eMonsterDirDown,  eMonsterDirUp,    eMonsterDirRight, eMonsterDirLeft},  // down
					{eMonsterDirLeft,  eMonsterDirRight, eMonsterDirDown,  eMonsterDirUp}     // right
				};

				// we draw the back row monsters before the front ones
				for (int row = 0; row < 3; ++row)
				{
				
Then we get the direction of the monster from the player's point of view and we find its sprite.

					for (int i = 0; i < 4; ++i)
					{
						SMonster&   monster = group.monsters[i];

						if (monster.pos != eMonsterPosNone)
						{
							// relative direction
							EMonsterDir tableDir = monsterDirDir[player.dir][monster.dir];

							// sprite
							int type = mapGroup.getType();
							QString spriteName;
							bool    flip = false;

							if (tableDir == eMonsterDirDown)
							{
								spriteName = monstersDatas[type].frontImage;
							}
							else if (tableDir == eMonsterDirUp)
							{
								spriteName = monstersDatas[type].backImage;
								if (spriteName.isEmpty() == true)
									spriteName = monstersDatas[type].frontImage;
							}
							else
							{
								spriteName = monstersDatas[type].sideImage;
								if (spriteName.isEmpty() == true)
									spriteName = monstersDatas[type].frontImage;
								else if (tableDir == eMonsterDirLeft)
									flip = true;
							}
				
If the monster is on the row we want to draw, we compute its position on the screen.

							if (monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][1] == (float)row / 2.0)
							{
								// table pos
								float tablePosX = tablePos.x * 2 + monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][0];
								float tablePosY = tablePos.y * 2 + monsterPosDir[player.dir][monster.pos - eMonsterPosNorthWest][1];

								// screen pos
								QImage  sprite = fileCache.getImage(spriteName.toStdString());
								CVec2   pos = getMonsterPos(tablePosX, tablePosY);
				
We scale and darken it like most of the elements in the game

								// compute the scale based on the reference position (the nearest)
								CVec2   pos0, pos1;
								float   scale;

								pos0 = getMonsterPos(WALL_TABLE_WIDTH, (WALL_TABLE_HEIGHT - 1) * 2 + 1);
								pos1 = getMonsterPos(WALL_TABLE_WIDTH, tablePosY);
								scale = 2.37 * (float)(pos1.x - 112) / (float)(pos0.x - 112);

								// scale the monster in a temporary image
								QImage  scaledMonster(sprite.size() * scale, QImage::Format_ARGB32);
								scaledMonster.fill(TRANSPARENT);
								graph2D.drawImageScaled(&scaledMonster, CVec2(), sprite, scale);

								// darken the monster based on it's distance
								float shadow = ((WALL_TABLE_HEIGHT - 1) * 2 - tablePosY - 1) * 0.13f;
								graph2D.darken(&scaledMonster, shadow);
				
And finally we draw it relatively to it's feet.

								// draw the monster (pos relative to its feets)
								pos.x -= scaledMonster.width() / 2;
								pos.y -= scaledMonster.height() - 9 * scale;
								graph2D.drawImage(image, pos, scaledMonster, 0, flip, QRect(0, 33, MAINVIEW_WIDTH, MAINVIEW_HEIGHT));
							}
						}
					}
				}
			}
		}