Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 3: dungeon building"

From RogueBasin
Jump to navigation Jump to search
m (Fixed repository link to point to the code for this part. Previously the link was to Part 2 of the code.)
(Use syntaxhighlight for code blocks, fixes broken map attribute name.)
Line 19: Line 19:
Whereas we were adding walls in an empty map in the last article, this time, we will dig holes in a map full of walls. We need to change the Map class declaration for that :
Whereas we were adding walls in an empty map in the last article, this time, we will dig holes in a map full of walls. We need to change the Map class declaration for that :


Tile() : canWalk(false) {}
<syntaxhighlight lang="C++">Tile() : canWalk(false) {}</syntaxhighlight>


Tiles default to non-walking.
Tiles default to non-walking.


protected :
<syntaxhighlight lang="C++">
    Tile *tiles;
protected :
    <span style="color:green">friend class BspListener;
  Tile *tiles;
  <span style="color:green">friend class BspListener;
    void dig(int x1, int y1, int x2, int y2);
 
    void createRoom(bool first, int x1, int y1, int x2, int y2);</span>
  void dig(int x1, int y1, int x2, int y2);
};
  void createRoom(bool first, int x1, int y1, int x2, int y2);</span>
};
</syntaxhighlight>


We replace the setWall function by a function that digs a rectangular zone. We also make a declaration that will allow some BspListener class to use the protected dig function. The BspListener class is not declared in the header because it's private to the Map class. The createRoom function will dig the room and populate it with actors.
We replace the setWall function by a function that digs a rectangular zone. We also make a declaration that will allow some BspListener class to use the protected dig function. The BspListener class is not declared in the header because it's private to the Map class. The createRoom function will dig the room and populate it with actors.
Line 37: Line 39:
We define some constants for the room size range :
We define some constants for the room size range :


static const int ROOM_MAX_SIZE = 12;
<syntaxhighlight lang="C++">
static const int ROOM_MIN_SIZE = 6;
static const int ROOM_MAX_SIZE = 12;
static const int ROOM_MIN_SIZE = 6;
</syntaxhighlight>


The static keyword, when used on a global variable means that the variable is not visible from outside the .cpp file.
The static keyword, when used on a global variable means that the variable is not visible from outside the .cpp file.
Line 44: Line 48:
===The digging function===
===The digging function===


void Map::dig(int x1, int y1, int x2, int y2) {
<syntaxhighlight lang="C++">
    if ( x2 < x1 ) {
void Map::dig(int x1, int y1, int x2, int y2) {
        int tmp=x2;
  if ( x2 < x1 ) {
        x2=x1;
      int tmp=x2;
        x1=tmp;
      x2=x1;
    }
      x1=tmp;
    if ( y2 < y1 ) {
  }
        int tmp=y2;
  if ( y2 < y1 ) {
        y2=y1;
      int tmp=y2;
        y1=tmp;
      y2=y1;
    }
      y1=tmp;
    for (int tilex=x1; tilex <= x2; tilex++) {
  }
        for (int tiley=y1; tiley <= y2; tiley++) {
  for (int tilex=x1; tilex <= x2; tilex++) {
            tiles[tilex+tiley*width].canWalk=true;
      for (int tiley=y1; tiley <= y2; tiley++) {
        }
          tiles[tilex+tiley*width].canWalk=true;
    }
      }
}
  }
}
</syntaxhighlight>


The only subtlety here is that we allow x2 to be smaller than x1 and y2 to be smaller than y1. That's why we swap the variables first to put them back in the right order. Being able to dig with *2 coordinates smaller than *1 will be handy for corridor digging.
The only subtlety here is that we allow x2 to be smaller than x1 and y2 to be smaller than y1. That's why we swap the variables first to put them back in the right order. Being able to dig with *2 coordinates smaller than *1 will be handy for corridor digging.
Line 66: Line 72:
===The room creation===
===The room creation===


void Map::createRoom(bool first, int x1, int y1, int x2, int y2) {
<syntaxhighlight lang="C++">
    dig (x1,y1,x2,y2);
void Map::createRoom(bool first, int x1, int y1, int x2, int y2) {
    if ( first ) {
  dig (x1,y1,x2,y2);
        // put the player in the first room
  if ( first ) {
        engine.player->x=(x1+x2)/2;
      // put the player in the first room
        engine.player->y=(y1+y2)/2;
      engine.player->x=(x1+x2)/2;
    } else {
      engine.player->y=(y1+y2)/2;
        TCODRandom *rng=TCODRandom::getInstance();
  } else {
        if ( rng->getInt(0,3)==0 ) {
      TCODRandom *rng=TCODRandom::getInstance();
            engine.actors.push(new Actor((x1+x2)/2,(y1+y2)/2,'@',
      if ( rng->getInt(0,3)==0 ) {
                TCODColor::yellow));
          engine.actors.push(new Actor((x1+x2)/2,(y1+y2)/2,'@',
        }
              TCODColor::yellow));
    }
      }
}
  }
}
</syntaxhighlight>


First we dig the room, then put either the player in its center (only for the first room) or some NPC in 25% of other rooms. We're using here libtcod's random number generator.
First we dig the room, then put either the player in its center (only for the first room) or some NPC in 25% of other rooms. We're using here libtcod's random number generator.
Line 85: Line 93:
===The new constructor : where the BSP magic resides===
===The new constructor : where the BSP magic resides===


Map::Map(int width, int height) : width(width),height(height) {
<syntaxhighlight lang="C++">
    tiles=new Tile[width*height];
Map::Map(int width, int height) : width(width),height(height) {
    <span style="color:green">TCODBsp bsp(0,0,width,height);
  tiles=new Tile[width*height];
    bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
  <span style="color:green">TCODBsp bsp(0,0,width,height);
    BspListener listener(*this);
  bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
    bsp.traverseInvertedLevelOrder(&listener,NULL);</span>
  BspListener listener(*this);
}
  bsp.traverseInvertedLevelOrder(&listener,NULL);</span>
}
</syntaxhighlight>


Here we're using libtcod's [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp.html?c=false&cpp=true&cs=false&py=false&lua=false BSP toolkit] to easily create a dungeon using [http://roguecentral.org/doryen/articles/bsp-dungeon-generation/ this algorithm]. We create a bsp object the same size as our map
Here we're using libtcod's [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp.html?c=false&cpp=true&cs=false&py=false&lua=false BSP toolkit] to easily create a dungeon using [http://roguecentral.org/doryen/articles/bsp-dungeon-generation/ this algorithm]. We create a bsp object the same size as our map


TCODBsp bsp(0,0,width,height);
<syntaxhighlight lang="C++">TCODBsp bsp(0,0,width,height);</syntaxhighlight>


and [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp_split.html?c=false&cpp=true&cs=false&py=false&lua=false#1 split it] so that every node size is at least the maximum size of our rooms. The recursion level (8) is not very important because the splitting process stops as soon as the nodes reach the desired size. Every new recursion level, the nodes are split in 2.
and [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp_split.html?c=false&cpp=true&cs=false&py=false&lua=false#1 split it] so that every node size is at least the maximum size of our rooms. The recursion level (8) is not very important because the splitting process stops as soon as the nodes reach the desired size. Every new recursion level, the nodes are split in 2.


bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
<syntaxhighlight lang="C++">bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);</syntaxhighlight>


The 1.5 coefficients are the maximum H/V and V/H ratio of the nodes. That means there will be rectangular nodes, but there won't be very flat nodes.
The 1.5 coefficients are the maximum H/V and V/H ratio of the nodes. That means there will be rectangular nodes, but there won't be very flat nodes.
Line 105: Line 115:
Finally we create the listener and [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp_traverse.html?c=false&cpp=true&cs=false&py=false&lua=false traverse the BSP tree].
Finally we create the listener and [http://roguecentral.org/doryen/data/libtcod/doc/1.5.1/html2/bsp_traverse.html?c=false&cpp=true&cs=false&py=false&lua=false traverse the BSP tree].


BspListener listener(*this);
<syntaxhighlight lang="C++">
bsp.traverseInvertedLevelOrder(&listener,NULL);
BspListener listener(*this);
bsp.traverseInvertedLevelOrder(&listener,NULL);
</syntaxhighlight>


===The BSP listener===
===The BSP listener===
Line 112: Line 124:
All we have to do now is a class that implements libtcod ITCODBspCallback interface.
All we have to do now is a class that implements libtcod ITCODBspCallback interface.


class BspListener : public ITCODBspCallback {
<syntaxhighlight lang="C++">
private :
class BspListener : public ITCODBspCallback {
    Map &map; // a map to dig
private :
    int roomNum; // room number
  Map &map; // a map to dig
    int lastx,lasty; // center of the last room
  int roomNum; // room number
  int lastx,lasty; // center of the last room
</syntaxhighlight>


Here we're creating a class that inherits a class declared in litbcod.hpp. This ITCODBspCallback class only has a single abstract virtual "visitNode" method. This is the method TCODBsp will call for every node in the BSP tree.
Here we're creating a class that inherits a class declared in litbcod.hpp. This ITCODBspCallback class only has a single abstract virtual "visitNode" method. This is the method TCODBsp will call for every node in the BSP tree.
Line 128: Line 142:
The constructor is straightforward :
The constructor is straightforward :
   
   
public :
<syntaxhighlight lang="C++">
    BspListener(Map &map) : map(map), roomNum(0) {}
public :
  BspListener(Map &map) : map(map), roomNum(0) {}
</syntaxhighlight>


And now the node visiting function :
And now the node visiting function :


bool visitNode(TCODBsp *node, void *userData) {
<syntaxhighlight lang="C++">
    if ( node->isLeaf() ) {
bool visitNode(TCODBsp *node, void *userData) {
  if ( node->isLeaf() ) {
</syntaxhighlight>


We're only creating rooms in the BSP leafs.
We're only creating rooms in the BSP leafs.


int x,y,w,h;
<syntaxhighlight lang="C++">
// dig a room
int x,y,w,h;
TCODRandom *rng=TCODRandom::getInstance();
// dig a room
w=rng->getInt(ROOM_MIN_SIZE, node->w-2);
TCODRandom *rng=TCODRandom::getInstance();
h=rng->getInt(ROOM_MIN_SIZE, node->h-2);
w=rng->getInt(ROOM_MIN_SIZE, node->w-2);
x=rng->getInt(node->x+1, node->x+node->w-w-1);
h=rng->getInt(ROOM_MIN_SIZE, node->h-2);
y=rng->getInt(node->y+1, node->y+node->h-h-1);
x=rng->getInt(node->x+1, node->x+node->w-w-1);
map.createRoom(roomNum == 0, x, y, x+w-1, y+h-1);
y=rng->getInt(node->y+1, node->y+node->h-h-1);
map.createRoom(roomNum == 0, x, y, x+w-1, y+h-1);
</syntaxhighlight>


We're using libtcod's random number generator to get some random room size and position. We make sure the size is bigger than ROOM_MIN_SIZE and smaller than the node size (remember the nodes are bigger than ROOM_MAX_SIZE). We subtract 2 to the node size to ensure the room does not reach the node border. Thus we avoid joined rooms and more important, we avoid rooms reaching the map border. This way, no need to check if the player goes out of the map in the walking code because there will always be a wall on the map border.
We're using libtcod's random number generator to get some random room size and position. We make sure the size is bigger than ROOM_MIN_SIZE and smaller than the node size (remember the nodes are bigger than ROOM_MAX_SIZE). We subtract 2 to the node size to ensure the room does not reach the node border. Thus we avoid joined rooms and more important, we avoid rooms reaching the map border. This way, no need to check if the player goes out of the map in the walking code because there will always be a wall on the map border.


if ( roomNum != 0 ) {
<syntaxhighlight lang="C++">
    // dig a corridor from last room
if ( roomNum != 0 ) {
    map.dig(lastx,lasty,x+w/2,lasty);
  // dig a corridor from last room
    map.dig(x+w/2,lasty,x+w/2,y+h/2);
  map.dig(lastx,lasty,x+w/2,lasty);
}
  map.dig(x+w/2,lasty,x+w/2,y+h/2);
}
</syntaxhighlight>


For other rooms, we dig a corridor from the center of the last room.
For other rooms, we dig a corridor from the center of the last room.


            lastx=x+w/2;
<syntaxhighlight lang="C++">
            lasty=y+h/2;
          lastx=x+w/2;
            roomNum++;
          lasty=y+h/2;
        }
          roomNum++;
        return true;
      }
    }
      return true;
};
  }
};
</syntaxhighlight>
Finally, we update the last room center coordinates and increase the room number. Returning true tells TCODBsp to keep traversing the tree. Returning false would allow to break from the traversal.
Finally, we update the last room center coordinates and increase the room number. Returning true tells TCODBsp to keep traversing the tree. Returning false would allow to break from the traversal.


Since the Map implementation is now using the TCODBsp and Engine classes, we need to update the headers :
Since the Map implementation is now using the TCODBsp and Engine classes, we need to update the headers :


#include "libtcod.hpp"
<syntaxhighlight lang="C++">
#include "Map.hpp"
#include "libtcod.hpp"
#include "Actor.hpp"
#include "Map.hpp"
#include "Engine.hpp"
#include "Actor.hpp"
#include "Engine.hpp"
</syntaxhighlight>


==Finalizing==
==Finalizing==
Line 177: Line 203:
That's it. We only have to slightly change the Engine constructor to remove the NPC creation and we're done :
That's it. We only have to slightly change the Engine constructor to remove the NPC creation and we're done :


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


Let's compile the code with the same commands as in the previous articles :
Let's compile the code with the same commands as in the previous articles :

Revision as of 05:45, 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.


In this article, we will improve the Map class to generate a real dungeon with rooms and corridors. We'll use a very simple dungeon generator, using a straightforward way to connect rooms together. This will result in "winnable" dungeons (no room disconnected) but with a slightly chaotic look. For a better corridor connection, check the libtcod C++ samples in the libtcod/samples/ directory.

View source here

libtcod functions used in this article

TCODRandom::getInstance

TCODRandom::getInt

TCODBsp::TCODBsp

TCODBsp::splitRecursive

TCODBsp::traverseInvertedLevelOrder

A hole digging map

Whereas we were adding walls in an empty map in the last article, this time, we will dig holes in a map full of walls. We need to change the Map class declaration for that :

Tile() : canWalk(false) {}

Tiles default to non-walking.

protected :
   Tile *tiles;
   <span style="color:green">friend class BspListener;

   void dig(int x1, int y1, int x2, int y2);
   void createRoom(bool first, int x1, int y1, int x2, int y2);</span>
};

We replace the setWall function by a function that digs a rectangular zone. We also make a declaration that will allow some BspListener class to use the protected dig function. The BspListener class is not declared in the header because it's private to the Map class. The createRoom function will dig the room and populate it with actors.

The implementation

We define some constants for the room size range :

static const int ROOM_MAX_SIZE = 12;
static const int ROOM_MIN_SIZE = 6;

The static keyword, when used on a global variable means that the variable is not visible from outside the .cpp file.

The digging function

void Map::dig(int x1, int y1, int x2, int y2) {
   if ( x2 < x1 ) {
       int tmp=x2;
       x2=x1;
       x1=tmp;
   }
   if ( y2 < y1 ) {
       int tmp=y2;
       y2=y1;
       y1=tmp;
   }
   for (int tilex=x1; tilex <= x2; tilex++) {
       for (int tiley=y1; tiley <= y2; tiley++) {
           tiles[tilex+tiley*width].canWalk=true;
       }
   }
}

The only subtlety here is that we allow x2 to be smaller than x1 and y2 to be smaller than y1. That's why we swap the variables first to put them back in the right order. Being able to dig with *2 coordinates smaller than *1 will be handy for corridor digging.

The room creation

void Map::createRoom(bool first, int x1, int y1, int x2, int y2) {
   dig (x1,y1,x2,y2);
   if ( first ) {
       // put the player in the first room
       engine.player->x=(x1+x2)/2;
       engine.player->y=(y1+y2)/2;
   } else {
       TCODRandom *rng=TCODRandom::getInstance();
       if ( rng->getInt(0,3)==0 ) {
           engine.actors.push(new Actor((x1+x2)/2,(y1+y2)/2,'@',
               TCODColor::yellow));
       }
   }
}

First we dig the room, then put either the player in its center (only for the first room) or some NPC in 25% of other rooms. We're using here libtcod's random number generator.

The new constructor : where the BSP magic resides

Map::Map(int width, int height) : width(width),height(height) {
   tiles=new Tile[width*height];
   <span style="color:green">TCODBsp bsp(0,0,width,height);
   bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
   BspListener listener(*this);
   bsp.traverseInvertedLevelOrder(&listener,NULL);</span>
}

Here we're using libtcod's BSP toolkit to easily create a dungeon using this algorithm. We create a bsp object the same size as our map

TCODBsp bsp(0,0,width,height);

and split it so that every node size is at least the maximum size of our rooms. The recursion level (8) is not very important because the splitting process stops as soon as the nodes reach the desired size. Every new recursion level, the nodes are split in 2.

bsp.splitRecursive(NULL,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);

The 1.5 coefficients are the maximum H/V and V/H ratio of the nodes. That means there will be rectangular nodes, but there won't be very flat nodes.

Finally we create the listener and traverse the BSP tree.

BspListener listener(*this);
bsp.traverseInvertedLevelOrder(&listener,NULL);

The BSP listener

All we have to do now is a class that implements libtcod ITCODBspCallback interface.

class BspListener : public ITCODBspCallback {
private :
   Map &map; // a map to dig
   int roomNum; // room number
   int lastx,lasty; // center of the last room

Here we're creating a class that inherits a class declared in litbcod.hpp. This ITCODBspCallback class only has a single abstract virtual "visitNode" method. This is the method TCODBsp will call for every node in the BSP tree.

Concerning the fields, we need a reference to the map to dig (we could have used a pointer. A reference is just a hidden pointer that can't be null).

roomNum will contains the number of room. We're only using it to put the player in the first room.

lastx and lasty will store the coordinates of the center of the last created room. We need this to dig a corridor between the last room and the current one.

The constructor is straightforward :

public :
   BspListener(Map &map) : map(map), roomNum(0) {}

And now the node visiting function :

bool visitNode(TCODBsp *node, void *userData) {
   if ( node->isLeaf() ) {

We're only creating rooms in the BSP leafs.

int x,y,w,h;
// dig a room
TCODRandom *rng=TCODRandom::getInstance();
w=rng->getInt(ROOM_MIN_SIZE, node->w-2);
h=rng->getInt(ROOM_MIN_SIZE, node->h-2);
x=rng->getInt(node->x+1, node->x+node->w-w-1);
y=rng->getInt(node->y+1, node->y+node->h-h-1);
map.createRoom(roomNum == 0, x, y, x+w-1, y+h-1);

We're using libtcod's random number generator to get some random room size and position. We make sure the size is bigger than ROOM_MIN_SIZE and smaller than the node size (remember the nodes are bigger than ROOM_MAX_SIZE). We subtract 2 to the node size to ensure the room does not reach the node border. Thus we avoid joined rooms and more important, we avoid rooms reaching the map border. This way, no need to check if the player goes out of the map in the walking code because there will always be a wall on the map border.

if ( roomNum != 0 ) {
   // dig a corridor from last room
   map.dig(lastx,lasty,x+w/2,lasty);
   map.dig(x+w/2,lasty,x+w/2,y+h/2);
}

For other rooms, we dig a corridor from the center of the last room.

           lastx=x+w/2;
           lasty=y+h/2;
           roomNum++;
       }
       return true;
   }
};

Finally, we update the last room center coordinates and increase the room number. Returning true tells TCODBsp to keep traversing the tree. Returning false would allow to break from the traversal.

Since the Map implementation is now using the TCODBsp and Engine classes, we need to update the headers :

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

Finalizing

That's it. We only have to slightly change the Engine constructor to remove the NPC creation and we're done :

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

Let's compile the code with the same commands as in the previous articles :

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 our new dungeon !