This is part of a series of tutorials; the main page can be found here.
The tutorial uses libtcod version 1.6.0 and above.
Lots of stuff happens under the hood of a game that players donât really appreciate, like the combat mechanics detailed in the last couple of sections. Weâll now work on something much more flashy â the Graphical User Interface! Using the full power of libtcodâs true-color consoles, and a bit of creativity, you can make some truly amazing graphics. You may argue that the limitations of a console actually make it easier to create a polished game, rather than if you had the freedom to position per-pixel graphics like most other games.
Weâll start by creating a GUI panel at the bottom of the screen. Of course, youâre welcome to change this to suit your taste. For now, it will hold the playerâs health bar and a colored message log.
Itâs easier to manage GUI windows and panels with an off-screen console for each one, created before the main loop:
panel = libtcod.console_new(SCREEN_WIDTH, PANEL_HEIGHT)
The constant PANEL_HEIGHT is defined later, along with others. Letâs jump right to the âstatus barâ rendering code! This is fully generic and can be used for experience bars, mana bars, recharge times, dungeon level, you name it.
The bar has two parts, one rectangle that changes size according to the proportion between the value and the maximum value, and a background rectangle. It just takes a simple formula to calculate that size, and a few calls to libtcodâs console_rect function for the rectangles.
def render_bar(x, y, total_width, name, value, maximum, bar_color, back_color):
#render a bar (HP, experience, etc). first calculate the width of the bar
bar_width = int(float(value) / maximum * total_width)
#render the background first
libtcod.console_set_default_background(panel, back_color)
libtcod.console_rect(panel, x, y, total_width, 1, False, libtcod.BKGND_SCREEN)
#now render the bar on top
libtcod.console_set_default_background(panel, bar_color)
if bar_width > 0:
libtcod.console_rect(panel, x, y, bar_width, 1, False, libtcod.BKGND_SCREEN)
For extra clarity, the actual value and maximum are displayed as text over the bar, along with a caption (âHealthâ, âManaâ, etc).
#finally, some centered text with the values
libtcod.console_set_default_foreground(panel, libtcod.white)
libtcod.console_print_ex(panel, x + total_width / 2, y, libtcod.BKGND_NONE, libtcod.CENTER,
name + ': ' + str(value) + '/' + str(maximum))
Now weâll modify the main rendering function to use this. First, define a few constants: the height of the panel, its position on the screen (itâs a bottom panel so only the Y is needed) and the size of the health bar.
#sizes and coordinates relevant for the GUI
BAR_WIDTH = 20
PANEL_HEIGHT = 7
PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
I also changed MAP_HEIGHT to 43 to give the panel more room. At the end of render_all, replace the code that shows the playerâs stats as text with the following code. It re-initializes the panel to black, calls our render_bar function to display the playerâs health, then shows the panel on the root console.
#prepare to render the GUI panel
libtcod.console_set_default_background(panel, libtcod.black)
libtcod.console_clear(panel)
#show the player's stats
render_bar(1, 1, BAR_WIDTH, 'HP', player.fighter.hp, player.fighter.max_hp,
libtcod.light_red, libtcod.darker_red)
#blit the contents of "panel" to the root console
libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
Time to test it â that health bar looks pretty sweet! And you can easily make more like it with different colors and all.
A small detail, the console where the map is rendered (con) should be the size of the map, not the size of the screen. This is more noticeable now that the panel takes up quite a bit of space. Change SCREEN_WIDTH and SCREEN_HEIGHT to MAP_WIDTH and MAP_HEIGHT when creating this console and blitting it. Itâs the con = libtcod.console_new(âŚ) line before the main loop, and the first console_blit in render_all.
Until now the combat messages were dumped in the standard console â not very user-friendly. Weâll make a nice scrolling message log embedded in the GUI panel, and use colored messages so the player can know what happened with a single glance. It will also feature word-wrap!
The constants that define the message barâs position and size are:
MSG_X = BAR_WIDTH + 2
MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
MSG_HEIGHT = PANEL_HEIGHT - 1
This is so it appears to the right of the health bar, and fills up the rest of the space. The messages will be stored in a list so they can be easily manipulated. Each message is a tuple with 2 fields: the message string, and its color.
#create the list of game messages and their colors, starts empty
game_msgs = []
A simple-to-use function will handle adding messages to the list. It will use Pythonâs textwrap module to split a message into several lines if itâs too long! For that, put import textwrap at the top of the file, and create the function:
def message(new_msg, color = libtcod.white):
#split the message if necessary, among multiple lines
new_msg_lines = textwrap.wrap(new_msg, MSG_WIDTH)
for line in new_msg_lines:
#if the buffer is full, remove the first line to make room for the new one
if len(game_msgs) == MSG_HEIGHT:
del game_msgs[0]
#add the new line as a tuple, with the text and the color
game_msgs.append( (line, color) )
After obtaining the broken up message as a list of strings, it adds them one at a time to the actual message log. This is so that, when the log gets full, the first line is removed to make space for the new line. Dealing with one line at a time makes it easy to ensure that the message log never has more than a maximum height.
The code to show the message log is simpler. Just loop through the lines and print them with the appropriate colors (right before rendering the health bar). Notice how we get the values of the tuple right in the for loop; this sort of feature in Python (called unpacking) allows you to write very concise code.
#print the game messages, one line at a time
y = 1
for (line, color) in game_msgs:
libtcod.console_set_default_foreground(panel, color)
libtcod.console_print_ex(panel, MSG_X, y, libtcod.BKGND_NONE, libtcod.LEFT, line)
y += 1
Ready to test! Letâs print a friendly message before the main loop to welcome the player to our dungeon of doom:
#a warm welcoming message!
message('Welcome stranger! Prepare to perish in the Tombs of the Ancient Kings.', libtcod.red)
The long message allows us to test the word-wrap. You can now replace all the calls to the standard print with calls to our own message function (all 4 of them). I made the player death message red (libtcod.red), and the monster death message orange (libtcod.orange), others are the default. By the way, hereâs the list of standard libtcod colors. Itâs very handy, if you donât mind using a pre-defined palette of colors!
This is just the kind of polish that our game needed, itâs much more attractive, even for casual players. Donât let the die-hard roguelike players fool you, everyone likes a little bit of eye candy!
Weâll now work some interactivity into our GUI. Roguelikes have a long tradition of using strict keyboard interfaces, and thatâs nice; but for a couple of tasks, like selecting a tile, a mouse interface is much easier. So weâll implement something like a âlookâ command, by automatically showing the name of any object the player hovers the mouse with! You could also use it for selecting targets of spells and ranged combat. Of course this is only a tutorial, showing you what you can do, and you may decide to replace this with a traditional âlookâ command!
Using libtcod itâs very easy to know the position of the mouse, and if there were any clicks: the sys_check_for_event function returns information on both keyboard and mouse activity: (see the docs for more details, and here for the mouse structure).
We need to restructure the program a little bit to use this combined mouse and keyboard detection. Just before the main loop, add
mouse = libtcod.Mouse()
key = libtcod.Key()
and just after the main loop begins, but before the call to render_all, add
libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS|libtcod.EVENT_MOUSE,key,mouse)
Now, declare key as a global in handle_keys, and remove the line which calls libtcod.console_check_for_keypress.
Weâll access the cx and cy fields of the mouse structure, which are the coordinates of the tile (or cell) that the mouse is over. This goes in a new function, that is supposed to return a string with the names of objects under the mouse:
def get_names_under_mouse():
global mouse
#return a string with the names of all objects under the mouse
(x, y) = (mouse.cx, mouse.cy)
Now we need to gather a list of names of objects that satisfy a few conditions: theyâre under the mouse, and inside the playerâs FOV. (Otherwise he or she would be able to detect enemies through walls!) This can be done with list comprehensions, using the âifâ variant.
#create a list with the names of all objects at the mouse's coordinates and in FOV
names = [obj.name for obj in objects
if obj.x == x and obj.y == y and libtcod.map_is_in_fov(fov_map, obj.x, obj.y)]
After that mouthful, itâs a simple matter of joining the names into a single string, using commas. Python has neat functions for this and capitalizing the first letter:
names = ', '.join(names) #join the names, separated by commas
return names.capitalize()
The function render_all can call this to get the string that depends on the mouseâs position, after rendering the health bar:
#display names of objects under the mouse
libtcod.console_set_default_foreground(panel, libtcod.light_gray)
libtcod.console_print_ex(panel, 1, 0, libtcod.BKGND_NONE, libtcod.LEFT, get_names_under_mouse())
But wait! If you recall, in a turn-based game, the rendering is done only once per turn; the rest of the time, the game is blocked on console_wait_for_keypress. During this time (which is most of the time) the code we wrote above would simply not be processed! We switched to real-time rendering by replacing the console_wait_for_keypress call in handle_keys with the sys_check_for_event in the main loop.
Wonât our game stop being turn-based then? Itâs funny, but surprisingly it wonât! Before you question logic itself, let me tell you that we did some changes earlier that had the side-effect of enabling this.
When the player doesnât take a turn (doesnât press a movement/attack key), handle_keys returns a special string ( âdidnt-take-turnâ ). Youâll notice that the main loop only allows enemies to take their turns if the value returned from handle_keys is not âdidnt-take-turnâ ! The main loop goes on, but the monsters donât move. The only real distinction between a real-time game and a turn-based game is that, in a turn-based game, the monsters wait until the player moves to make their move. Makes sense!
If you hadnât before, you now need to call libtcod.sys_set_fps(LIMIT_FPS) before the main loop to limit the gameâs speed. Youâll also probably find the movement keys respond too rapidly for a turn-based game where every key press should count as only one turn (see the earlier discussion about this in Part 1). Just replace the calls to console_is_key_pressed in handle_keys with calls like:
if key.vk == libtcod.KEY_UP:
And similarly for the other movement keys. Thatâs it! You can move the mouse around to quickly know the names of every object in sight.
The whole code is available here.