Tutorial:Tile-based Scrolling

In this little tutorial we will cover a widely used technique used to draw game backgrounds: tilemaps. Using this technique, our backgrounds are going to be made up of pieces (tiles) that, drawn together, will allow us to create bigger and more complex levels without using huge amounts of memory. The basic idea is to have a 2d value table, where each value represents a tile. Using that table, we will know what tiles to draw at a certain position, given some basic parameters like the map width, height and the current coordinates of an imaginary viewport. Don't worry if you don't understand these terms right now, I'll explain them in more detail as we start coding. So, let's start with some variables:

-- our tiles
   tile = {}
   for i=0,3 do -- change 3 to the number of tile images minus 1.
      tile[i] = love.graphics.newImage( "tile"..i..".png" )
   end
 
   love.graphics.setNewFont(12)
 
   -- map variables
   map_w = 20
   map_h = 20
   map_x = 0
   map_y = 0
   map_offset_x = 30
   map_offset_y = 30
   map_display_w = 14
   map_display_h = 10
   tile_w = 48
   tile_h = 48

We first create a Lua table to store our tiles. As I already said, these are little bitmaps we'll use to draw our maps, something like mosaics, which are made up of smaller pieces. Since this will be a very simple tilemap, we'll just use four tiles, loading them inside a for loop. Now, the next thing we have to do is set up a handful of map and tile variables to be able to draw the map correctly. This ones should be pretty self-explanatory: besides height and width of the tiles and our tilemap, we also have the size of the region of tiles to display. The two offset variables are used to define the point where we will start drawing out tilemap. You might want to play a little with these variables to see their effects. Now, to the map structure itself:

map={
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 
   { 0, 1, 0, 0, 2, 2, 2, 0, 3, 0, 3, 0, 1, 1, 1, 0, 0, 0, 0, 0},
   { 0, 1, 0, 0, 2, 0, 2, 0, 3, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0},
   { 0, 1, 1, 0, 2, 2, 2, 0, 0, 3, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
   { 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
   { 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 2, 0, 0, 0, 0, 0, 0},
   { 0, 2, 0, 0, 0, 3, 0, 3, 0, 1, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0},
   { 0, 2, 0, 0, 0, 3, 0, 3, 0, 1, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0},
   { 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
   { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}


As you can see, not much magic here, we just define a 2d table where each number represents an index of our tile table. In our example, the zero represents grass, the one represents dirt, the two represents stone and the three represents water. Obviously, your tiles may represent whatever you want, even parts of a bigger structure if you so desire. Let's continue:

function draw_map()
   for y=1, map_display_h do
      for x=1, map_display_w do                                                         
         love.graphics.draw( 
            tile[map[y+map_y][x+map_x]], 
            (x*tile_w)+map_offset_x, 
            (y*tile_h)+map_offset_y )
      end
   end
end


To keep our love.draw callback clean, I decided to move our little map drawing routine its own function. Again, nothing really impressive here, we just go through our map table, checking every column (x values) for each row (y values). You can draw as many tiles as you want, just change the map_display_h and map_display_w variables accordingly. Just note that if you give them too high values you'll start drawing tiles off-screen, wasting time and resources. You can change the coordinates of the region to be drawn just by changing the map_x and map_y values.

Obviously, you have to call draw_map() in your love.draw callback. If you run your example right now, you should see our little tilemap on screen. You can't do any scrolling right now, but we'll get onto that right now, adding the following to our love.keypressed callback:

function love.keypressed(key, unicode)
   if key == 'up' then
      map_y = map_y-1
      if map_y < 0 then map_y = 0; end
   end
   if key == 'down' then
      map_y = map_y+1
      if map_y > map_h-map_display_h then map_y = map_h-map_display_h; end
   end
 
   if key == 'left' then
      map_x = math.max(map_x-1, 0)
   end
   if key == 'right' then
      map_x = math.min(map_x+1, map_w-map_display_w)
   end
end


This is fairly simple: first, we check if the arrow keys are being pressed and we update the map_x and map_y variables accordingly. Remember, here we're talking on tile coordinates so when we're increasing or decreasing map_x or map_y by one, we're actually moving 64 px (the size of a single tile). Finally, to avoid off-map scrolling, we include some boundary checking code. The y-coordinates shows a simple way of checking the boundaries while the boundary checks for x-coordinates use a more concise variant.

function love.draw()
  draw_map()
end

Finally, we use our draw_map () that we created earlier to actually display the tiles.

Now go and run your script. Isn't LÖVE cool? You bet it is, congratulations on making your first tilemap scroller! While this little example should be enough to get you up and running, there are quite a few possible enhancements:

  • Writing our maps by hand is a little stupid, we should make them with tools like Tiled and load them in our game.
  • Drawing characters inside the tilemap.
  • Fine scrolling: As you probably noticed, the map scrolls tile by tile and you probably don't want that if you need to move your characters/object pixel by pixel. (Example)
  • Multi-layered maps: Most real tile engines are able to render more than one layer. Together with fine scrolling, you would be able to scroll a near layer at a certain speed and move another one behind it more slowly.
  • Character to map collisions.

See Also

Fine Tile-based Scrolling
Efficient Tile-based Scrolling



Other languages