Part 47: Monsters Part 7: seeking
Downloads
Cone of vision
bool CMonsterGroup2::isTargetInConeOfVision(CVec2 pos, CVec2 target, EMonsterDir dir)
{
int distA, distB;
if (dir == eMonsterDirUp)
{
distA = pos.y - target.y;
distB = ABS(pos.x - target.x);
}
else if (dir == eMonsterDirDown)
{
distA = target.y - pos.y;
distB = ABS(pos.x - target.x);
}
else if (dir == eMonsterDirLeft)
{
distA = pos.x - target.x;
distB = ABS(pos.y - target.y);
}
else
{
distA = target.x - pos.x;
distB = ABS(pos.y - target.y);
}
return (distA > 0 && distB <= distA);
}
As the monsters in the group can have different orientations, we will check this cone for each of them until we
int CMonsterGroup2::getVisibleDistance(CVec2 pos, CVec2 target, uint8_t type)
{
CMonsters::SMonsterData& data = monsters.monstersDatas[type];
for (int i = 0; i < 4; ++i)
{
if (mMonsters[i].pos != eMonsterPosNone)
{
if (data.sideAttack == true ||
isTargetInConeOfVision(pos, target, mMonsters[i].dir) == true)
{
Distance
<!-- 07 Screamer -->
<monster name = "MONSTER07">
[...]
<side_attack/>
<sight_range>1</sight_range>
[...]
</monster>
Computing the distance between the group and the party using the classic Pythagoras formula is expensive as it
CVec2 CVec2::abs(const CVec2& vec)
{
return CVec2(ABS(vec.x),
ABS(vec.y));
}
uint32_t CVec2::manhattanDistance(const CVec2& dest)
{
CVec2 dist = CVec2::abs(dest - *this);
return dist.x + dist.y;
}
The view distance of the monsters varies with the light. The darker the dungeon, the shorter this max distance is.
int CMonsterGroup2::getVisibleDistance(CVec2 pos, CVec2 target, uint8_t type)
{
CMonsters::SMonsterData& data = monsters.monstersDatas[type];
for (int i = 0; i < 4; ++i)
{
if (mMonsters[i].pos != eMonsterPosNone)
{
if (data.sideAttack == true ||
isTargetInConeOfVision(pos, target, mMonsters[i].dir) == true)
{
int range = data.sightRange;
int light = game.getLightPower();
range -= (256 - light) / 102;
if (range < 1)
range = 1;
int dist = pos.manhattanDistance(target);
if (dist > range)
return 0;
else
return distanceWithWalls(pos, target);
}
}
}
return 0;
}
Walls blocking the sight
//---------------------------------------------------------------------------------------------
// returns the distance between source and dest if there are no walls in between, or returns 0 if there are walls
int CMonsterGroup2::distanceWithWalls(CVec2 source, CVec2 dest)
{
CVec2 delta = dest - source;
CVec2 absDist = CVec2::abs(delta);
// if we are right next to the destination, don't take the walls into account.
if (absDist.x + absDist.y <= 1)
return 1;
// is the line more horizontal or more vertical ?
bool isXSmallerThanY = (absDist.x < absDist.y);
// compute the steps for advancing one tile at a time along the largest axis
float stepX, stepY;
if (isXSmallerThanY == true)
{
stepY = SGN(delta.y);
stepX = stepY * (float)delta.x / (float)delta.y;
}
else
{
stepX = SGN(delta.x);
stepY = stepX * (float)delta.y / (float)delta.x;
}
// for symetry reasons, start at the center of the tile
float posX = (float)source.x + 0.5;
float posY = (float)source.y + 0.5;
while (true)
{
// advance
float nextX = posX + stepX;
float nextY = posY + stepY;
// test walls
CVec2 srcTile((int)posX, (int)posY);
CVec2 nextTile((int)nextX, (int)nextY);
if (isWallBetweenPos(srcTile, nextTile) == true)
return 0;
// are we at the end ?
if (nextTile.manhattanDistance(dest) <= 1)
return absDist.x + absDist.y;
// update position and loop back
posX = nextX;
posY = nextY;
}
}
The isWallBetweenPos() function will check if there is a wall between 2 tiles.
bool CMonsterGroup2::isWallBetweenPos(CVec2 source, CVec2 dest)
{
CTile* srcTile = map.getTile(source);
CTile* dstTile = map.getTile(dest);
// check walls
if (dest.x > source.x)
{
if (srcTile->mWalls[eWallSideRight].getType() != 0)
return true;
}
else if (dest.x < source.x)
{
if (srcTile->mWalls[eWallSideLeft].getType() != 0)
return true;
}
if (dest.y > source.y)
{
if (srcTile->mWalls[eWallSideDown].getType() != 0)
return true;
}
else if (dest.y < source.y)
{
if (srcTile->mWalls[eWallSideUp].getType() != 0)
return true;
}
// check stairs
if (dstTile->getType() == eTileStairs)
return true;
// check doors
if (dstTile->getType() == eTileDoor && dstTile->getBoolParam("isOpened") == false)
{
// we can't see through a closed door unless it's a porticullis
if (dstTile->getIntParam("Type") != 0)
return true;
}
return false;
}
Getting the direction to the party
//---------------------------------------------------------------------------------------------
EMonsterDir CMonsterGroup2::nextDir(EMonsterDir dir)
{
static const EMonsterDir nextDirs[4] =
{
eMonsterDirRight, // up
eMonsterDirLeft, // down
eMonsterDirUp, // left
eMonsterDirDown // right
};
return nextDirs[dir];
}
//---------------------------------------------------------------------------------------------
EMonsterDir CMonsterGroup2::prevDir(EMonsterDir dir)
{
static const EMonsterDir prevDirs[4] =
{
eMonsterDirLeft, // up
eMonsterDirRight, // down
eMonsterDirDown, // left
eMonsterDirUp // right
};
return prevDirs[dir];
}
//---------------------------------------------------------------------------------------------
EMonsterDir CMonsterGroup2::oppositeDir(EMonsterDir dir)
{
static const EMonsterDir oppositeDirs[4] =
{
eMonsterDirDown, // up
eMonsterDirUp, // down
eMonsterDirRight, // left
eMonsterDirLeft // right
};
return oppositeDirs[dir];
}
Now, to get the direction the first cases are simple: if the group is aligned with the party along x or y, we
EMonsterDir CMonsterGroup2::getDirectionToTarget(CVec2 pos, CVec2 target, EMonsterDir& secondaryDir)
{
// group aligned with target
if (pos.x == target.x)
{
secondaryDir = (RANDOM(2) ? eMonsterDirLeft: eMonsterDirRight);
if (target.y < pos.y)
return eMonsterDirUp;
else
return eMonsterDirDown;
}
if (pos.y == target.y)
{
secondaryDir = (RANDOM(2) ? eMonsterDirUp: eMonsterDirDown);
if (target.x < pos.x)
return eMonsterDirLeft;
else
return eMonsterDirRight;
}
Now here is the interesting part.
// group is not aligned with target: we find in which direction we need to turn to face it
EMonsterDir primaryDir = eMonsterDirUp;
while (true)
{
if (isTargetInConeOfVision(pos, target, primaryDir) == true)
{
Then we check the directions 90° to the left and right. If we can't see the party in one of those directions, then
// found a direction where we can see the target
secondaryDir = nextDir(primaryDir);
if (isTargetInConeOfVision(pos, target, secondaryDir) == false)
{
secondaryDir = prevDir(primaryDir);
if (isTargetInConeOfVision(pos, target, secondaryDir) == false)
{
// target can neither be seen when we turn right nor when we turn left, so we are in the "middle" of the cone
if (RANDOM(2))
secondaryDir = oppositeDir(secondaryDir);
return primaryDir;
}
}
If the party is also visible in one of the 2 perpendicular directions we tested, that means that it is on the edge
// we can see the target both in the primary and secondary direction, it is on the edge of the cone
if (RANDOM(2))
{
EMonsterDir temp = primaryDir;
primaryDir = secondaryDir;
secondaryDir = temp;
}
return primaryDir;
}
primaryDir = (EMonsterDir)(primaryDir + 1);
}
}
The seek state
void CMonsterGroup2::fight(CVec2 mapPos, uint8_t type)
{
// transition to seek state ?
if (mapPos.manhattanDistance(player.pos) != 1)
enterSeekState();
}
Notice that I wrote "enter" functions for each state just to initialize the variables needed.
void CMonsterGroup2::wander(CVec2 mapPos, uint8_t type)
{
// transition to seek state ?
int dist = getVisibleDistance(mapPos, player.pos, type);
if (dist > 0)
{
enterSeekState();
}
else
{
// wander
[...]
}
}
Finally the in the seek state, we first check the transition to the fight state.
void CMonsterGroup2::seek(CVec2 mapPos, uint8_t type)
{
// transition to fight state ?
if (mapPos.manhattanDistance(player.pos) == 1)
{
enterFightState(mapPos);
}
Now remember that I said at the beginning that the group does not always go towards the party.
else
{
// seek
int dist = getVisibleDistance(mapPos, player.pos, type);
if (dist > 0)
{
lastPlayerPos = player.pos;
}
else
{
// We can't see the player. If we arrived at the destination, go back to wander
if (mapPos == lastPlayerPos)
{
enterWanderState();
return;
}
}
Now we will walk towards lastPlayerPos.
if (moveTimer.update() == true)
{
initMoveTimer(type);
// choose direction
EMonsterDir primaryDir;
EMonsterDir secondaryDir;
primaryDir = getDirectionToTarget(mapPos, lastPlayerPos, secondaryDir);
if (canMove(mapPos, (EWallSide)primaryDir) == true)
{
}
else if (canMove(mapPos, (EWallSide)secondaryDir) == true)
{
primaryDir = secondaryDir;
}
else if (canMove(mapPos, (EWallSide)oppositeDir(secondaryDir)) == true)
{
primaryDir = oppositeDir(secondaryDir);
}
else if (canMove(mapPos, (EWallSide)oppositeDir(primaryDir)) == true)
{
primaryDir = oppositeDir(primaryDir);
}
else
{
primaryDir = (EMonsterDir)4;
enterWanderState();
}
// move
if (primaryDir != 4)
{
dir = primaryDir;
move(mapPos, type);
for (int i = 0; i < 4; ++i)
mMonsters[i].dir = primaryDir;
}
}
}
}
Conclusion