Part 41: Monsters part 3: Behaviors and attacking
Downloads
Group behaviors
Implementing the states
//---------------------------------------------------------------------------------------------
class CMonster
{
public:
[...]
EMonsterPos pos;
EMonsterDir dir;
int life;
[...]
};
//---------------------------------------------------------------------------------------------
class CMonsterGroup2
{
public:
[...]
CMonster monsters[4];
EMonsterDir dir;
[...]
};
Note that I renamed SMonsterGroup to CMonsterGroup2 to avoid confusion with the CMonsterGroup class declared in
enum EMonsterState
{
eMonsterState_Wandering,
eMonsterState_Fight
};
class CMonsterGroup2
{
public:
[...]
EMonsterState state;
};
To change the value of this state variable, we will need an update() function that will be called every frame.
void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
{
[...]
// handle group behavior for each state
switch (state)
{
case eMonsterState_Wandering:
wander(mapPos, type);
break;
case eMonsterState_Fight:
fight(mapPos, type);
break;
}
}
At the group level these functions don't do very much except handling the state transitions according to the
//---------------------------------------------------------------------------------------------
void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
{
// transition to fight state ?
CVec2 dist = player.pos - mapPos;
bool isNearPlayer = (dist.x == 0 && ABS(dist.y) == 1) || (dist.y == 0 && ABS(dist.x) == 1);
if (isNearPlayer == true)
{
state = eMonsterState_Fight;
[...]
}
}
//---------------------------------------------------------------------------------------------
void CMonsterGroup2::fight(CVec2 mapPos, uint8_t type)
{
// transition to wandering state ?
CVec2 dist = player.pos - mapPos;
bool isNearPlayer = (dist.x == 0 && ABS(dist.y) == 1) || (dist.y == 0 && ABS(dist.x) == 1);
if (isNearPlayer == false)
{
state = eMonsterState_Wandering;
}
}
In the update function of our group, we will also call an update() function for each monster inside this group.
void CMonsterGroup2::update(CVec2 mapPos, uint8_t type)
{
// update all the monsters of the group
for (int i = 0; i < 4; ++i)
if (monsters[i].pos != eMonsterPosNone)
monsters[i].update(*this, mapPos, type);
[...]
}
Monsters behaviors
void CMonster::update(CMonsterGroup2& group, CVec2 mapPos, uint8_t type)
{
displayAttackTimer.update();
switch (group.state)
{
case eMonsterState_Wandering:
wander();
break;
case eMonsterState_Fight:
fight(mapPos, type);
break;
}
}
We said that we won't do anything in the wander state so the wander() function will be empty.
Attack parameters
<monsters>
[...]
<!-- 01 Giant Scorpion -->
<monster name = "MONSTER01">
[...]
<time_between_atk>200</time_between_atk>
<atk_display_time>40</atk_display_time>
<atk_sound>Attack_Scorpion.wav</atk_sound>
[...]
</monster>
class CMonster
{
public:
[...]
CTimer displayAttackTimer;
CTimer nextAttackTimer;
};
In the original game, the time between 2 attacks was computed with a formula looking like this:
void CMonster::initNextAttack(uint8_t type)
{
int minTime = monsters.monstersDatas[type].timeBetweenAttack;
int nextTime = minTime + (rand() % 40) - 10;
if (minTime > 150)
nextTime += (rand() % 80) - 20;
nextAttackTimer.set(nextTime, true);
}
This function sets up one of our timers so we will call it at the moment we enter the "fight" state in the group.
void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
{
// transition to fight state ?
[...]
if (isNearPlayer == true)
{
state = eMonsterState_Fight;
// direction towards the player
EMonsterDir dir;
if (dist.x > 0)
dir = eMonsterDirRight;
else if (dist.x < 0)
dir = eMonsterDirLeft;
else if (dist.y > 0)
dir = eMonsterDirDown;
else
dir = eMonsterDirUp;
// initialize each monster of the group
for (int i = 0; i < 4; ++i)
if (monsters[i].pos != eMonsterPosNone)
{
monsters[i].dir = dir;
monsters[i].initNextAttack(type);
}
}
}
Now, in the fight function of the monster, we update this timer.
void CMonster::fight(CVec2 mapPos, uint8_t type)
{
if (nextAttackTimer.update() == true)
{
QString soundName = monsters.monstersDatas[type].attackSound;
if (soundName.isEmpty() == false)
sound->play(mapPos, soundName.toLocal8Bit().constData());
int attackTime = monsters.monstersDatas[type].attackDisplayTime;
displayAttackTimer.set(attackTime, true);
initNextAttack(type);
}
}
Displaying the attack
// sprite
[...]
if (tableDir == eMonsterDirDown)
{
// facing
if (monster.displayAttackTimer.isRunning())
{
// attack
spriteName = monstersDatas[type].attackImage;
if (spriteName.isEmpty() == true)
spriteName = monstersDatas[type].frontImage;
}
else
{
// normal
spriteName = monstersDatas[type].frontImage;
}
}
[...]