Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 10.2: game menu"

From RogueBasin
Jump to navigation Jump to search
(added source link)
(Add syntaxhighlight.)
Line 19: Line 19:
Engine.hpp :
Engine.hpp :


<syntaxhighlight lang="C++" highlight="2">
     void init();
     void init();
     <span style="color:green">void term();</span>
     void term();
};
};
</syntaxhighlight>


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.
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() {
<syntaxhighlight lang="C++">
Engine::~Engine() {
     <term();
     <term();
     delete gui;
     delete gui;
}
}
 
void Engine::term() {
void Engine::term() {
     actors.clearAndDelete();
     actors.clearAndDelete();
     if ( map ) delete map;
     if ( map ) delete map;
     gui->clear();
     gui->clear();
}
}
</syntaxhighlight>


The Gui::clear function clears the log :
The Gui::clear function clears the log :


Gui::~Gui() {
<syntaxhighlight lang="C++">
Gui::~Gui() {
     delete con;
     delete con;
     clear();
     clear();
}
}
 
void Gui::clear() {
void Gui::clear() {
     log.clearAndDelete();
     log.clearAndDelete();
}
}
</syntaxhighlight>
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 :
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 :


<syntaxhighlight lang="C++" highlight="3">
     gui->message(TCODColor::red,  
     gui->message(TCODColor::red,  
         "Welcome stranger!\nPrepare to perish in the Tombs of the Ancient Kings.");
         "Welcome stranger!\nPrepare to perish in the Tombs of the Ancient Kings.");
    <span style="color:green">gameStatus=STARTUP;</span>
    gameStatus=STARTUP;
}
}
</syntaxhighlight>


==A menu helper==
==A menu helper==
Line 59: Line 67:
Gui.hpp :
Gui.hpp :


class Menu {
<syntaxhighlight lang="C++">
public :
class Menu {
public :
     enum MenuItemCode {
     enum MenuItemCode {
     NONE,
     NONE,
Line 71: Line 80:
     void addItem(MenuItemCode code, const char *label);
     void addItem(MenuItemCode code, const char *label);
     MenuItemCode pick();
     MenuItemCode pick();
protected :
protected :
     struct MenuItem {
     struct MenuItem {
         MenuItemCode code;
         MenuItemCode code;
Line 77: Line 86:
     };
     };
     TCODList<MenuItem *> items;
     TCODList<MenuItem *> items;
};
};
</syntaxhighlight>


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 :
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 {
<syntaxhighlight lang="C++" highlight="3">
public :
class Gui : public Persistent {
     <span style="color:green">Menu menu;</span>
public :
     Menu menu;
</syntaxhighlight>


The clear function remove all the items from the menu.
The clear function remove all the items from the menu.


Menu::~Menu() {
<syntaxhighlight lang="C++">
Menu::~Menu() {
     clear();
     clear();
}
}
 
void Menu::clear() {
void Menu::clear() {
     items.clearAndDelete();
     items.clearAndDelete();
}
}
</syntaxhighlight>


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


void Menu::addItem(MenuItemCode code, const char *label) {
<syntaxhighlight lang="C++">
void Menu::addItem(MenuItemCode code, const char *label) {
     MenuItem *item=new MenuItem();
     MenuItem *item=new MenuItem();
     item->code=code;
     item->code=code;
     item->label=label;
     item->label=label;
     items.push(item);
     items.push(item);
}
}
</syntaxhighlight>


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.
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.
Line 110: Line 126:
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.
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() {
<syntaxhighlight lang="C++">
Menu::MenuItemCode Menu::pick() {
     static TCODImage img("menu_background1.png");
     static TCODImage img("menu_background1.png");
</syntaxhighlight>


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


int selectedItem=0;
<syntaxhighlight lang="C++">
while( !TCODConsole::isWindowClosed() ) {
int selectedItem=0;
while( !TCODConsole::isWindowClosed() ) {
</syntaxhighlight>


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


img.blit2x(TCODConsole::root,0,0);
<syntaxhighlight lang="C++">
img.blit2x(TCODConsole::root,0,0);
</syntaxhighlight>


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.
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;
<syntaxhighlight lang="C++">
for (MenuItem **it=items.begin(); it!=items.end(); it++) {
int currentItem=0;
for (MenuItem **it=items.begin(); it!=items.end(); it++) {
     if ( currentItem == selectedItem ) {
     if ( currentItem == selectedItem ) {
         TCODConsole::root->setDefaultForeground(TCODColor::lighterOrange);
         TCODConsole::root->setDefaultForeground(TCODColor::lighterOrange);
Line 133: Line 156:
     TCODConsole::root->print(10,10+currentItem*3,(*it)->label);
     TCODConsole::root->print(10,10+currentItem*3,(*it)->label);
     currentItem++;
     currentItem++;
}
}
</syntaxhighlight>


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 :
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();
<syntaxhighlight lang="C++">
TCODConsole::flush();
// check key presses
 
TCOD_key_t key;
// check key presses
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL);
TCOD_key_t key;
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL);
</syntaxhighlight>


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.
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.
Line 147: Line 173:
When the player presses the UP key, we set selectedItem to the previous item :
When the player presses the UP key, we set selectedItem to the previous item :


switch (key.vk) {
<syntaxhighlight lang="C++">
switch (key.vk) {
         case TCODK_UP :  
         case TCODK_UP :  
             selectedItem--;  
             selectedItem--;  
Line 154: Line 181:
             }
             }
         break;
         break;
</syntaxhighlight>


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


<syntaxhighlight lang="C++">
     case TCODK_DOWN :  
     case TCODK_DOWN :  
     selectedItem = (selectedItem + 1) % items.size();  
     selectedItem = (selectedItem + 1) % items.size();  
break;
break;
</syntaxhighlight>


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


<syntaxhighlight lang="C++">
         case TCODK_ENTER : return items.get(selectedItem)->code;
         case TCODK_ENTER : return items.get(selectedItem)->code;
         default : break;
         default : break;
     }
     }
}
}
</syntaxhighlight>
Finally, if the player closes the game window, we return NONE :
Finally, if the player closes the game window, we return NONE :


<syntaxhighlight lang="C++">
     return NONE;
     return NONE;
}
}
</syntaxhighlight>


==The start game menu==
==The start game menu==
Line 176: Line 210:
Everything happens in the Engine::load function. We first create (or recreate) the menu :
Everything happens in the Engine::load function. We first create (or recreate) the menu :


engine.gui->menu.clear();
<syntaxhighlight lang="C++">
engine.gui->menu.addItem(Menu::NEW_GAME,"New game");
engine.gui->menu.clear();
if ( TCODSystem::fileExists("game.sav")) {
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::CONTINUE,"Continue");
}
}
engine.gui->menu.addItem(Menu::EXIT,"Exit");
engine.gui->menu.addItem(Menu::EXIT,"Exit");
</syntaxhighlight>


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


Menu::MenuItemCode menuItem=engine.gui->menu.pick();
<syntaxhighlight lang="C++">
Menu::MenuItemCode menuItem=engine.gui->menu.pick();
</syntaxhighlight>


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


if ( menuItem == Menu::EXIT || menuItem == Menu::NONE ) {
<syntaxhighlight lang="C++">
if ( menuItem == Menu::EXIT || menuItem == Menu::NONE ) {
     // Exit or window closed
     // Exit or window closed
     exit(0);
     exit(0);
</syntaxhighlight>


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.
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 ) {
<syntaxhighlight lang="C++">
} else if ( menuItem == Menu::NEW_GAME ) {
     // New game
     // New game
     engine.term();
     engine.term();
     engine.init();
     engine.init();
</syntaxhighlight>
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 :
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 :


<syntaxhighlight lang="C++">
     } else {
     } else {
         TCODZip zip;
         TCODZip zip;
Line 213: Line 256:
         gameStatus=STARTUP;
         gameStatus=STARTUP;
     }
     }
}
}
</syntaxhighlight>


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.
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.
Line 222: Line 266:
We handle the keypress events in Engine::update :
We handle the keypress events in Engine::update :


TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
<syntaxhighlight lang="C++" highlight="2-5">
     <span style="color:green">if ( lastKey.vk == TCODK_ESCAPE ) {
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
     if ( lastKey.vk == TCODK_ESCAPE ) {
         save();
         save();
         load();
         load();
     }</span>
     }
     player->update();
     player->update();
</syntaxhighlight>
That's it. First, we overwrite the saved game with the current game state (save) then call the load function to display the menu.
That's it. First, we overwrite the saved game with the current game state (save) then call the load function to display the menu.



Revision as of 10:06, 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'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.