Complete roguelike tutorial using C++ and libtcod - part 2: map and actors

From RogueBasin
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 the first part, we created a "walking @" demo. Let's add a NPC and some walls !

View source here

libtcod functions used in this article

TCODConsole:setChar

TCODConsole::setCharForeground

TCODConsole::setCharBackground

Actor class declaration

We'll create two files in the src/ directory. One Actor.hpp header and one Actor.cpp module. The header contains the declaration of the class, the module contains the actual code. I'm using the hpp extension for headers but you can use hxx or h as well.

Actor.hpp

class Actor { 
public :
    int x,y; // position on map
    int ch; // ascii code
    TCODColor col; // color
 
    Actor(int x, int y, int ch, const TCODColor &col);
    void render() const;
};

A few remarks :

  • we're using an int to store the ascii code of the character representing the Actor instead of a char or unsigned char because it makes it possible to use more than 256 different characters.
  • the color in the constructor is passed as a const reference (syntax const <type> & varname) to keep the compiler from duplicating the object before calling the function.
  • the const keyword after the render declaration means that the function does not modify the content of the Actor object (and thus, the function can be called on constant objects).

Actor class implementation

Actor.cpp

#include "libtcod.hpp"
#include "Actor.hpp"

Actor::Actor(int x, int y, int ch, const TCODColor &col) :
    x(x),y(y),ch(ch),col(col) {
}

void Actor::render() const {
    TCODConsole::root->setChar(x,y,ch);
    TCODConsole::root->setCharForeground(x,y,col);
}

We need to include libtcod.hpp before Actor.hpp because Actor.hpp contains references to TCODColor.

The constructor uses an initialization list to define the value of the class' members :

Actor::Actor(int x, int y, int ch, const TCODColor &col) :
    x(x),y(y),ch(ch),col(col)

It's often faster (especially for non intrinsic members) so you should as often as possible use that instead of affectations in the constructor. The fields in the initialization list must appear in the order of their declaration in the .hpp file.

The render function uses some console methods to set the ascii code and foreground color on the root console, leaving the background color unmodified.

Map class declaration

Once again, we create two files, Map.hpp and Map.cpp.

Map.hpp

struct Tile {
    bool canWalk; // can we walk through this tile?
    Tile() : canWalk(true) {}
};

class Map {
public :
    int width,height;

    Map(int width, int height);
    ~Map();
    bool isWall(int x, int y) const;
    void render() const;
protected :
    Tile *tiles;

    void setWall(int x, int y);
};

There are actually two classes in there. In C++, you can declare as many classes as you want in a header file. I consider the Tile class not being big enough to have its own files.

Right now, the Tile only has a single boolean field but it's obvious that we'll add more properties later.

Note that the Tile declaration uses the struct keyword instead of class. They're basically the same except that the field visibility defaults to public in a struct whereas it's private in a class (that's why we add public: in the beginning of our classes). I generally use structs for data-only classes.

The Map class has a Tile pointer member. Since we don't want to hardcode the map size, we will dynamically allocate an array of Tiles. The tiles field will contain the address of the first element of the array.

Since that field will be dynamically allocated, we need to delete it to release the memory when the Map object is destroyed. That's why we declare a destructor.

Map class implementation

First the dependencies :

#include "libtcod.hpp"
#include "Map.hpp"

The constructor allocates the Tile array and creates two pillars :

Map::Map(int width, int height) : width(width),height(height) {
    tiles=new Tile[width*height];
    setWall(30,22);
    setWall(50,22);
}

We're using a single dimension array because it uses less memory and are easier to create than a two dimensions dynamic array.

The destructor releases everything that was allocated in the constructor :

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

The isWall and setWall functions are simple helpers :

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

void Map::setWall(int x, int y) {
    tiles[x+y*width].canWalk=false;
}

Now the render function. First we define some color constants for the map rendering :

void Map::render() const {
    static const TCODColor darkWall(0,0,100);
    static const TCODColor darkGround(50,50,150);

The static keyword means that the variable won't be created every time we call the render function. They'll be created only during the first call. The const keyword helps the compiler to optimize the code by telling him that we won't modify the content of the objects once they're created.

The map rendering code scans the whole map and fills the console background color with the right color.

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

The x and y variable are declared directly inside the for statement. Again, this helps the compiler to optimize the code by knowing exactly the scope of the variables.

We're using the ternary conditional operator ?: instead of a if (...) else statement. We could have used :

if ( isWall(x,y) ) {
    TCODConsole::root->setCharBackground(x,y,darkWall);
} else {
    TCODConsole::root->setCharBackground(x,y,darkGround);
}

The engine

We'll move the main game loop code from the main.cpp file to a Engine class. This will make things easier for the next chapters.

Engine.hpp

class Actor;
class Map;

We will be using pointers to Actor and Map, we don't need to include their definition, but we at least have to declare that they exist.

class Engine {
public :
    TCODList<Actor *> actors;
    Actor *player;
    Map *map;

    Engine();
    ~Engine();
    void update();
    void render();
};

extern Engine engine;

actors is the list of all Actors on the map. Even though the player Actor will be in the list, it's convenient to keep a separate pointer on it for the walking code.

The extern declaration tells the compiler that somewhere, in a .cpp file, there is a global variable named engine. Don't forget to actually declare the variable !

In the Engine implementation, we need several headers :

#include "libtcod.hpp"
#include "Actor.hpp"
#include "Map.hpp"
#include "Engine.hpp"

Engine.hpp is depending on all three previous headers since it references TCODList, Actor and Map.

Constructor

Engine::Engine() {
    TCODConsole::initRoot(80,50,"libtcod C++ tutorial",false);
    player = new Actor(40,25,'@',TCODColor::white);
    actors.push(player);
    actors.push(new Actor(60,13,'@',TCODColor::yellow));
    map = new Map(80,45);
}

We dynamically allocate two Actor objects, one for the player and one for some NPC, and store the object addresses in the actors list. The Map object is also dynamically allocated.

Destructor

Engine::~Engine() {
    actors.clearAndDelete();
    delete map;
}

The TCODList has a helper function to call delete on every object it contains.

World updating code

The update function will handle the player walking code. We use the map isWall function to keep the player from walking through walls.

void Engine::update() {
    TCOD_key_t key;
    TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL);
    switch(key.vk) {
        case TCODK_UP : 
            if ( ! map->isWall(player->x,player->y-1)) {
                player->y--;   
            }
        break;
        case TCODK_DOWN : 
            if ( ! map->isWall(player->x,player->y+1)) {
                player->y++;
            }
        break;
        case TCODK_LEFT : 
            if ( ! map->isWall(player->x-1,player->y)) {
                player->x--;
            }
        break;
        case TCODK_RIGHT : 
            if ( ! map->isWall(player->x+1,player->y)) {
                player->x++;
            }
        break;
        default:break;
    }
}

World rendering code

The world rendering code clears the console, then draw the map.

void Engine::render() {
    TCODConsole::root->clear();
    // draw the map
    map->render();

Finally, we draw all the actors and flush the result to the screen :

    // draw the actors
    for (Actor **iterator=actors.begin(); 
        iterator != actors.end(); iterator++) {
        (*iterator)->render();
    }
}

The TCODList begin() function returns a pointer to the first element. Since our elements are already Actor pointers, we get a pointer to a pointer to an Actor : Actor **. We use the indirection operator (*iterator) to retrieve the object behind the pointer (the actual Actor pointer) and the dereference operator -> to access a member on the pointed object (the actual Actor). Welcome to C/C++ pointers hell !

Alternatively since we are iterating on all actors of the list we could have written the loop with a range base for loop.

    // draw the actors
    for (auto actor: actors) {
        actor->render();
    }

Update the main file

First, we need to include the new headers and create the global Engine variable. Thanks to the Engine class, the main function is now very simple :

#include "libtcod.hpp"
#include "Actor.hpp"
#include "Map.hpp"
#include "Engine.hpp"

Engine engine;

int main() {
    while ( !TCODConsole::isWindowClosed() ) {
        engine.update();
        engine.render();
        TCODConsole::flush();
    }
    return 0;
}

At this stage, we could call TCODConsole::flush() directly in the Engine::render() function but it will be convenient for later chapters to be able to draw something after calling Engine::render buf before the screen is flushed.

Let's run !

Compile the code with the same commands as in the first article :

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

And enjoy !