A scrolling demo (a roguelike in Python #2)

So after making a simple @ walking demo with libtcod, I took a look at it again and thought, you know what would make this cooler? If the map scrolled! Yes — scrolling is cool, and I had never written a scrolling viewport, so my plans were set.

Now there are many scrolling tutorials out there (I usually look at Tony Pa’s for inspiration) and the theory is simple. Instead of drawing the entire map and its contents to the window (as I did in the first demo), you create a frame — sometimes called a frame, viewport, or camera — which the map ‘scrolls’ under.

scrollingdemo

So instead of each coordinate on the map corresponding exactly with the same coordinates in the window, the correspondence between the map coordinates and the window coordinates will change as your viewpoint (for my purposes, the location of the @ character) moves around the map.

In simple terms, the @ is always in the center of the window — it is the map contents which ‘slide’ under the @ as it moves. So it is logical to ask what happens when the @ gets to the edge of the map. You have two basic choices in this situation — one, keep the @ in the center of the viewport window, and make anything ‘off-map’ a frame color or texture (for example, like when you dynamically resize a web page by making the browser window wider or narrower); and two, allow the @ to go to the edge of the window when it comes close to the edge of the map. Of course, the latter option is much better looking, so the choice is clear there. There are no compromises made on this blog.

So on to the code — I won’t post it all, as the imports, utility functions, game state update, and main loop are still the same.

#!/usr/bin/python

'''
libtcod python tutorial
This code modifies samples_py.py from libtcod 1.4.1. It shows a '@' 
walking around a scrolling map with a source of light giving simple FOV. 
It's in the public domain.
'''

#############################################
# global constants and variables
#############################################

header = 3

view_width = 19
view_height = 19

px = 26
py = 16

vx = 26
vy = 16

fov_recompute = True
fov_map = None
fov_radius = 4

fov_colors =    {
                'dark wall' : libtcod.Color(0, 0, 100),      
                'light wall' : libtcod.Color(130, 110, 50),  
                'dark ground' : libtcod.Color(50, 50, 150),    
                'light ground' : libtcod.Color(200, 180, 50)     
                }
                
do =    {
        'up'    : (0, -1),
        'down'  : (0, 1),
        'right' : (1, 0),
        'left'  : (-1, 0)
        }

keys =  {
        'k' :                   do['up'],
        'j' :                   do['down'],
        'h' :                   do['left'],
        'l' :                   do['right']
        }

Most of this is the same as before, but there are some notable differences. I changed the name of the constant containing the window size to view width and height to make it clearer what I’m doing with it. There are two new variables, vx and vy. This is the x, y coordinate of the viewpoint — in this case, where the @ will be drawn.

Of special note is the header constant. You’ll see later how this gives a banner across the top in which to draw the instructions, without affecting the rest of the viewport drawing.

I’ve also made two dictionaries where just one (move_controls) used to be. This allows me to make it more clear what each key does, and paves the way for a more readable key configuration file.

The map has changed as well — I just made it a little bigger.

lmap_width = 46
lmap_height = 31
lmap = ['##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##########   #################################',
        '##########   #################################',
        '###########  #################   #############',
        '############  ###############   ##############',
        '#############   ###########     ##############',
        '###############  #########      ##############',
        '################   ####         ##############',
        '################   ###   #####################',
        '###############   ##   #######################',
        '#############   ###  #########################',
        '#############       ##########################',
        '##############################################',
        ]

Next is the drawing code. Notice there is a convenience function to make libtcod.put_char a little shorter to type (as many of its arguments are always the same in this program). Everything but drawing the instructions and placing the @ is now contained in the if fov_recompute block, as we basically redraw everything each time the @ moves.

#############################################
# drawing
#############################################

def put_char(x, y, char):
libtcod.console_put_char(0, x, y, char, libtcod.BKGND_NONE)

def draw():
global px, py, fov_map, fov_recompute, lmap
global vx, vy

libtcod.console_set_foreground_color(0, libtcod.black)

if fov_recompute:

fov_map = libtcod.map_new(view_width, view_height)

vx, vy = view_width//2, view_height//2

for m in range(view_height):
if py >= (lmap_height – view_height//2):
y = (lmap_height – view_height) + m
vy = (view_height//2) + (view_height//2 – (lmap_height – py) + 1)
elif py < view_height//2: y = 0 + m vy = py else: y = py - (view_height//2) + m for n in range(view_width): if px >= (lmap_width – view_width//2):
x = (lmap_width – view_width) + n
vx = (view_width//2) + (view_width//2 – (lmap_width – px) + 1)
elif px < view_width//2: x = 0 + n vx = px else: x = px - (view_width//2) + n mh = m + header put_char(n, mh, ' ') if lmap[y][x] == ' ': libtcod.map_set_properties(fov_map, n, m, True, True) elif lmap[y][x] == '=': libtcod.map_set_properties(fov_map, n, m, True, False) put_char(n, mh, libtcod.CHAR_DHLINE) libtcod.map_compute_fov(fov_map, vx, vy, fov_radius, True) for m in range(view_height): for n in range(view_width): mh = m + header affect, cell = 'dark', 'wall' if libtcod.map_is_in_fov(fov_map, n, m): affect = 'light' if libtcod.map_is_walkable(fov_map, n, m): cell = 'ground' color = fov_colors['%s %s' % (affect, cell)] libtcod.console_set_back(0, n, mh, color, libtcod.BKGND_SET) libtcod.console_set_foreground_color(0, libtcod.white) libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE, "HJKL move around") libtcod.console_set_foreground_color(0, libtcod.black) put_char(vx, vy+header, '@') [/sourcecode] Normally, the view is centered on the viewpoint and extends equally in both directions. So, to correspond the map coordinates to the view coordinates, you simply want to take the @'s position (px, py), overlay onto the map a box the size of the view centered on the @, and ‘trace’ the contents of each map cell onto the box view.

However, we come to the case where the @ is so close to the map edge that some of this box ‘falls off the edge’ of the map. In this case the most important thing is the checking of bounds. You want to carefully check where the position of the @ is on the lmap, and if it approaches the map edge, don’t try to draw a coordinate ‘off’ the map onto the view. So when your @ does approach the map edge, you want to re-adjust the view so it stops at the map edge, and then extends backward the size of the view. You want to do this separately for x and y, as this allows you for example to run to the bottom of the map, and then smoothly scroll to the left and right.

If it’s the case the @ is too close to the map edge to center the viewpoint in the view, you also want to put the @ in the right spot — which is the reason for the vx, vy variables.

As you can see, I put all the bounds checking in the for loops that create the box. This probably is not the most efficient way to do it — so that’s something to work on.

Lastly, the header constant (and adding it to m to create mh — used as the y coordinate of the viewport whenever we explicitly draw something there) gives a black banner in which to draw the instructions. Otherwise, your @ will trample all over these carefully composed instructions indiscriminately. This would be a simple way to create a border or UI area around your viewport. Remember that in the initialization of the console (not shown here) you need to add the header to the view_height, to get the proper size height of the window.

Advertisements

3 comments so far

  1. […] (a roguelike in Python #3) Posted April 15, 2009 Filed under: roguelike | If you look at the code for the scrolling demo, it’s obvious that I didn’t use any of the OO features of Python. Instead I just wrote […]

  2. kenny on

    I’ve been aware of your articles on writing RL for some time now but I’ve only just started writing my own ( NY resolution ) for purposes of pracisting Python. The above code doesn’t run because I think ‘n’ variable in line 41 of the draw() module isn’t initialised.

    • georgek on

      hi kenny, that n is the iterator variable in the for loop, so I don’t think that’s the problem, but I haven’t run the code; I recommend ignoring this example and going to the Roguebasin wiki Python tutorial, it’s much better, more current and complete.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: