# Tutorial:Efficient Tile-based Scrolling

This tutorial introduces the SpriteBatch class for more efficient tile-based scrolling. For a tutorial on basic tile-based scrolling, see Tutorial:Tile-based_Scrolling.

## Quads and SpriteBatch

The love.graphics.draw method can draw a portion of an image specified by a Quad. If quads are drawn from different parts of a single image, we can make the system more efficient by using a SpriteBatch and not changing the quads every frame.

When creating a SpriteBatch, we must specify the image that we take the quads ("tiles") from and the maximum number of quads that we will be adding. In this case, the maximum number of tiles visible on the screen.

    tilesetBatch = love.graphics.newSpriteBatch(tilesetImage, tilesDisplayWidth * tilesDisplayHeight)

## Map Initialization

We initialize a map with the following. See the Tile-based Scrolling Tutorial for an explanation.

    function love.load()
      mapWidth = 60
      mapHeight = 40

      map = {}
      for x=1,mapWidth do
        map[x] = {}
        for y=1,mapHeight do
          map[x][y] = love.math.random(0,3)
        end
      end

      mapX = 1
      mapY = 1
      tilesDisplayWidth = 26
      tilesDisplayHeight = 20

      zoomX = 1
      zoomY = 1
    end

## Adding a SpriteBatch as Tilemap

Example tileset.

Next, we load a tileset, create quads for the tiles we want to use, and make a SpriteBatch object to hold the tiles. As an example, we are going to use a free tileset from http://silveiraneto.net/....

    function love.load()
      ... --map init

      tilesetImage = love.graphics.newImage( "tileset.png" )
      tilesetImage:setFilter("nearest", "linear") -- this "linear filter" removes some artifacts if we were to scale the tiles
      tileSize = 32

      -- grass
      tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- kitchen floor tile
      tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- parquet flooring
      tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- middle of red carpet
      tileQuads[3] = love.graphics.newQuad(3 * tileSize, 9 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())

      tilesetBatch = love.graphics.newSpriteBatch(tilesetImage, tilesDisplayWidth * tilesDisplayHeight)
    end

We only wish to add to the SpriteBatch the tiles that are presently visible. To do this, we make a function that updates the tileset and call it whenever the map focus changes. We also call it once in the initialization.

    function updateTilesetBatch()
      tilesetBatch:bind()
      tilesetBatch:clear()
      for x=0, tilesDisplayWidth-1 do
        for y=0, tilesDisplayHeight-1 do
          tilesetBatch:add(tileQuads[map[x+mapX][y+mapY]], x*tileSize, y*tileSize)
        end
      end
      tilesetBatch:unbind()
    end

Finally, to draw the SpriteBatch, we just send it to `love.graphics.draw`.

    function love.draw()
      love.graphics.draw(tilesetBatch)
    end

## Discrete Moving

Moving around the map is done the same way as in the Tile-based Scrolling tutorial; we check if any keys are pressed and update the map accordingly. We must also remember to update the SpriteBatch.

    -- central function for moving the mapfunction moveMap(dx, dy)
      oldMapX = mapX
      oldMapY = mapY
      mapX = math.max(math.min(mapX + dx, mapWidth - tilesDisplayWidth), 1)
      mapY = math.max(math.min(mapY + dy, mapHeight - tilesDisplayHeight), 1)
      -- only update if we actually moved
      if math.floor(mapX) ~= math.floor(oldMapX) or math.floor(mapY) ~= math.floor(oldMapY) then
        updateTilesetBatch()
      end
    end

    function love.keypressed(key)
      if key == "up" then
        moveMap(0, -1)
      end
      if key == "down" then
        moveMap(0, 1)
      end
      if key == "left" then
        moveMap(-1, 0)
      end
      if key == "right" then
        moveMap(1, 0)
      end
    end

## Continuous Movement

We make the movement a bit nicer by allowing mapX and mapY to take on non-integer values. When adding quads to the SpriteBatch, we will only consider the integer part while the drawing will shift the SpriteBatch to handle the fractional part. We replace the love.keypressed callback with a love.update callback and move the map in small steps.

    function love.update(dt)
      if love.keyboard.isDown("up")  then
        moveMap(0, -0.2 * tileSize * dt)
      end
      if love.keyboard.isDown("down")  then
        moveMap(0, 0.2 * tileSize * dt)
      end
      if love.keyboard.isDown("left")  then
        moveMap(-0.2 * tileSize * dt, 0)
      end
      if love.keyboard.isDown("right")  then
        moveMap(0.2 * tileSize * dt, 0)
      end
    end

We add a floor to the SpriteBatch update.

    function updateTilesetBatch()
      tilesetBatch:bind()
      tilesetBatch:clear()
      for x=0, tilesDisplayWidth-1 do
        for y=0, tilesDisplayHeight-1 do
          tilesetBatch:add(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
            x*tileSize/2, y*tileSize/2)
        end
      end
      tilesetBatch:unbind()
    end

Finally, we shift the SpriteBatch by the fractional part.

    function love.draw()
      love.graphics.draw(tilesetBatch,
        math.floor(-(mapX%1)*tileSize), math.floor(-(mapY%1)*tileSize))
      love.graphics.print("FPS: "..love.timer.getFPS(), 10, 20)
    end

## Putting It All Together

We have also added zoomX and zoomY variables to this code to allow, e.g., 16x16 tiles to be drawn as 32x32.

    local map -- stores tiledata
    local mapWidth, mapHeight -- width and height in tiles

    local mapX, mapY -- view x,y in tiles. can be a fractional value like 3.25.

    local tilesDisplayWidth, tilesDisplayHeight -- number of tiles to show
    local zoomX, zoomY

    local tilesetImage
    local tileSize -- size of tiles in pixels
    local tileQuads = {} -- parts of the tileset used for different tiles
    local tilesetSprite

    function love.load()
      setupMap()
      setupMapView()
      setupTileset()
      love.graphics.setFont(12)
    end

    function setupMap()
      mapWidth = 60
      mapHeight = 40

      map = {}
      for x=1,mapWidth do
        map[x] = {}
        for y=1,mapHeight do
          map[x][y] = love.math.random(0,3)
        end
      end
    end

    function setupMapView()
      mapX = 1
      mapY = 1
      tilesDisplayWidth = 26
      tilesDisplayHeight = 20

      zoomX = 1
      zoomY = 1
    end

    function setupTileset()
      tilesetImage = love.graphics.newImage( "tileset.png" )
      tilesetImage:setFilter("nearest", "linear") -- this "linear filter" removes some artifacts if we were to scale the tiles
      tileSize = 32
      tileSize = 32

      -- grass
      tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- kitchen floor tile
      tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- parquet flooring
      tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())
      -- middle of red carpet
      tileQuads[3] = love.graphics.newQuad(3 * tileSize, 9 * tileSize, tileSize, tileSize,
        tilesetImage:getWidth(), tilesetImage:getHeight())

      tilesetBatch = love.graphics.newSpriteBatch(tilesetImage, tilesDisplayWidth * tilesDisplayHeight)

      updateTilesetBatch()
    end

    function updateTilesetBatch()
      tilesetBatch:bind()
      tilesetBatch:clear()
      for x=0, tilesDisplayWidth-1 do
        for y=0, tilesDisplayHeight-1 do
          tilesetBatch:add(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
            x*tileSize, y*tileSize)
        end
      end
      tilesetBatch:unbind()
    end

    -- central function for moving the map
    function moveMap(dx, dy)
      oldMapX = mapX
      oldMapY = mapY
      mapX = math.max(math.min(mapX + dx, mapWidth - tilesDisplayWidth), 1)
      mapY = math.max(math.min(mapY + dy, mapHeight - tilesDisplayHeight), 1)
      -- only update if we actually moved
      if math.floor(mapX) ~= math.floor(oldMapX) or math.floor(mapY) ~= math.floor(oldMapY) then
        updateTilesetBatch()
      end
    end

    function love.update(dt)
      if love.keyboard.isDown("up")  then
        moveMap(0, -0.2 * tileSize * dt)
      end
      if love.keyboard.isDown("down")  then
        moveMap(0, 0.2 * tileSize * dt)
      end
      if love.keyboard.isDown("left")  then
        moveMap(-0.2 * tileSize * dt, 0)
      end
      if love.keyboard.isDown("right")  then
        moveMap(0.2 * tileSize * dt, 0)
      end
    end

    function love.draw()
      love.graphics.draw(tilesetBatch,
        math.floor(-zoomX*(mapX%1)*tileSize), math.floor(-zoomY*(mapY%1)*tileSize),
        0, zoomX, zoomY)
      love.graphics.print("FPS: "..love.timer.getFPS(), 10, 20)
    end

## Other languages

Category:

- Tutorials