Complete Roguelike Tutorial, using python+libtcod, part 7

From RogueBasin
Revision as of 16:36, 8 November 2010 by Teddy Leach (talk | contribs)
Jump to navigation Jump to search

This is part of a series of tutorials; the main page can be found here.

The GUI


Status bars

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_background_color(panel, back_color)
    libtcod.console_rect(panel, x, y, total_width, 1, False)
    
    #now render the bar on top
    libtcod.console_set_background_color(panel, bar_color)
    if bar_width > 0:
        libtcod.console_rect(panel, x, y, bar_width, 1, False)


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_foreground_color(panel, libtcod.white)
    libtcod.console_print_center(panel, x + total_width / 2, y, libtcod.BKGND_NONE,
        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 = 6
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_background_color(panel, libtcod.black)
    libtcod.console_clear(panel)
    
    #show the player's stats
    render_bar(1, 0, 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, MAP_WIDTH, PANEL_HEIGHT, 0, 0, SCREEN_HEIGHT-PANEL_HEIGHT)


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.


The message log

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


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) == PANEL_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 = 0
    for (line, color) in game_msgs:
        libtcod.console_set_foreground_color(panel, color)
        libtcod.console_print_left(panel, MSG_X, y, libtcod.BKGND_NONE, 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!


The whole code is available here.

Go on to the next part.