Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 7: the GUI"

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


class Gui {
<syntaxhighlight lang="C++">
public :
class Gui {
public :
     Gui();
     Gui();
     ~Gui();
     ~Gui();
     void render();
     void render();
 
protected :
protected :
     TCODConsole *con;
     TCODConsole *con;
   
   
Line 39: Line 40:
         float value, float maxValue, const TCODColor &barColor,
         float value, float maxValue, const TCODColor &barColor,
         const TCODColor &backColor);
         const TCODColor &backColor);
};
};
</syntaxhighlight>


We use a constructor to allocate the GUI console, a destructor to delete it. The render function will be called by the Engine and will use the renderBar utility to draw the health bar.
We use a constructor to allocate the GUI console, a destructor to delete it. The render function will be called by the Engine and will use the renderBar utility to draw the health bar.
Line 46: Line 48:
Engine.hpp :
Engine.hpp :


int screenWidth;
<syntaxhighlight lang="C++" highlight="3">
int screenHeight;
int screenWidth;
<span style="color:green">Gui *gui;</span>
int screenHeight;
Gui *gui;
Engine(int screenWidth, int screenHeight);
 
Engine(int screenWidth, int screenHeight);
</syntaxhighlight>


We could have used a non pointer field :
We could have used a non pointer field :


Gui gui;
<syntaxhighlight lang="C++">
Gui gui;
</syntaxhighlight>
but as you will see below, we need engine.screenHeight to be initialized when the constructor of Gui is called. That's why we're allocating the Gui field dynamically :
but as you will see below, we need engine.screenHeight to be initialized when the constructor of Gui is called. That's why we're allocating the Gui field dynamically :


Engine constructor :
Engine constructor :


map = new Map(80,43);
<syntaxhighlight lang="C++">
gui = new Gui();
map = new Map(80,43);
gui = new Gui();
</syntaxhighlight>
We're going to use 7 lines for the GUI : one for the mouse look description and 6 for the log. That's why we slightly reduced the map's height from 45 cells to 43.
We're going to use 7 lines for the GUI : one for the mouse look description and 6 for the log. That's why we slightly reduced the map's height from 45 cells to 43.


Of course, don't forget to delete gui in the Engine's destructor :
Of course, don't forget to delete gui in the Engine's destructor :


Engine::~Engine() {
<syntaxhighlight lang="C++" highlight="4">
Engine::~Engine() {
     actors.clearAndDelete();
     actors.clearAndDelete();
     delete map;
     delete map;
     <span style="color:green">delete gui;</span>
     delete gui;
}
}
</syntaxhighlight>
and call gui->render() in the Engine::render method :
and call gui->render() in the Engine::render method :


Engine::render :
Engine::render :


player->render();
<syntaxhighlight lang="C++" highlight="3">
// show the player's stats
player->render();
<span style="color:green">gui->render();</span>
// show the player's stats
gui->render();
</syntaxhighlight>
Ok now we can start the GUI implementation.
Ok now we can start the GUI implementation.


static const int PANEL_HEIGHT=7;
<syntaxhighlight lang="C++">
static const int BAR_WIDTH=20;
static const int PANEL_HEIGHT=7;
static const int BAR_WIDTH=20;
</syntaxhighlight>


PANEL_HEIGHT is the height of the GUI console. BAR_WIDTH the width of the player health bar.
PANEL_HEIGHT is the height of the GUI console. BAR_WIDTH the width of the player health bar.
Line 86: Line 100:
The constructor allocates the console, the destructor deletes it :
The constructor allocates the console, the destructor deletes it :


Gui::Gui() {
<syntaxhighlight lang="C++">
Gui::Gui() {
     con = new TCODConsole(engine.screenWidth,PANEL_HEIGHT);
     con = new TCODConsole(engine.screenWidth,PANEL_HEIGHT);
}
}
 
Gui::~Gui() {
Gui::~Gui() {
     delete con;
     delete con;
}
}
</syntaxhighlight>
First, the render function fills the console with black (to erase the content that was drawn in the previous frame) :
First, the render function fills the console with black (to erase the content that was drawn in the previous frame) :


void Gui::render() {
<syntaxhighlight lang="C++">
void Gui::render() {
     // clear the GUI console
     // clear the GUI console
     con->setDefaultBackground(TCODColor::black);
     con->setDefaultBackground(TCODColor::black);
     con->clear();
     con->clear();
</syntaxhighlight>
Then we draw the health bar using the renderBar function :
Then we draw the health bar using the renderBar function :


// draw the health bar
<syntaxhighlight lang="C++">
renderBar(1,1,BAR_WIDTH,"HP",engine.player->destructible->hp,
// draw the health bar
renderBar(1,1,BAR_WIDTH,"HP",engine.player->destructible->hp,
     engine.player->destructible->maxHp,
     engine.player->destructible->maxHp,
     TCODColor::lightRed,TCODColor::darkerRed);
     TCODColor::lightRed,TCODColor::darkerRed);
</syntaxhighlight>
And finally, we blit the console on the game window :
And finally, we blit the console on the game window :


// blit the GUI console on the root console
<syntaxhighlight lang="C++">
TCODConsole::blit(con,0,0,engine.screenWidth,PANEL_HEIGHT,
// blit the GUI console on the root console
TCODConsole::blit(con,0,0,engine.screenWidth,PANEL_HEIGHT,
     TCODConsole::root,0,engine.screenHeight-PANEL_HEIGHT);
     TCODConsole::root,0,engine.screenHeight-PANEL_HEIGHT);
</syntaxhighlight>
And now the most important : the renderBar function.
And now the most important : the renderBar function.


void Gui::renderBar(int x, int y, int width, const char *name,
<syntaxhighlight lang="C++">
void Gui::renderBar(int x, int y, int width, const char *name,
     float value, float maxValue, const TCODColor &barColor,
     float value, float maxValue, const TCODColor &barColor,
     const TCODColor &backColor) {
     const TCODColor &backColor) {
</syntaxhighlight>
First, we're filling the bar with the background color :
First, we're filling the bar with the background color :


// fill the background
<syntaxhighlight lang="C++">
con->setDefaultBackground(backColor);
// fill the background
con->rect(x,y,width,1,false,TCOD_BKGND_SET);
con->setDefaultBackground(backColor);
con->rect(x,y,width,1,false,TCOD_BKGND_SET);
</syntaxhighlight>
Then we're computing how much of the bar should be filled with the bar color :
Then we're computing how much of the bar should be filled with the bar color :


int barWidth = (int)(value / maxValue * width);
<syntaxhighlight lang="C++">
if ( barWidth > 0 ) {
int barWidth = (int)(value / maxValue * width);
if ( barWidth > 0 ) {
     // draw the bar
     // draw the bar
     con->setDefaultBackground(barColor);
     con->setDefaultBackground(barColor);
     con->rect(x,y,barWidth,1,false,TCOD_BKGND_SET);
     con->rect(x,y,barWidth,1,false,TCOD_BKGND_SET);
}
}
</syntaxhighlight>
We're also writing the values on top of the bar, using the printEx function to center the text :     
We're also writing the values on top of the bar, using the printEx function to center the text :     


<syntaxhighlight lang="C++">
     // print text on top of the bar
     // print text on top of the bar
     con->setDefaultForeground(TCODColor::white);
     con->setDefaultForeground(TCODColor::white);
     con->printEx(x+width/2,y,TCOD_BKGND_NONE,TCOD_CENTER,
     con->printEx(x+width/2,y,TCOD_BKGND_NONE,TCOD_CENTER,
         "%s : %g/%g", name, value, maxValue);
         "%s : %g/%g", name, value, maxValue);
}
}
</syntaxhighlight>


==The message log==
==The message log==
Line 140: Line 170:
We want a handy function to write to the log. Let's add it to Gui.hpp :
We want a handy function to write to the log. Let's add it to Gui.hpp :


void message(const TCODColor &col, const char *text, ...);
<syntaxhighlight lang="C++">
void message(const TCODColor &col, const char *text, ...);
</syntaxhighlight>


This is our first variadic function. The three dots in the function prototype mean that there might be more parameters following the text parameter. If fact, we already used such a function when we sent messages to the standard output :
This is our first variadic function. The three dots in the function prototype mean that there might be more parameters following the text parameter. If fact, we already used such a function when we sent messages to the standard output :


printf("%s attacks %s for %g hit points.", owner->name, target->name, damage);
<syntaxhighlight lang="C++">
printf("%s attacks %s for %g hit points.", owner->name, target->name, damage);
</syntaxhighlight>


We did not use the C++ cout stream because it's now very easy to move the logs to the game window. Simple replace the printf calls with :
We did not use the C++ cout stream because it's now very easy to move the logs to the game window. Simple replace the printf calls with :


engine.gui->message(TCODColor::white,
<syntaxhighlight lang="C++">
engine.gui->message(TCODColor::white,
     "%s attacks %s for %g hit points.", owner->name, target->name, damage);
     "%s attacks %s for %g hit points.", owner->name, target->name, damage);
</syntaxhighlight>


We want to be able to define the color of each line in the log. So we need a structure to store the message's text and its color. Since this structure is only used by the Gui class, we put it in its protected declaration zone.
We want to be able to define the color of each line in the log. So we need a structure to store the message's text and its color. Since this structure is only used by the Gui class, we put it in its protected declaration zone.


  protected :
<syntaxhighlight lang="C++" highlight="3-9">
protected :
     TCODConsole *con;
     TCODConsole *con;
     <span style="color:green">struct Message {
     struct Message {
         char *text;
         char *text;
         TCODColor col;
         TCODColor col;
Line 161: Line 198:
         ~Message();
         ~Message();
     };
     };
     TCODList<Message *> log;</span>
     TCODList<Message *> log;
</syntaxhighlight>


That's all for the log's declaration. Let's implement it. Log messages will be dynamically allocated so we need to clean them in the destructor :
That's all for the log's declaration. Let's implement it. Log messages will be dynamically allocated so we need to clean them in the destructor :


Gui::~Gui() {
<syntaxhighlight lang="C++">
Gui::~Gui() {
     delete con;
     delete con;
     log.clearAndDelete();
     log.clearAndDelete();
}
}
</syntaxhighlight>


We also define a constant for the x position of the log messages (after the health bar) and how many log lines we can print (the size of the GUI console minus one for the mouse look line):
We also define a constant for the x position of the log messages (after the health bar) and how many log lines we can print (the size of the GUI console minus one for the mouse look line):


static const int MSG_X=BAR_WIDTH+2;
<syntaxhighlight lang="C++">
static const int MSG_HEIGHT=PANEL_HEIGHT-1;
static const int MSG_X=BAR_WIDTH+2;
static const int MSG_HEIGHT=PANEL_HEIGHT-1;
</syntaxhighlight>


In the render function, we draw the log just after the health bar :
In the render function, we draw the log just after the health bar :


// draw the message log
<syntaxhighlight lang="C++">
int y=1;
// draw the message log
for (Message **it=log.begin(); it != log.end(); it++) {
int y=1;
for (Message **it=log.begin(); it != log.end(); it++) {
     Message *message=*it;
     Message *message=*it;
     con->setDefaultForeground(message->col);
     con->setDefaultForeground(message->col);
     con->print(MSG_X,y,message->text);
     con->print(MSG_X,y,message->text);
     y++;
     y++;
}
}
</syntaxhighlight>


We can add a slight improvement : darken the oldest lines to give a sense of fading. One way to darken a TCODColor is to multiply it with a float < 1.0.
We can add a slight improvement : darken the oldest lines to give a sense of fading. One way to darken a TCODColor is to multiply it with a float < 1.0.


// draw the message log
<syntaxhighlight lang="C++" highlight="3,6,9-10">
int y=1;
// draw the message log
<span style="color:green">float colorCoef=0.4f;</span>
int y=1;
for (Message **it=log.begin(); it != log.end(); it++) {
float colorCoef=0.4f;
for (Message **it=log.begin(); it != log.end(); it++) {
     Message *message=*it;
     Message *message=*it;
     <span style="color:green">con->setDefaultForeground(message->col * colorCoef);</span>
     con->setDefaultForeground(message->col * colorCoef);
     con->print(MSG_X,y,message->text);
     con->print(MSG_X,y,message->text);
     y++;
     y++;
     <span style="color:green">if ( colorCoef < 1.0f ) {
     if ( colorCoef < 1.0f ) {
         colorCoef+=0.3f;</span>
         colorCoef+=0.3f;
     }
     }
}
}
</syntaxhighlight>


The oldest line will have 40% luminosity, the second oldest 70% and all other 100%.
The oldest line will have 40% luminosity, the second oldest 70% and all other 100%.
Line 205: Line 251:
Now we need to define the Gui::Message constructors and destructors :
Now we need to define the Gui::Message constructors and destructors :


Gui::Message::Message(const char *text, const TCODColor &col) :
<syntaxhighlight lang="C++">
Gui::Message::Message(const char *text, const TCODColor &col) :
     text(strdup(text)),col(col) {     
     text(strdup(text)),col(col) {     
}
}
 
Gui::Message::~Message() {
Gui::Message::~Message() {
     free(text);
     free(text);
}  
}
</syntaxhighlight>
We're using the standard C strdup function to duplicate the text variable. Since this function allocates memory using the C function malloc, we need to release it with the C function free. If we want to use only C++, we would have to do :
We're using the standard C strdup function to duplicate the text variable. Since this function allocates memory using the C function malloc, we need to release it with the C function free. If we want to use only C++, we would have to do :


Gui::Message::Message(const char *text, const TCODColor &col) :
<syntaxhighlight lang="C++">
Gui::Message::Message(const char *text, const TCODColor &col) :
     col(col) {
     col(col) {
     this->text = new char[strlen(text)];
     this->text = new char[strlen(text)];
     strcpy(this->text,text);
     strcpy(this->text,text);
}
}
 
Gui::Message::~Message() {
Gui::Message::~Message() {
     delete [] text;
     delete [] text;
}
}
</syntaxhighlight>
which is a bit more complex. Another way would be to store the text in a std::string object.
which is a bit more complex. Another way would be to store the text in a std::string object.


The core of the log is obviously the Gui::message function :
The core of the log is obviously the Gui::message function :


void Gui::message(const TCODColor &col, const char *text, ...) {
<syntaxhighlight lang="C++">
void Gui::message(const TCODColor &col, const char *text, ...) {
     // build the text
     // build the text
     va_list ap;
     va_list ap;
Line 234: Line 285:
     vsprintf(buf,text,ap);
     vsprintf(buf,text,ap);
     va_end(ap);
     va_end(ap);
</syntaxhighlight>
This is the black magic making it possible to handle those unknown parameters. It requires the stdarg.h standard header that defines the va_list type.    va_start is used to initialize the va_list parameter list, using the name of the last named parameter. The parameters in the va_list variable can then be scanned using the va_arg function, but we don't need it because vsprintf will do it for us, formatting the message and writing it in the buf array (you remember that a C string is an array of char, don't you?). va_end is called to clean up the mess in the stack.
This is the black magic making it possible to handle those unknown parameters. It requires the stdarg.h standard header that defines the va_list type.    va_start is used to initialize the va_list parameter list, using the name of the last named parameter. The parameters in the va_list variable can then be scanned using the va_arg function, but we don't need it because vsprintf will do it for us, formatting the message and writing it in the buf array (you remember that a C string is an array of char, don't you?). va_end is called to clean up the mess in the stack.
You don't have to understand all the dirty tricks behind C variadic functions, but if you want to know more, check [http://linux.die.net/man/3/va_end the man page].
You don't have to understand all the dirty tricks behind C variadic functions, but if you want to know more, check [http://linux.die.net/man/3/va_end the man page].
Line 241: Line 293:
For this, we need a pointer to the beginning and the end of each line :
For this, we need a pointer to the beginning and the end of each line :


char *lineBegin=buf;
<syntaxhighlight lang="C++">
char *lineEnd;
char *lineBegin=buf;
char *lineEnd;
</syntaxhighlight>


Before writing a new line in the log, we check that there is some room. If there isn't, we remove the oldest message :
Before writing a new line in the log, we check that there is some room. If there isn't, we remove the oldest message :


do {
<syntaxhighlight lang="C++">
do {
     // make room for the new message
     // make room for the new message
     if ( log.size() == MSG_HEIGHT ) {
     if ( log.size() == MSG_HEIGHT ) {
Line 253: Line 308:
         delete toRemove;
         delete toRemove;
     }
     }
</syntaxhighlight>
Now we're looking for a \n character in our line using the standard C strchr function :
Now we're looking for a \n character in our line using the standard C strchr function :


// detect end of the line
<syntaxhighlight lang="C++">
lineEnd=strchr(lineBegin,'\n');
// detect end of the line
lineEnd=strchr(lineBegin,'\n');
</syntaxhighlight>


If such a character is found (lineEnd is not NULL), we replace it with '\0' or 0 which is the end-of-string character for C strings.
If such a character is found (lineEnd is not NULL), we replace it with '\0' or 0 which is the end-of-string character for C strings.


if ( lineEnd ) {
<syntaxhighlight lang="C++">
if ( lineEnd ) {
     *lineEnd='\0';
     *lineEnd='\0';
}
}
</syntaxhighlight>
Now that the string is splitted, we can add the first part in the log :
Now that the string is splitted, we can add the first part in the log :


// add a new message to the log
<syntaxhighlight lang="C++">
Message *msg=new Message(lineBegin, col);
// add a new message to the log
log.push(msg);
Message *msg=new Message(lineBegin, col);
log.push(msg);
</syntaxhighlight>


And then we can loop, starting at the character just after the \n :
And then we can loop, starting at the character just after the \n :


<syntaxhighlight lang="C++">
         // go to next line
         // go to next line
         lineBegin=lineEnd+1;
         lineBegin=lineEnd+1;
     } while ( lineEnd );
     } while ( lineEnd );
}
}
</syntaxhighlight>
The loop ends when (or if) no \n is found.
The loop ends when (or if) no \n is found.


Line 281: Line 345:
Ai.cpp:
Ai.cpp:


engine.gui->message(TCODColor::lightGrey,"There's a %s here",actor->name);
<syntaxhighlight lang="C++">
engine.gui->message(TCODColor::lightGrey,"There's a %s here",actor->name);
</syntaxhighlight>


Attacker.cpp :
Attacker.cpp :


engine.gui->message(owner==engine.player ? TCODColor::red : TCODColor::lightGrey,
<syntaxhighlight lang="C++">
engine.gui->message(owner==engine.player ? TCODColor::red : TCODColor::lightGrey,
     "%s attacks %s for %g hit points.", owner->name, target->name,
     "%s attacks %s for %g hit points.", owner->name, target->name,
     power - target->destructible->defense);
     power - target->destructible->defense);
engine.gui->message(TCODColor::lightGrey,
engine.gui->message(TCODColor::lightGrey,
     "%s attacks %s but it has no effect!", owner->name, target->name);           
     "%s attacks %s but it has no effect!", owner->name, target->name);           
engine.gui->message(TCODColor::lightGrey,
engine.gui->message(TCODColor::lightGrey,
     "%s attacks %s in vain.",owner->name,target->name);
     "%s attacks %s in vain.",owner->name,target->name);
</syntaxhighlight>
Destructible.cpp:
Destructible.cpp:


engine.gui->message(TCODColor::lightGrey,"%s is dead",owner->name);
<syntaxhighlight lang="C++">
engine.gui->message(TCODColor::red,"You died!");
engine.gui->message(TCODColor::lightGrey,"%s is dead",owner->name);
engine.gui->message(TCODColor::red,"You died!");
</syntaxhighlight>


I also added a welcome message in the Engine constructor :
I also added a welcome message in the Engine constructor :


gui->message(TCODColor::red,
<syntaxhighlight lang="C++">
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.");
</syntaxhighlight>


Note the \n character. The whole string wouldn't fit in a single line.
Note the \n character. The whole string wouldn't fit in a single line.
Line 308: Line 380:
For this, we need to know the mouse's cursor coordinates. Let's add a TCOD_mouse_t field in the Engine :
For this, we need to know the mouse's cursor coordinates. Let's add a TCOD_mouse_t field in the Engine :


TCOD_key_t lastKey;
<syntaxhighlight lang="C++" highlight="2">
<span style="color:green">TCOD_mouse_t mouse;</span>
TCOD_key_t lastKey;
TCODList<Actor *> actors;
TCOD_mouse_t mouse;
TCODList<Actor *> actors;
</syntaxhighlight>


To get the mouse position, we update the checkForEvent call in Engine::update :
To get the mouse position, we update the checkForEvent call in Engine::update :


TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
<syntaxhighlight lang="C++">
TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);
</syntaxhighlight>
   
   
We want to print what's behind the mouse cursor, but only when it's in the player's field of view. The problem is that the Map::isInFov function does not handle out of the map values for its x,y parameters. Let's fix that :
We want to print what's behind the mouse cursor, but only when it's in the player's field of view. The problem is that the Map::isInFov function does not handle out of the map values for its x,y parameters. Let's fix that :


bool Map::isInFov(int x, int y) const {
<syntaxhighlight lang="C++" highlight="2-4">
     <span style="color:green">if ( x < 0 || x >= width || y < 0 || y >= height ) {
bool Map::isInFov(int x, int y) const {
     if ( x < 0 || x >= width || y < 0 || y >= height ) {
         return false;
         return false;
     }</span>
     }
     if ( map->isInFov(x,y) ) {
     if ( map->isInFov(x,y) ) {
</syntaxhighlight>


Now all we have to do is to add a protected renderMouseLook function in Gui.hpp:
Now all we have to do is to add a protected renderMouseLook function in Gui.hpp:


void renderMouseLook();
<syntaxhighlight lang="C++">
void renderMouseLook();
</syntaxhighlight>


And implement it. First let's ditch the case where the mouse cursor is not in FOV :
And implement it. First let's ditch the case where the mouse cursor is not in FOV :


void Gui::renderMouseLook() {
<syntaxhighlight lang="C++">
void Gui::renderMouseLook() {
     if (! engine.map->isInFov(engine.mouse.cx, engine.mouse.cy)) {
     if (! engine.map->isInFov(engine.mouse.cx, engine.mouse.cy)) {
         // if mouse is out of fov, nothing to render
         // if mouse is out of fov, nothing to render
         return;
         return;
     }
     }
</syntaxhighlight>
We're going to simply write a comma separated list of everything on the cell. Once again, you can use a std::string object, which is easier (but less performant) to manipulate. Let's create a buffer for that string :
We're going to simply write a comma separated list of everything on the cell. Once again, you can use a std::string object, which is easier (but less performant) to manipulate. Let's create a buffer for that string :


char buf[128]="";
<syntaxhighlight lang="C++">
char buf[128]="";
</syntaxhighlight>


This is equivalent to :
This is equivalent to :


char buf[128]={'\0'};
<syntaxhighlight lang="C++">
char buf[128]={'\0'};
</syntaxhighlight>


or
or


char buf[128];
<syntaxhighlight lang="C++">
buf[0]=0;
char buf[128];
buf[0]=0;
</syntaxhighlight>


Setting the first char of the array to 0 means it's an empty string.     
Setting the first char of the array to 0 means it's an empty string.     
Line 352: Line 440:
Let's scan the actors list and add the name of every actor on the mouse cell in buf :
Let's scan the actors list and add the name of every actor on the mouse cell in buf :


bool first=true;
<syntaxhighlight lang="C++">
for (Actor **it=engine.actors.begin(); it != engine.actors.end(); it++) {
bool first=true;
for (Actor **it=engine.actors.begin(); it != engine.actors.end(); it++) {
     Actor *actor=*it;
     Actor *actor=*it;
     // find actors under the mouse cursor
     // find actors under the mouse cursor
Line 364: Line 453:
         strcat(buf,actor->name);
         strcat(buf,actor->name);
     }
     }
}
}
</syntaxhighlight>


Finally, write this line in the first line of the GUI console.     
Finally, write this line in the first line of the GUI console.     


<syntaxhighlight lang="C++">
     // display the list of actors under the mouse cursor
     // display the list of actors under the mouse cursor
     con->setDefaultForeground(TCODColor::lightGrey);
     con->setDefaultForeground(TCODColor::lightGrey);
     con->print(1,0,buf);
     con->print(1,0,buf);
}
}
</syntaxhighlight>


Don't forget to call renderMouseLook in Gui::render, just before blitting the console :
Don't forget to call renderMouseLook in Gui::render, just before blitting the console :


// mouse look
<syntaxhighlight lang="C++">
renderMouseLook();
// mouse look
renderMouseLook();
</syntaxhighlight>


That's it. You can compile as usual and enjoy your new shiny GUI.
That's it. You can compile as usual and enjoy your new shiny GUI.

Latest revision as of 07:54, 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.


After the longish article 6 about melee fighting, we're going to take a break and do something easier.

  • First, we're going to slightly improve the player health bar with some visual fluff.
  • We will also move the game log from the standard output to the game window.
  • Finally, we will start to use the mouse and implement a mouse look feature to display information about what is under the mouse cursor.

These are three very distinct features but for the sake of simplicity, we will stuff everything in a single Gui class (for Graphical User Interface).

View source here

libtcod functions used in this article

TCODConsole::TCODConsole

TCODConsole::blit

TCODConsole::rect

TCODConsole::printEx

A shiny health bar

To make it easier to change the GUI position, we're going to render everything on an offscreen console. So let's create a Gui header with everything we need :

Gui.hpp

class Gui {
public :
    Gui();
    ~Gui();
    void render();

protected :
    TCODConsole *con;
 
    void renderBar(int x, int y, int width, const char *name,
        float value, float maxValue, const TCODColor &barColor,
        const TCODColor &backColor);
};

We use a constructor to allocate the GUI console, a destructor to delete it. The render function will be called by the Engine and will use the renderBar utility to draw the health bar. Before we start the implementation, let's update the Engine :

Engine.hpp :

int screenWidth;
int screenHeight;
Gui *gui;

Engine(int screenWidth, int screenHeight);

We could have used a non pointer field :

Gui gui;

but as you will see below, we need engine.screenHeight to be initialized when the constructor of Gui is called. That's why we're allocating the Gui field dynamically :

Engine constructor :

map = new Map(80,43);
gui = new Gui();

We're going to use 7 lines for the GUI : one for the mouse look description and 6 for the log. That's why we slightly reduced the map's height from 45 cells to 43.

Of course, don't forget to delete gui in the Engine's destructor :

Engine::~Engine() {
    actors.clearAndDelete();
    delete map;
    delete gui;
}

and call gui->render() in the Engine::render method :

Engine::render :

player->render();
// show the player's stats
gui->render();

Ok now we can start the GUI implementation.

static const int PANEL_HEIGHT=7;
static const int BAR_WIDTH=20;

PANEL_HEIGHT is the height of the GUI console. BAR_WIDTH the width of the player health bar.

The constructor allocates the console, the destructor deletes it :

Gui::Gui() {
    con = new TCODConsole(engine.screenWidth,PANEL_HEIGHT);
}

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

First, the render function fills the console with black (to erase the content that was drawn in the previous frame) :

void Gui::render() {
    // clear the GUI console
    con->setDefaultBackground(TCODColor::black);
    con->clear();

Then we draw the health bar using the renderBar function :

// draw the health bar
renderBar(1,1,BAR_WIDTH,"HP",engine.player->destructible->hp,
    engine.player->destructible->maxHp,
    TCODColor::lightRed,TCODColor::darkerRed);

And finally, we blit the console on the game window :

// blit the GUI console on the root console
TCODConsole::blit(con,0,0,engine.screenWidth,PANEL_HEIGHT,
    TCODConsole::root,0,engine.screenHeight-PANEL_HEIGHT);

And now the most important : the renderBar function.

void Gui::renderBar(int x, int y, int width, const char *name,
    float value, float maxValue, const TCODColor &barColor,
    const TCODColor &backColor) {

First, we're filling the bar with the background color :

// fill the background
con->setDefaultBackground(backColor);
con->rect(x,y,width,1,false,TCOD_BKGND_SET);

Then we're computing how much of the bar should be filled with the bar color :

int barWidth = (int)(value / maxValue * width);
if ( barWidth > 0 ) {
    // draw the bar
    con->setDefaultBackground(barColor);
    con->rect(x,y,barWidth,1,false,TCOD_BKGND_SET);
}

We're also writing the values on top of the bar, using the printEx function to center the text :

    // print text on top of the bar
    con->setDefaultForeground(TCODColor::white);
    con->printEx(x+width/2,y,TCOD_BKGND_NONE,TCOD_CENTER,
        "%s : %g/%g", name, value, maxValue);
}

The message log

We want a handy function to write to the log. Let's add it to Gui.hpp :

void message(const TCODColor &col, const char *text, ...);

This is our first variadic function. The three dots in the function prototype mean that there might be more parameters following the text parameter. If fact, we already used such a function when we sent messages to the standard output :

printf("%s attacks %s for %g hit points.", owner->name, target->name, damage);

We did not use the C++ cout stream because it's now very easy to move the logs to the game window. Simple replace the printf calls with :

engine.gui->message(TCODColor::white,
    "%s attacks %s for %g hit points.", owner->name, target->name, damage);

We want to be able to define the color of each line in the log. So we need a structure to store the message's text and its color. Since this structure is only used by the Gui class, we put it in its protected declaration zone.

protected :
    TCODConsole *con;
    struct Message {
        char *text;
        TCODColor col;
        Message(const char *text, const TCODColor &col);
        ~Message();
    };
    TCODList<Message *> log;

That's all for the log's declaration. Let's implement it. Log messages will be dynamically allocated so we need to clean them in the destructor :

Gui::~Gui() {
    delete con;
    log.clearAndDelete();
}

We also define a constant for the x position of the log messages (after the health bar) and how many log lines we can print (the size of the GUI console minus one for the mouse look line):

static const int MSG_X=BAR_WIDTH+2;
static const int MSG_HEIGHT=PANEL_HEIGHT-1;

In the render function, we draw the log just after the health bar :

// draw the message log
int y=1;
for (Message **it=log.begin(); it != log.end(); it++) {
    Message *message=*it;
    con->setDefaultForeground(message->col);
    con->print(MSG_X,y,message->text);
    y++;
}

We can add a slight improvement : darken the oldest lines to give a sense of fading. One way to darken a TCODColor is to multiply it with a float < 1.0.

// draw the message log
int y=1;
float colorCoef=0.4f;
for (Message **it=log.begin(); it != log.end(); it++) {
    Message *message=*it;
    con->setDefaultForeground(message->col * colorCoef);
    con->print(MSG_X,y,message->text);
    y++;
    if ( colorCoef < 1.0f ) {
        colorCoef+=0.3f;
    }
}

The oldest line will have 40% luminosity, the second oldest 70% and all other 100%.

Now we need to define the Gui::Message constructors and destructors :

Gui::Message::Message(const char *text, const TCODColor &col) :
    text(strdup(text)),col(col) {    
}

Gui::Message::~Message() {
    free(text);
}

We're using the standard C strdup function to duplicate the text variable. Since this function allocates memory using the C function malloc, we need to release it with the C function free. If we want to use only C++, we would have to do :

Gui::Message::Message(const char *text, const TCODColor &col) :
    col(col) {
    this->text = new char[strlen(text)];
    strcpy(this->text,text);
}

Gui::Message::~Message() {
    delete [] text;
}

which is a bit more complex. Another way would be to store the text in a std::string object.

The core of the log is obviously the Gui::message function :

void Gui::message(const TCODColor &col, const char *text, ...) {
    // build the text
    va_list ap;
    char buf[128];
    va_start(ap,text);
    vsprintf(buf,text,ap);
    va_end(ap);

This is the black magic making it possible to handle those unknown parameters. It requires the stdarg.h standard header that defines the va_list type. va_start is used to initialize the va_list parameter list, using the name of the last named parameter. The parameters in the va_list variable can then be scanned using the va_arg function, but we don't need it because vsprintf will do it for us, formatting the message and writing it in the buf array (you remember that a C string is an array of char, don't you?). va_end is called to clean up the mess in the stack. You don't have to understand all the dirty tricks behind C variadic functions, but if you want to know more, check the man page.

Now a high quality log would do line wrapping for us, but to reduce this article length, we suppose that messages won't wrap : it's up to the Gui::message caller to ensure a line never reach the right border of the console. To avoid headache, we still offer some help : being able to put carriage returns '\n' to write several messages with a single Gui::message call.

For this, we need a pointer to the beginning and the end of each line :

char *lineBegin=buf;
char *lineEnd;

Before writing a new line in the log, we check that there is some room. If there isn't, we remove the oldest message :

do {
    // make room for the new message
    if ( log.size() == MSG_HEIGHT ) {
        Message *toRemove=log.get(0);
        log.remove(toRemove);
        delete toRemove;
    }

Now we're looking for a \n character in our line using the standard C strchr function :

// detect end of the line
lineEnd=strchr(lineBegin,'\n');

If such a character is found (lineEnd is not NULL), we replace it with '\0' or 0 which is the end-of-string character for C strings.

if ( lineEnd ) {
    *lineEnd='\0';
}

Now that the string is splitted, we can add the first part in the log :

// add a new message to the log
Message *msg=new Message(lineBegin, col);
log.push(msg);

And then we can loop, starting at the character just after the \n :

        // go to next line
        lineBegin=lineEnd+1;
    } while ( lineEnd );
}

The loop ends when (or if) no \n is found.

Now it's time to replace all the printf commands with engine.gui->message. You'll have to choose a color for each message. Here are all the calls updated :

Ai.cpp:

engine.gui->message(TCODColor::lightGrey,"There's a %s here",actor->name);

Attacker.cpp :

engine.gui->message(owner==engine.player ? TCODColor::red : TCODColor::lightGrey,
    "%s attacks %s for %g hit points.", owner->name, target->name,
    power - target->destructible->defense);
engine.gui->message(TCODColor::lightGrey,
    "%s attacks %s but it has no effect!", owner->name, target->name);           
engine.gui->message(TCODColor::lightGrey,
    "%s attacks %s in vain.",owner->name,target->name);

Destructible.cpp:

engine.gui->message(TCODColor::lightGrey,"%s is dead",owner->name);
engine.gui->message(TCODColor::red,"You died!");

I also added a welcome message in the Engine constructor :

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

Note the \n character. The whole string wouldn't fit in a single line.

Mouse look

For this, we need to know the mouse's cursor coordinates. Let's add a TCOD_mouse_t field in the Engine :

TCOD_key_t lastKey;
TCOD_mouse_t mouse;
TCODList<Actor *> actors;

To get the mouse position, we update the checkForEvent call in Engine::update :

TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS|TCOD_EVENT_MOUSE,&lastKey,&mouse);

We want to print what's behind the mouse cursor, but only when it's in the player's field of view. The problem is that the Map::isInFov function does not handle out of the map values for its x,y parameters. Let's fix that :

bool Map::isInFov(int x, int y) const {
    if ( x < 0 || x >= width || y < 0 || y >= height ) {
        return false;
    }
    if ( map->isInFov(x,y) ) {

Now all we have to do is to add a protected renderMouseLook function in Gui.hpp:

void renderMouseLook();

And implement it. First let's ditch the case where the mouse cursor is not in FOV :

void Gui::renderMouseLook() {
    if (! engine.map->isInFov(engine.mouse.cx, engine.mouse.cy)) {
        // if mouse is out of fov, nothing to render
        return;
    }

We're going to simply write a comma separated list of everything on the cell. Once again, you can use a std::string object, which is easier (but less performant) to manipulate. Let's create a buffer for that string :

char buf[128]="";

This is equivalent to :

char buf[128]={'\0'};

or

char buf[128];
buf[0]=0;

Setting the first char of the array to 0 means it's an empty string.

Let's scan the actors list and add the name of every actor on the mouse cell in buf :

bool first=true;
for (Actor **it=engine.actors.begin(); it != engine.actors.end(); it++) {
    Actor *actor=*it;
    // find actors under the mouse cursor
    if (actor->x == engine.mouse.cx && actor->y == engine.mouse.cy ) {
        if (! first) {
            strcat(buf,", ");
        } else {
            first=false;
        }
        strcat(buf,actor->name);
    }
}

Finally, write this line in the first line of the GUI console.

    // display the list of actors under the mouse cursor
    con->setDefaultForeground(TCODColor::lightGrey);
    con->print(1,0,buf);
}

Don't forget to call renderMouseLook in Gui::render, just before blitting the console :

// mouse look
renderMouseLook();

That's it. You can compile as usual and enjoy your new shiny GUI.