Home Projects Philosophy Hobbies About Me Updates

Tiled Map in PyGame

A more complete version of this tutorial is now available at http://readthedocs.org/docs/qq.

This is a sort of a tutorial about writing a tile-based animated game in Python using PyGame. I'm writing it, because it took me some time to figure out a way that works well for me – it wasn't immediately obvious – and I'd like to save others some effort. Please don't treat it as one and the only, or even just best way to do it. Of course, I'm always interested about hearing about better ways. I'm also not going to tell you how to write the whole game: I will concentrate on the parts directly related to PyGame. You are on your own with the rest.


Just to make sure we are on the same page: if you ever played any RPG games for Game Boy, NES, SNES or some of the titles on Game Boy Advance or Nintendo DS, you have surely noticed that the display consists of a scrolled map, composed of square (or rectangular) elements, with animated characters, monsters and other objects added to it. This is what we are going to make. Usually games also have a more or less sophisticated system of menus for inventory and combat, and of course plot and dialogs a lot of finer details – that's outside the scope of this article.

As was said, the map is composed of rectangular elements, fit together on a grid. Sometimes there are so many of them and they are so well drawn that you can hardly tell where one of them begins and other ends. We will be calling them "tiles", because they are arranged like tiles on a kitchen floor. Incidentally, they will sometimes be tiles of actual floor tiles, but also of grass, walls, ground, trees, etc. – usually most non-moving things in those games are created using tiles. The graphics for those tiles is usually taken from tilesets. Tilestes are, well sets of tiles, usually in a form of images containing rows of tiles aligned one next to the other, one of each. Our program will slice them into single tiles, and "stamp" those on the screen to create the map. An example of a small tileset may look like this:


To use that tileset, we will need some code that will load it into memory and slice into single tiles. We will use something like this:

import pygame
import pygame.locals

def load_tile_table(filename, width, height):
    image = pygame.image.load(filename).convert()
    image_width, image_height = image.get_size()
    tile_table = []
    for tile_x in range(0, image_width/width):
        line = []
        for tile_y in range(0, image_height/height):
            rect = (tile_x*width, tile_y*height, width, height)
    return tile_table

if __name__=='__main__':
    screen = pygame.display.set_mode((128, 98))
    screen.fill((255, 255, 255))
    table = load_tile_table("tileset.png", 24, 16)
    for x, row in enumerate(table):
        for y, tile in enumerate(row):
            screen.blit(tile, (x*32, y*24))
    while pygame.event.wait().type != pygame.locals.QUIT:

The function load_tile_table will load tiles from a tileset file, and return them sliced up as a list of lists. At first I did that by creating separate surfaces, but later I found out that using subsurface is better – it doesn't create copies in memory. Once the tiles are loaded, they are displayed, a little spaced so that you can see where they end:


Map definition

We usually don't want to hardcode the maps in our game, so it's best to put them in a file. You might distract yourself by writing a map editor, or use an existing editor and merely distract yourself trying to figure out how to parse the files it produces. I took an easier route and just used a plain text file, parsed with the ConfigParser. I define the map as lines of characters, every character representing one map square. What exactly that square contains is defined later in the file, in sort of a legend. So, an example map definition may look like this:

tileset = tileset.png
map = ####

name = floor
tile = 0, 3

name = wall
wall = true
block = true

This way, for every map square I have a dictionary of key-value pairs describing that square. If I need to add something, I just add a new character to the legend, with the description. It goes something like this:

import ConfigParser

class Level(object):
    def load_file(self, filename="level.map"):
        self.map = []
        self.key = {}
        parser = ConfigParser.ConfigParser()
        self.tileset = parser.get("level", "tileset")
        self.map = parser.get("level", "map").split("\n")
        for section in parser.sections():
            if len(section) == 1:
                desc = dict(parser.items(section))
                self.key[section] = desc
        self.width = len(self.map[0])
        self.height = len(self.map)

    def get_tile(self, x, y):
            char = self.map[y][x]
        except IndexError:
            return {}
            return self.key[char]
        except KeyError:
            return {}

With this code, you can just use get_tile to get a dict of values for any particular map square.

Drawing the map

Now we can just iterate over all map squares and draw appropriate tiles in the right places. This works fine for floor tiles and maybe some simple walls, but you might have noticed that we have a lot of different tiles for corners, straight walls, etc. You could of course use different characters for them in your map definition, but making them match manually is pretty boring and can be easily automated. Se we have a flag "wall" for our map squares, and when that flag is set we choose the right tile by looking at the neighboring squares. The code is not very pretty, but it does what we want. Oh, I forgot, we don't draw our map directly on the screen, we draw it on a separate "background" surface, and we only do it once per map. Then we can draw that surface on the screen, draw some more elements on top and still be able to erase those elements, by drawing parts from the background over them. We might also make the background larger than the screen and implement scrolling, although I won't cover that here.

This is enough if we have a "flat" map, for example only different kinds of terrain. But there is a problem if we want walls. Walls should obscure the view and so we need to draw parts of them on top of anything we add to the map. We will do it by keeping a list of all the elements that need to be drawn on top, called overlays. Our finished map-drawing code may look like this:

    def is_wall(self, x, y):
        return self.get_bool(x, y, 'wall')

    def render(self):
        wall = self.is_wall
        tiles = MAP_CACHE[self.tileset]
        image = pygame.Surface((self.width*MAP_TILE_WIDTH, self.height*MAP_TILE_HEIGHT))
        overlays = {}
        for map_y, line in enumerate(self.map):
            for map_x, c in enumerate(line):
                if wall(map_x, map_y):
                    # Draw different tiles depending on neighbourhood
                    if not wall(map_x, map_y+1):
                        if wall(map_x+1, map_y) and wall(map_x-1, map_y):
                            tile = 1, 2
                        elif wall(map_x+1, map_y):
                            tile = 0, 2
                        elif wall(map_x-1, map_y):
                            tile = 2, 2
                            tile = 3, 2
                        if wall(map_x+1, map_y+1) and wall(map_x-1, map_y+1):
                            tile = 1, 1
                        elif wall(map_x+1, map_y+1):
                            tile = 0, 1
                        elif wall(map_x-1, map_y+1):
                            tile = 2, 1
                            tile = 3, 1
                    # Add overlays if the wall may be obscuring something
                    if not wall(map_x, map_y-1):
                            over = 1, 0
                        elif wall(map_x+1, map_y):
                            over = 0, 0
                        elif wall(map_x-1, map_y):
                            over = 2, 0
                            over = 3, 0
                        overlays[(map_x, map_y)] = tiles[over[0]][over[1]]
                        tile = self.key[c]['tile'].split(',')
                        tile = int(tile[0]), int(tile[1])
                    except (ValueError, KeyError):
                        # Default to ground tile
                        tile = 0, 3
                tile_image = tiles[tile[0]][tile[1]]
                           (map_x*MAP_TILE_WIDTH, map_y*MAP_TILE_HEIGHT))
        return image, overlays

Then we can draw the background image on the screen, followed by all the movable objects and sprites, and then draw the overlays on top of them. We will keep them as a sprite group for convenience.

We can write our main game loop like this:

        clock = pygame.time.Clock()
        screen = pygame.display.set_mode((424, 320))
        background, overlay_dict = level.render()
        overlays = pygame.sprite.RenderUpdates()
        for (x, y), image in overlay_dict.iteritems():
            overlay = pygame.sprite.Sprite(overlays)
            overlay.image = image
            overlay.rect = image.get_rect().move(x*24, y*16-16)
        screen.blit(background, (0, 0))
        game_over = False
        while not game_over:

            # XXX draw all the objects here

            for event in pygame.event.get():
                if event.type == pygame.locals.QUIT:
                    game_over = True
                elif event.type == pygame.locals.KEYDOWN:
                    pressed_key = event.key

We are doing 15 iterations of the loop per second, using pygame's clock. In each iteration we will draw our objects on the background, and then draw the overlays over them. For now we refresh the whole screen, later we will see how to only refresh the parts that changed. Lastly, we check for events and record keypresses for future processing.

To be continued

This article is a work in progress, I will be adding more detail.

Final code

You can view or get the final code. It's well commented, so that you can easily understand it. You can use it as a starting point for your own game, or just see how the things are done and do it your way. You can also download the whole archive.