Complete roguelike tutorial using C++ and libtcod - part 4: field of view

From RogueBasin
Revision as of 18:07, 20 December 2015 by Joel Pera (talk | contribs) (added source link)
Jump to navigation Jump to search
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.


In this fourth part, we're computing the player field of view to light only the visible part of the dungeon. We also only display the part of the dungeon that the player has explored. We're going to use the libtcod TCODMap object to compute the field of view.

View source here

libtcod functions used in this article

TCODMap::TCODMap

TCODMap::isWalkable

TCODMap::isInFov

TCODMap::computeFov

TCODMap::setProperties

Updating the Map class

In Map.hpp we're adding a reference to a TCODMap object and we add a few helper functions along with the FOV compute function :

   bool isInFov(int x, int y) const;
   bool isExplored(int x, int y) const;
   void computeFov();
   void render() const;
protected :
   Tile *tiles;
   TCODMap *map;
   friend class BspListener;

Since TCODMap already contains canWalk / canSeeThrough information, we remove the canWalk field from the Tile object. In fact, we're replacing it with an "explored" field that indicates whether this tile has already been seen by the player.

struct Tile {
   bool explored; // has the player already seen this tile ?
   Tile() : explored(false) {}
};

In Map.cpp, we allocate the TCODMap object in the constructor :

Map::Map(int width, int height) : width(width),height(height) {
   tiles=new Tile[width*height];
   map=new TCODMap(width,height);

and delete it in the destructor :

Map::~Map() {
   delete [] tiles;
   delete map;
}

The new isWall method uses the TCODMap object :

bool Map::isWall(int x, int y) const {
   return !map->isWalkable(x,y);
}

whereas the isExplored function uses our Tile array :

bool Map::isExplored(int x, int y) const {
   return tiles[x+y*width].explored;
}

And now the functions dealing with the field of view :

bool Map::isInFov(int x, int y) const {
   if ( map->isInFov(x,y) ) {
       tiles[x+y*width].explored=true;
       return true;
   }
   return false;
}

void Map::computeFov() {
   map->computeFov(engine.player->x,engine.player->y,
       engine.fovRadius);
}

We're using a fovRadius field on the engine class, this way we'll be able to dynamically change the radius.

The isInFov function does not only returns the computed value from the TCODMap object, it also updates the explored flags when a cell enters the field of view.

The dig function must also be updated to use the TCODMap object :

for (int tilex=x1; tilex <= x2; tilex++) {
   for (int tiley=y1; tiley <= y2; tiley++) {
       map->setProperties(tilex,tiley,true,true);
   }
}

In Map::render(), we define two new constants for the light colors :

static const TCODColor lightWall(130,110,50);
static const TCODColor lightGround(200,180,50);

During the map drawing phase, we use the light color for tiles in FOV and the dark color for explored tiles not in fov. Other tiles are not drawn.

for (int x=0; x < width; x++) {
   for (int y=0; y < height; y++) {
       if ( isInFov(x,y) ) {
           TCODConsole::root->setCharBackground(x,y,
               isWall(x,y) ? lightWall :lightGround );
       } else if ( isExplored(x,y) ) {
           TCODConsole::root->setCharBackground(x,y,
               isWall(x,y) ? darkWall : darkGround );
       }
   }
}

Updating the rendering code in the Engine class

First, we add a computeFov boolean in the Engine class. Computing the field of view is expensive so we will only do it when the player moves.

In Engine.hpp :

public :
   int fovRadius;
private :
   bool computeFov;

We have to compute the field of view for the first game frame so we initialize computeFov to true in Engine.cpp :

Engine::Engine() : fovRadius(10), computeFov(true) {

You can set fovRadius to 0 for an infinite radius. In Engine::update(), we recompute the FOV every time the player moves :

switch(key.vk) {
   case TCODK_UP :
       if ( !map->isWall(player->x,player->y-1)) {
           player->y--;
           computeFov=true;
       }
   break;
   case TCODK_DOWN :
       if ( !map->isWall(player->x,player->y+1)) {
           player->y++;
           computeFov=true;
       }
   break;
   case TCODK_LEFT :
       if ( !map->isWall(player->x-1,player->y)) {
           player->x--;
           computeFov=true;
       }
   break;
   case TCODK_RIGHT :
       if ( !map->isWall(player->x+1,player->y)) {
           player->x++;
           computeFov=true;
       }
   break;
   default:break;
 }
if ( computeFov ) {
   map->computeFov();
   computeFov=false;
}

In Engine::render(), we only draw actors if they're in the FOV :

// draw the actors
for (Actor **iterator=actors.begin(); iterator != actors.end();
   iterator++) {
   Actor *actor=*iterator;
   if ( map->isInFov(actor->x,actor->y) ) {
       actor->render();
   }
}

Let's roll !

As usual, compile and enjoy exploring the dungeon :

Windows :

> g++ src/*.cpp -o tuto -Iinclude -Llib -ltcod-mingw -static-libgcc -static-libstdc++ -Wall

Linux :

> g++ src/*.cpp -o tuto -Iinclude -L. -ltcod -ltcodxx -Wl,-rpath=. -Wall