Complete roguelike tutorial using C++ and libtcod - part 11: dungeon levels and character progression

From RogueBasin
Revision as of 12:37, 19 October 2015 by Joel Pera (talk | contribs) (pasted →‎Stairs)
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 article, we turn the single level prototype into a real game by adding multi-level dungeons (in fact infinite levels) and experience level for the player.

Stairs

Obviously, the first thing we need is some stairs to go deeper into the dungeon. Where as other actors are only displayed when they are in the field of view, we would like the stairs to be always visible. We need another boolean on the Actor class for this :

bool blocks; // can we walk on this actor?
bool fovOnly; // only display when in fov
Attacker *attacker; // something that deals damages

Of course this field must be initialized in the constructor :

Actor::Actor(int x, int y, int ch, const char *name, 
   const TCODColor &col) :
   x(x),y(y),ch(ch),col(col),name(name),
   blocks(true),fovOnly(true),attacker(NULL),destructible(NULL),ai(NULL),
   pickable(NULL),container(NULL) {
}

The engine rendering code must be updated to use this new field :

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

Now we can create the stairs. But we need to keep a pointer on them to be able to detect when the player stands on their cell, so let's add an Actor pointer in the Engine class :

Actor *player;
Actor *stairs;
Map *map;

We create the stair in the Engine::init function :

actors.push(player);
stairs = new Actor(0,0,'>',"stairs",TCODColor::white);
stairs->blocks=false;
stairs->fovOnly=false;
actors.push(stairs);
map = new Map(80,43);

In the end of Map::createRoom, we put the stair in the middle of the room. This will be called for every room in the dungeon. In the end, the stairs will be in the last room of the BSP tree, far from the player who is in the first room.

// set stairs position
engine.stairs->x=(x1+x2)/2;
engine.stairs->y=(y1+y2)/2;

The stairs must be saved along with other actors in Engine::save :

// then the player
player->save(zip);
// then the stairs
stairs->save(zip);
// then all the other actors
zip.putInt(actors.size()-2);
for (Actor **it=actors.begin(); it!=actors.end(); it++) {
   if ( *it != player && *it != stairs ) {

And restored in Engine::load :

player->load(zip);
// the stairs
stairs=new Actor(0,0,0,NULL,TCODColor::white);
stairs->load(zip);
actors.push(stairs);           
// then all other actors

For this tutorial, we create one-way stairs. Once you go down, you can't go back up. But you can improve this by adding stairs going up. You don't even need a new class, you can use the actor character (either > or <) to detect the type of stair.

When the player is on the stair, he can push '>' to go down to the next level. We check this in PlayerAi::handleActionKey :

case '>' :
   if ( engine.stairs->x == owner->x && engine.stairs->y == owner->y ) {
       engine.nextLevel();
   } else {
       engine.gui->message(TCODColor::lightGrey,"There are no stairs here.");
   }
break;

The Engine::nextLevel function deals with the dungeon regeneration. We'll keep the current level number in a field of the Engine.

Engine.hpp:

int level;
void nextLevel();

This level starts with 1 :

Engine::Engine(int screenWidth, int screenHeight) : gameStatus(STARTUP),
   player(NULL),map(NULL),fovRadius(10),
   screenWidth(screenWidth),screenHeight(screenHeight),level(1) {

and is increased in nextLevel() :

void Engine::nextLevel() {
   level++;

We also display some messages and heal the player :

gui->message(TCODColor::lightViolet,"You take a moment to rest, and recover your strength.");
player->destructible->heal(player->destructible->maxHp/2);
gui->message(TCODColor::red,"After a rare moment of peace, you descend\ndeeper into the heart of the dungeon...");

Now we clean the dungeon, removing all the monsters and items, but not the player. There's also no point deleting and recreating the stairs :

   delete map;
   // delete all actors but player and stairs
   for (Actor **it=actors.begin(); it!=actors.end(); it++) {
       if ( *it != player && *it != stairs ) {
           delete *it;
           it = actors.remove(it);
       }
   }

We're using a special function of TCODList here that makes it possible to remove an element from the list while iterating over it. It's very important to use the remove function that takes the iterator as parameter and not the function that takes a list element. Doing this :

actors.remove(*it)

on the last element of the list would result in it getting bigger than actors.end(). The loop would keep rolling until a SIGSEGV occurs (see the extra article about debugging).

Another very important thing when iterating over a TCODList : never add an element to the list inside the loop. Adding an element might result in a reallocation of the list. The iterator value would not be correct anymore after that. If you need to add an element to the list, create another toAdd list, fill it with the elements to be added. Once you finished iterating over the first list, call myList.addAll(toAdd).

Back to the nextLevel function, we can now recreate a map (including actors) :

   // create a new map
   map = new Map(80,43);
   map->init(true);
   gameStatus=STARTUP;
}

One last thing we want to do is to display the dungeon level, in Gui::render, just before blitting the gui console :

// dungeon level
con->setDefaultForeground(TCODColor::white);
con->print(3,3,"Dungeon level %d",engine.level);