Complete roguelike tutorial using C++ and libtcod - part 10.2: game menu

From RogueBasin
Revision as of 18:20, 20 December 2015 by Joel Pera (talk | contribs) (added source link)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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're going to implement the game menu. This will bring several improvements to the game :

possibility to start a new game even if there is a saved game. possibility to restart a new game once the player dies or wins, or even in the middle of a game. libtcod function used in this article :

TCODImage::TCODImage

TCODImage::blit2x

View source here

Engine refactoring

We now have to be able to restart a new game in the middle of a game. For this, we need some function to clean the engine without having to call the destructor :

Engine.hpp :

   void init();
   void term();
};

The term function must clean the map, the actors and the message log so that the engine is ready to either restart a new game or load a saved one.

Engine::~Engine() {
    <term();
   delete gui;
}

void Engine::term() {
   actors.clearAndDelete();
   if ( map ) delete map;
   gui->clear();
}

The Gui::clear function clears the log :

Gui::~Gui() {
   delete con;
   clear();
}

void Gui::clear() {
   log.clearAndDelete();
}

Now a small subtlety, this the engine can now be initialized after a game was started, so we need to reset the game status to STARTUP to force the recomputation of the field of view, else the fov would be empty after we load a game from the pause menu. In Engine::init :

   gui->message(TCODColor::red, 
       "Welcome stranger!\nPrepare to perish in the Tombs of the Ancient Kings.");
    gameStatus=STARTUP;
}

A menu helper

Now we need some utility to display a menu and return the selected item. I put it in the Gui.hpp/Gui.cpp files to avoid using a dedicated file for such a small class :

Gui.hpp :

class Menu {
public :
   enum MenuItemCode {
   NONE,
   NEW_GAME,
   CONTINUE,
       EXIT
   };
   ~Menu();
   void clear();
   void addItem(MenuItemCode code, const char *label);
   MenuItemCode pick();
protected :
   struct MenuItem {
       MenuItemCode code;
       const char *label;
   };
   TCODList<MenuItem *> items;
};

We use an enum to list all possible cases. NONE is when the player didn't select an item in the menu (when he closes the game window). Each menu item has a code and a label. We add this menu as a public field of the Gui class :

class Gui : public Persistent {
public :
   Menu menu;

The clear function remove all the items from the menu.

Menu::~Menu() {
   clear();
}

void Menu::clear() {
   items.clearAndDelete();
}

The addItem function create a new item associated with a value from the MenuItemCode enumeration.

void Menu::addItem(MenuItemCode code, const char *label) {
   MenuItem *item=new MenuItem();
   item->code=code;
   item->label=label;
   items.push(item);
}

The pick function displays the menu and wait for the player to select an item with UP/DOWN/ENTER keys. To improve the menu look, we'll use some background image. libtcod makes it possible to display an image using the cells background colors. Of course, one pixel per cell results in a very pixelized picture. We can improve that slightly by using some special characters in libtcod's font that make it possible, using both foreground and background color, to display 2x2 pixels per console cell.

We will use the same image as the python tutorial :

You can save it to your project's main directory (right click the image and choose save as..). The image is also in the zip file attached to the article. We load it in a static variable so that the image is loaded only the first time the menu is displayed. On subsequent calls, we reuse the same image.

Menu::MenuItemCode Menu::pick() {
   static TCODImage img("menu_background1.png");

Then we declare a variable to store the currently selected menu item and start the menu loop :

int selectedItem=0;
while( !TCODConsole::isWindowClosed() ) {

The rendering part start by blitting the image on the root console, using subcell resolution :

img.blit2x(TCODConsole::root,0,0);

The image size is 160x50, twice the size of the console, thus, it covers the whole console and will erase all existing characters/colors. Then we render the menu.

int currentItem=0;
for (MenuItem **it=items.begin(); it!=items.end(); it++) {
   if ( currentItem == selectedItem ) {
       TCODConsole::root->setDefaultForeground(TCODColor::lighterOrange);
   } else {
       TCODConsole::root->setDefaultForeground(TCODColor::lightGrey);
   }
   TCODConsole::root->print(10,10+currentItem*3,(*it)->label);
   currentItem++;
}

We use a light orange for the selected item and light grey for the other items. Once the menu is rendered, we flush it to the screen, then check for keypress :

TCODConsole::flush();

// check key presses
TCOD_key_t key;
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL);

We don't use waitForEvent to allow the window close event to be detected. While waiting on waitForEvent, you cannot close the game window by clicking on the close window button.

When the player presses the UP key, we set selectedItem to the previous item :

switch (key.vk) {
       case TCODK_UP : 
           selectedItem--; 
           if (selectedItem < 0) {
               selectedItem=items.size()-1;
           }
       break;

For the DOWN key, we don't need a test, we can use the modulo function to handle the loop :

   case TCODK_DOWN : 
   selectedItem = (selectedItem + 1) % items.size(); 
break;

When the player presses ENTER, we return the item index. Other keys are ignored.

       case TCODK_ENTER : return items.get(selectedItem)->code;
       default : break;
   }
}

Finally, if the player closes the game window, we return NONE :

   return NONE;
}

The start game menu

Everything happens in the Engine::load function. We first create (or recreate) the menu :

engine.gui->menu.clear();
engine.gui->menu.addItem(Menu::NEW_GAME,"New game");
if ( TCODSystem::fileExists("game.sav")) {
   engine.gui->menu.addItem(Menu::CONTINUE,"Continue");
}
engine.gui->menu.addItem(Menu::EXIT,"Exit");

Then we display the menu and wait for the player to chose an item :

Menu::MenuItemCode menuItem=engine.gui->menu.pick();

Let's handle the case when we should simply exit :

if ( menuItem == Menu::EXIT || menuItem == Menu::NONE ) {
   // Exit or window closed
   exit(0);

If the player chooses a new game, we call Engine::init to regenerate a new map. In case there is a game initialized, we call Engine::term first to destroy the map and actors.

} else if ( menuItem == Menu::NEW_GAME ) {
   // New game
   engine.term();
   engine.init();

If we reached this point, the player wants to continue the saved game. Load it as previously, but call Engine::term first to be able to load a game while there is already a running game. Also don't forget to reset the status to STARTUP to force a field of view computation :

   } else {
       TCODZip zip;
       // continue a saved game
       engine.term();
       zip.loadFromFile("game.sav");
       ...

       // finally the message log
       gui->load(zip);
       // to force FOV recomputation
       gameStatus=STARTUP;
   }
}

Ok you can compile and enjoy the menu. You can now restart a new game without having to erase the savegame file by hand when you killed all creatures. But there's still an issue. When you die, you're stuck. We need a way to go back to the menu while in game.

The pause menu

This will be a lot easier than expected because we did everything to be able to create a new game or reload one even if there is already a map and actors. While playing, we want to be able to press ESC to bring the menu. We handle the keypress events in Engine::update :

TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
   if ( lastKey.vk == TCODK_ESCAPE ) {
       save();
       load();
   }
   player->update();

That's it. First, we overwrite the saved game with the current game state (save) then call the load function to display the menu.