Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 10.1: persistence"

From RogueBasin
Jump to navigation Jump to search
(pasted →‎The map)
Line 80: Line 80:
     return 0;
     return 0;
  }
  }
===The map===
We could save the map by putting every tile property in the file. Or we can only save the seed of the random number generator and re-generate the map. For this, we need to store the seed and the random number generator in the map class. Previously, we were using libtcod's default random number generator with an unknown seed. We also have to move the map initialization code out of the constructor so that we're able to call it when the map is loaded from the file :
Map.hpp
  <span style="color:green">void init(bool withActors);</span>
protected :
  Tile *tiles;
  TCODMap *map;
  <span style="color:green">long seed;
  TCODRandom *rng;</span>
  friend class BspListener;
  void dig(int x1, int y1, int x2, int y2);
  <span style="color:green">void createRoom(bool first, int x1, int y1, int x2, int y2, bool withActors);</span>
The withActors parameter tells the map whether it should create monsters and items or if they will be loaded from a saved game. The constructor new only gets some random seed :
Map::Map(int width, int height)
    : width(width),height(height) {
    seed=TCODRandom::getInstance()->getInt(0,0x7FFFFFFF);
}
In case you wonder, 0x7FFFFFFF is the highest possible 32 bit signed integer value. The init function uses the code that was previously in the constructor, but the splitRecursive function now uses the map's RNG :
void Map::init(bool withActors) {
    rng = new TCODRandom(seed, TCOD_RNG_CMWC);
    tiles=new Tile[width*height];
    map=new TCODMap(width,height);
    TCODBsp bsp(0,0,width,height);
    bsp.splitRecursive(rng,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
    BspListener listener(*this);
    bsp.traverseInvertedLevelOrder(&listener,(void *)withActors);
}
The withActors boolean is passed  the the BSP listener in the userData parameter. It's a void * parameter that can contain pretty much anything. You can use it to store any numeric value (an int, a  float, a boolean, a char...) or the adress of some struct/class. Here, we're simply casting the boolean into void *.
The room creation code now uses the map's RNG instead of libtcod's default one. It also retrieve the withActors value to pass it to the createRoom function. In Map.cpp, BspListener::visitNode :
    bool withActors=(bool)userData;
<span style="color:green">// dig a room
w=map.rng->getInt(ROOM_MIN_SIZE, node->w-2);
h=map.rng->getInt(ROOM_MIN_SIZE, node->h-2);
x=map.rng->getInt(node->x+1, node->x+node->w-w-1);
y=map.rng->getInt(node->y+1, node->y+node->h-h-1);</span>
map.createRoom(roomNum == 0, x, y, x+w-1, y+h-1, withActors);
The createRoom function digs the room, then handle actors only if withActors is true :
<span style="color:green">void Map::createRoom(bool first, int x1, int y1, int x2, int y2, bool withActors) {</span>
    dig (x1,y1,x2,y2);
    <span style="color:green">if (!withActors) {
        return;
    }</span>
    if ( first ) {
If you want to be able to restart a game using the exact same map as before by entering the same seed, you also need to use the map's RNG in the createRoom, addMonster and addItem functions. But since there's no way to enter a seed right now, we won't do it.




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

Revision as of 12:41, 16 October 2015

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 part, we will implement saving and loading the game to be able to close it and resume it later. And this is not going to be pleasing. C++ has a few great feats, but it certainly sucks at type introspection and object serialization. Most modern languages have these features built in. For this reason, the article 10 of Jotaf's python tutorial will be split in two for the C++ version.

In fact, there's a way to serialize a C++ object and store it on the disk using only a couple of lines of code : using boost Serialization library. But we're not going to use it for several reasons :

  • this tutorial is not about using boost but rather how to code the features yourself using basic C++.
  • even if using boost would considerably reduce the size of the code to write, you still have to embed boost in your game. Using hand-made code will replace 8000 lines of boost code with less than 300 lines of specific code, resulting in a lighter executable and a simplified compilation process for your project.
  • using libtcod's zip toolkit means that the save file will be compressed. Of course, this means less space used on the disk, but also harder to hack savegame files. Of course, using boost doesn't keep you from compressing the file, but it just comes at no cost when using libtcod.

Anyway if you prefer to use boost, you can follow this article to reorganize the Map and Engine code, and just skip the whole Persistent stuff and replace Engine::load and Engine::save code with boost invocation.

libtcod features used in this article :

TCODZip

TCODSystem::deleteFile

TCODSystem::fileExists

The plan

Since this part does not include a game menu, we're going to do a minimal savegame support :

  • when the player closes the game window, the game state is saved to a file
  • when the player starts the game, it resumes the last saved game
  • when the player dies, the savegame is deleted (you wouldn't write a roguelike without permadeath, would you?)

While this simplifies the article, it leaves an annoying case : when the player manage to kill all monsters, he will be stuck. The only way to restart a game is to erase the savegame by hand. This will be fixed in the next article.

Refactoring

The engine

The first thing, obviously, is to be able to save and load the whole game. We also need to remove the initializing code from the Engine constructor, because when the Engine is created, we don't know yet if we have to generate a new map or load a previously saved one. We put the functions in the Engine class :

Engine.hpp

   bool pickATile(int *x, int *y, float maxRange = 0.0f);
   void init();
   void load();
   void save();
};

The new constructor doesn't create the map and actors anymore :

Engine::Engine(int screenWidth, int screenHeight) : gameStatus(STARTUP),
   player(NULL),map(NULL),fovRadius(10),
   screenWidth(screenWidth),screenHeight(screenHeight) {
   TCODConsole::initRoot(screenWidth,screenHeight,"libtcod C++ tutorial",false);
   gui = new Gui();
}

All the initialization code has been moved to the init function :

void Engine::init() { 
   player = new Actor(40,25,'@',"player",TCODColor::white);
   player->destructible=new PlayerDestructible(30,2,"your cadaver");
   player->attacker=new Attacker(5);
   player->ai = new PlayerAi();
   player->container = new Container(26);
   actors.push(player);
   map = new Map(80,43);
   map->init(true);
   gui->message(TCODColor::red, 
       "Welcome stranger!\nPrepare to perish in the Tombs of the Ancient Kings.");
}

Now the load function will either load a saved game or call Engine::init to create a new one. We simply call it before the main loop. We also save the game once the player closes the game window.

main.cpp :

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

The map

We could save the map by putting every tile property in the file. Or we can only save the seed of the random number generator and re-generate the map. For this, we need to store the seed and the random number generator in the map class. Previously, we were using libtcod's default random number generator with an unknown seed. We also have to move the map initialization code out of the constructor so that we're able to call it when the map is loaded from the file :

Map.hpp

  void init(bool withActors);
protected :
  Tile *tiles;
  TCODMap *map;
  long seed;
  TCODRandom *rng;
  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, bool withActors);

The withActors parameter tells the map whether it should create monsters and items or if they will be loaded from a saved game. The constructor new only gets some random seed :

Map::Map(int width, int height) 
   : width(width),height(height) {
   seed=TCODRandom::getInstance()->getInt(0,0x7FFFFFFF);
}

In case you wonder, 0x7FFFFFFF is the highest possible 32 bit signed integer value. The init function uses the code that was previously in the constructor, but the splitRecursive function now uses the map's RNG :

void Map::init(bool withActors) {
   rng = new TCODRandom(seed, TCOD_RNG_CMWC);
   tiles=new Tile[width*height];
   map=new TCODMap(width,height);
   TCODBsp bsp(0,0,width,height);
   bsp.splitRecursive(rng,8,ROOM_MAX_SIZE,ROOM_MAX_SIZE,1.5f,1.5f);
   BspListener listener(*this);
   bsp.traverseInvertedLevelOrder(&listener,(void *)withActors);
}

The withActors boolean is passed the the BSP listener in the userData parameter. It's a void * parameter that can contain pretty much anything. You can use it to store any numeric value (an int, a float, a boolean, a char...) or the adress of some struct/class. Here, we're simply casting the boolean into void *.

The room creation code now uses the map's RNG instead of libtcod's default one. It also retrieve the withActors value to pass it to the createRoom function. In Map.cpp, BspListener::visitNode :

   bool withActors=(bool)userData;
// dig a room
w=map.rng->getInt(ROOM_MIN_SIZE, node->w-2);
h=map.rng->getInt(ROOM_MIN_SIZE, node->h-2);
x=map.rng->getInt(node->x+1, node->x+node->w-w-1);
y=map.rng->getInt(node->y+1, node->y+node->h-h-1);
map.createRoom(roomNum == 0, x, y, x+w-1, y+h-1, withActors);

The createRoom function digs the room, then handle actors only if withActors is true :

void Map::createRoom(bool first, int x1, int y1, int x2, int y2, bool withActors) {
   dig (x1,y1,x2,y2);
   if (!withActors) {
       return;
   }
   if ( first ) {

If you want to be able to restart a game using the exact same map as before by entering the same seed, you also need to use the map's RNG in the createRoom, addMonster and addItem functions. But since there's no way to enter a seed right now, we won't do it.