Part 40: Monsters part 2: Parameters and display
Downloads
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.
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
Loading and initializing the monsters
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.
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
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.
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
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));
}
}
}
}
}
}