Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - extra 3: scent tracking"

From RogueBasin
Jump to navigation Jump to search
(Add syntaxhighlight. Try to keep indentation across code blocks.)
 
(One intermediate revision by one other user not shown)
Line 9: Line 9:
The first thing to do is to add a scent level to the tile struct, in Map.hpp :
The first thing to do is to add a scent level to the tile struct, in Map.hpp :


struct Tile {
<syntaxhighlight lang="C++" highlight="3-4">
struct Tile {
     bool explored; // has the player already seen this tile ?
     bool explored; // has the player already seen this tile ?
     <span style="color:green">unsigned int scent; // amount of player scent on this cell
     unsigned int scent; // amount of player scent on this cell
     Tile() : explored(false),scent(0) {}</span>
     Tile() : explored(false),scent(0) {}
};
};
</syntaxhighlight>
We also add a currentSmellValue to the Map and a helper to get the scent level of a cell :
We also add a currentSmellValue to the Map and a helper to get the scent level of a cell :


unsigned int currentScentValue;
<syntaxhighlight lang="C++">
unsigned int getScent(int x, int y) const;
unsigned int currentScentValue;
unsigned int getScent(int x, int y) const;
</syntaxhighlight>


The base idea is that every time the player moves, currentScentValue is increased and the scent is updated in every cell in the field of view with the value currentScentValue - distance to player. The monster will track the player based on their surrounding cell scent. The closer a cell's scent is to currentScentValue the higher the scent is for the monster. This way, the closer you are to the player, the higher is the scent.  And scent is decreased every new turn in a cell that is not in the player field of view without having to update the scent values in the whole map.
The base idea is that every time the player moves, currentScentValue is increased and the scent is updated in every cell in the field of view with the value currentScentValue - distance to player. The monster will track the player based on their surrounding cell scent. The closer a cell's scent is to currentScentValue the higher the scent is for the monster. This way, the closer you are to the player, the higher is the scent.  And scent is decreased every new turn in a cell that is not in the player field of view without having to update the scent values in the whole map.
Line 23: Line 27:
So the first thing to do is to increase the current scent value every new turn, in Engine::update :
So the first thing to do is to increase the current scent value every new turn, in Engine::update :


if ( gameStatus == NEW_TURN ) {
<syntaxhighlight lang="C++" highlight="2">
  <span style="color:green"> map->currentScentValue++;</span>
if ( gameStatus == NEW_TURN ) {
    map->currentScentValue++;
</syntaxhighlight>
   
   
The Map::getScent method is simple :
The Map::getScent method is simple :


unsigned int Map::getScent(int x, int y) const {
<syntaxhighlight lang="C++">
unsigned int Map::getScent(int x, int y) const {
     return tiles[x+y*width].scent;
     return tiles[x+y*width].scent;
}
}
</syntaxhighlight>


The core of the scent field computation is in Map::computeFov :
The core of the scent field computation is in Map::computeFov :


void Map::computeFov() {
<syntaxhighlight lang="C++" highlight="4-18">
void Map::computeFov() {
     map->computeFov(engine.player->x,engine.player->y,
     map->computeFov(engine.player->x,engine.player->y,
         engine.fovRadius);
         engine.fovRadius);
     <span style="color:green">// update scent field
     // update scent field
     for (int x=0; x < width; x++) {
     for (int x=0; x < width; x++) {
         for (int y=0; y < height; y++) {
         for (int y=0; y < height; y++) {
Line 51: Line 60:
             }
             }
         }
         }
     }</span>
     }
}
}
</syntaxhighlight>


==Monsters with a truffle==
==Monsters with a truffle==
Line 58: Line 68:
Now we must decide how "far" the scent is propagating. "Far" concerns both the distance and time. We use a constant for that, to be able to fine tune it later :
Now we must decide how "far" the scent is propagating. "Far" concerns both the distance and time. We use a constant for that, to be able to fine tune it later :


// after 20 turns, the monster cannot smell the scent anymore
<syntaxhighlight lang="C++">
static const int SCENT_THRESHOLD=20;
// after 20 turns, the monster cannot smell the scent anymore
static const int SCENT_THRESHOLD=20;
</syntaxhighlight>


This means that a monster won't detect the scent if the player is 20 cells away or if the player has been here more than 20 turns ago. This translates to : cell scent >= currentScentValue-20. This constant has to be defined in Ai.hpp, not Ai.cpp because we need to access its value in Map.cpp too. Indeed, we will initialize the currentScentValue with the threshold value to keep monsters from detecting smell everywhere during the 20 first turns :
This means that a monster won't detect the scent if the player is 20 cells away or if the player has been here more than 20 turns ago. This translates to : cell scent >= currentScentValue-20. This constant has to be defined in Ai.hpp, not Ai.cpp because we need to access its value in Map.cpp too. Indeed, we will initialize the currentScentValue with the threshold value to keep monsters from detecting smell everywhere during the 20 first turns :


Map::Map(int width, int height) : width(width),height(height),currentScentValue(SCENT_THRESHOLD) {
<syntaxhighlight lang="C++">
Map::Map(int width, int height) : width(width),height(height),currentScentValue(SCENT_THRESHOLD) {
</syntaxhighlight>


We don't need the Ai::moveCount field anymore. You can remove it from the header and the constructor. The update function is much simpler : always track the player !
We don't need the Ai::moveCount field anymore. You can remove it from the header and the constructor. The update function is much simpler : always track the player !


void MonsterAi::update(Actor *owner) {
<syntaxhighlight lang="C++">
void MonsterAi::update(Actor *owner) {
     if ( owner->destructible && owner->destructible->isDead() ) {
     if ( owner->destructible && owner->destructible->isDead() ) {
         return;
         return;
     }
     }
     moveOrAttack(owner, engine.player->x,engine.player->y);
     moveOrAttack(owner, engine.player->x,engine.player->y);
}
}
</syntaxhighlight>
All the actual Ai will be in the new moveOrAttack function. First, handle the case where we're at melee range :
All the actual Ai will be in the new moveOrAttack function. First, handle the case where we're at melee range :


void MonsterAi::moveOrAttack(Actor *owner, int targetx, int targety) {
<syntaxhighlight lang="C++">
void MonsterAi::moveOrAttack(Actor *owner, int targetx, int targety) {
     int dx = targetx - owner->x;
     int dx = targetx - owner->x;
     int dy = targety - owner->y;
     int dy = targety - owner->y;
Line 85: Line 102:
         }
         }
         return;
         return;
</syntaxhighlight>


If the player is not at attack range but is in fov, walk towards him :
If the player is not at attack range but is in fov, walk towards him :


} else if (engine.map->isInFov(owner->x,owner->y)) {
<syntaxhighlight lang="C++">
    } else if (engine.map->isInFov(owner->x,owner->y)) {
         // player in sight. go towards him !
         // player in sight. go towards him !
         dx = (int)(round(dx/distance));
         dx = (int)(round(dx/distance));
Line 97: Line 116:
                 return;
                 return;
         }
         }
}
    }
</syntaxhighlight>
Now we're in the case where the player is not visible. We're going to scan the monster's 8 adjacent cells and see which one has the highest scent. First, let's define some variables :
Now we're in the case where the player is not visible. We're going to scan the monster's 8 adjacent cells and see which one has the highest scent. First, let's define some variables :


// player not visible. use scent tracking.
<syntaxhighlight lang="C++">
// find the adjacent cell with the highest scent level
    // player not visible. use scent tracking.
unsigned int bestLevel=0;
    // find the adjacent cell with the highest scent level
int bestCellIndex=-1;
    unsigned int bestLevel=0;
static int tdx[8]={-1,0,1,-1,1,-1,0,1};
    int bestCellIndex=-1;
static int tdy[8]={-1,-1,-1,0,0,1,1,1};
    static int tdx[8]={-1,0,1,-1,1,-1,0,1};
    static int tdy[8]={-1,-1,-1,0,0,1,1,1};
</syntaxhighlight>


bestLevel will contain the best scent level found so far, and bestCellIndex, the number of the cell where this scent was found. The tdx and tdy arrays will make it easy to scan the 8 surrounding cells. They contain the dx,dy movement to reach each 8 cell.
bestLevel will contain the best scent level found so far, and bestCellIndex, the number of the cell where this scent was found. The tdx and tdy arrays will make it easy to scan the 8 surrounding cells. They contain the dx,dy movement to reach each 8 cell.


for (int i=0; i<  8; i++) {
<syntaxhighlight lang="C++">
    int cellx=owner->x+tdx[i];
    for (int i=0; i<  8; i++) {
    int celly=owner->y+tdy[i];
        int cellx=owner->x+tdx[i];
    if (engine.map->canWalk(cellx,celly)) {
        int celly=owner->y+tdy[i];
        if (engine.map->canWalk(cellx,celly)) {
</syntaxhighlight>


So we're scanning the cells, taking into account only walkable cells.
So we're scanning the cells, taking into account only walkable cells.


                unsigned int cellScent = engine.map->getScent(cellx,celly);       
<syntaxhighlight lang="C++">
                if (cellScent > engine.map->currentScentValue - SCENT_THRESHOLD
            unsigned int cellScent = engine.map->getScent(cellx,celly);       
                    && cellScent > bestLevel) {
            if (cellScent > engine.map->currentScentValue - SCENT_THRESHOLD
                        bestLevel=cellScent;
                && cellScent > bestLevel) {
                        bestCellIndex=i;
                bestLevel=cellScent;
                }
                bestCellIndex=i;
        }
            }
}
        }
    }
</syntaxhighlight>


All the "intelligence" lies within those few lines. We get the cell's current scent value and check that it's not too old/far :
All the "intelligence" lies within those few lines. We get the cell's current scent value and check that it's not too old/far :


cellScent > engine.map->currentScentValue - SCENT_THRESHOLD
<syntaxhighlight lang="C++">
    cellScent > engine.map->currentScentValue - SCENT_THRESHOLD
</syntaxhighlight>


If it's better than what we found so far, we update the bestLevel/bestCellIndex values.
If it's better than what we found so far, we update the bestLevel/bestCellIndex values.
Line 133: Line 161:
If a cell with enough scent has been found, we walk on it.
If a cell with enough scent has been found, we walk on it.


if ( bestCellIndex != -1 ) {
<syntaxhighlight lang="C++">
    // the monster smells the player. follow the scent
    if ( bestCellIndex != -1 ) {
    owner->x += tdx[bestCellIndex];
        // the monster smells the player. follow the scent
    owner->y += tdy[bestCellIndex];
        owner->x += tdx[bestCellIndex];
}
        owner->y += tdy[bestCellIndex];
    }
</syntaxhighlight>


You can now compile and enjoy your clever monsters.
You can now compile and enjoy your clever monsters.
==Visible smells==
It can be convenient to display the smell value in the game, for debugging purposes. You can do that by changing the Map::render code :
<syntaxhighlight lang="C++">
for (int x=0; x < width; x++) {
    for (int y=0; y < height; y++) {
        int scent=SCENT_THRESHOLD - (currentScentValue - getScent(x,y));
        scent = CLAMP(0,10,scent);
        float sc=scent * 0.1f;
        if ( isInFov(x,y) ) {
            TCODConsole::root->setCharBackground(x,y,
                isWall(x,y) ? lightWall : TCODColor::lightGrey * sc );
        } else if ( isExplored(x,y) ) {
            TCODConsole::root->setCharBackground(x,y,
                isWall(x,y) ? darkWall : TCODColor::lightGrey * sc );
        } else if (!isWall(x,y)) {
            TCODConsole::root->setCharBackground(x,y,
                TCODColor::white * sc );
        }
    }
}
</syntaxhighlight>




[[Category:Developing]]
[[Category:Developing]]

Latest revision as of 08:24, 20 July 2022

Complete roguelike tutorial using C++ and libtcod
-originally written by Jice
Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21.


This article is an optional "extra" that will bring scent tracking to the monster Ai. In can be applied on the article 6 source code.

Even with the wall sliding trick, the monsters are still quite dumb. We'll implement scent tracking so that they can track the player even when they don't see him.

Tiles that smell

The first thing to do is to add a scent level to the tile struct, in Map.hpp :

struct Tile {
    bool explored; // has the player already seen this tile ?
    unsigned int scent; // amount of player scent on this cell
    Tile() : explored(false),scent(0) {}
};

We also add a currentSmellValue to the Map and a helper to get the scent level of a cell :

unsigned int currentScentValue;
unsigned int getScent(int x, int y) const;

The base idea is that every time the player moves, currentScentValue is increased and the scent is updated in every cell in the field of view with the value currentScentValue - distance to player. The monster will track the player based on their surrounding cell scent. The closer a cell's scent is to currentScentValue the higher the scent is for the monster. This way, the closer you are to the player, the higher is the scent. And scent is decreased every new turn in a cell that is not in the player field of view without having to update the scent values in the whole map.

So the first thing to do is to increase the current scent value every new turn, in Engine::update :

if ( gameStatus == NEW_TURN ) {
    map->currentScentValue++;

The Map::getScent method is simple :

unsigned int Map::getScent(int x, int y) const {
    return tiles[x+y*width].scent;
}

The core of the scent field computation is in Map::computeFov :

void Map::computeFov() {
    map->computeFov(engine.player->x,engine.player->y,
        engine.fovRadius);
    // update scent field
    for (int x=0; x < width; x++) {
        for (int y=0; y < height; y++) {
            if (isInFov(x,y)) {
                unsigned int oldScent=getScent(x,y);
                int dx=x-engine.player->x;
                int dy=y-engine.player->y;
                long distance=(int)sqrt(dx*dx+dy*dy);
                unsigned int newScent=currentScentValue-distance;
                if (newScent > oldScent) {
                    tiles[x+y*width].scent = newScent;
                }
            }
        }
    }
}

Monsters with a truffle

Now we must decide how "far" the scent is propagating. "Far" concerns both the distance and time. We use a constant for that, to be able to fine tune it later :

// after 20 turns, the monster cannot smell the scent anymore
static const int SCENT_THRESHOLD=20;

This means that a monster won't detect the scent if the player is 20 cells away or if the player has been here more than 20 turns ago. This translates to : cell scent >= currentScentValue-20. This constant has to be defined in Ai.hpp, not Ai.cpp because we need to access its value in Map.cpp too. Indeed, we will initialize the currentScentValue with the threshold value to keep monsters from detecting smell everywhere during the 20 first turns :

Map::Map(int width, int height) : width(width),height(height),currentScentValue(SCENT_THRESHOLD) {

We don't need the Ai::moveCount field anymore. You can remove it from the header and the constructor. The update function is much simpler : always track the player !

void MonsterAi::update(Actor *owner) {
    if ( owner->destructible && owner->destructible->isDead() ) {
        return;
    }
    moveOrAttack(owner, engine.player->x,engine.player->y);
}

All the actual Ai will be in the new moveOrAttack function. First, handle the case where we're at melee range :

void MonsterAi::moveOrAttack(Actor *owner, int targetx, int targety) {
    int dx = targetx - owner->x;
    int dy = targety - owner->y;
    float distance=sqrtf(dx*dx+dy*dy);
    if ( distance < 2 ) {
        // at melee range. attack !
        if ( owner->attacker ) {
            owner->attacker->attack(owner,engine.player);
        }
        return;

If the player is not at attack range but is in fov, walk towards him :

    } else if (engine.map->isInFov(owner->x,owner->y)) {
        // player in sight. go towards him !
        dx = (int)(round(dx/distance));
        dy = (int)(round(dy/distance));
        if ( engine.map->canWalk(owner->x+dx,owner->y+dy) ) {
                owner->x += dx;
                owner->y += dy;
                return;
        }
    }

Now we're in the case where the player is not visible. We're going to scan the monster's 8 adjacent cells and see which one has the highest scent. First, let's define some variables :

    // player not visible. use scent tracking.
    // find the adjacent cell with the highest scent level
    unsigned int bestLevel=0;
    int bestCellIndex=-1;
    static int tdx[8]={-1,0,1,-1,1,-1,0,1};
    static int tdy[8]={-1,-1,-1,0,0,1,1,1};

bestLevel will contain the best scent level found so far, and bestCellIndex, the number of the cell where this scent was found. The tdx and tdy arrays will make it easy to scan the 8 surrounding cells. They contain the dx,dy movement to reach each 8 cell.

    for (int i=0; i<  8; i++) {
        int cellx=owner->x+tdx[i];
        int celly=owner->y+tdy[i];
        if (engine.map->canWalk(cellx,celly)) {

So we're scanning the cells, taking into account only walkable cells.

            unsigned int cellScent = engine.map->getScent(cellx,celly);      
            if (cellScent > engine.map->currentScentValue - SCENT_THRESHOLD
                && cellScent > bestLevel) {
                bestLevel=cellScent;
                bestCellIndex=i;
            }
        }
    }

All the "intelligence" lies within those few lines. We get the cell's current scent value and check that it's not too old/far :

    cellScent > engine.map->currentScentValue - SCENT_THRESHOLD

If it's better than what we found so far, we update the bestLevel/bestCellIndex values.

If a cell with enough scent has been found, we walk on it.

    if ( bestCellIndex != -1 ) {
        // the monster smells the player. follow the scent
        owner->x += tdx[bestCellIndex];
        owner->y += tdy[bestCellIndex];
    }

You can now compile and enjoy your clever monsters.


Visible smells

It can be convenient to display the smell value in the game, for debugging purposes. You can do that by changing the Map::render code :

for (int x=0; x < width; x++) {
    for (int y=0; y < height; y++) {
        int scent=SCENT_THRESHOLD - (currentScentValue - getScent(x,y));
        scent = CLAMP(0,10,scent);
        float sc=scent * 0.1f;
        if ( isInFov(x,y) ) {
            TCODConsole::root->setCharBackground(x,y,
                isWall(x,y) ? lightWall : TCODColor::lightGrey * sc );
        } else if ( isExplored(x,y) ) {
            TCODConsole::root->setCharBackground(x,y,
                isWall(x,y) ? darkWall : TCODColor::lightGrey * sc );
        } else if (!isWall(x,y)) {
            TCODConsole::root->setCharBackground(x,y,
                 TCODColor::white * sc );
        }
    }
}