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
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: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:flush() 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 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.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: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:flush() 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 -- 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: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:flush() 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
Dansk –
Deutsch –
English –
Español –
Français –
Indonesia –
Italiano –
Lietuviškai –
Magyar –
Nederlands –
Polski –
Português –
Română –
Slovenský –
Suomi –
Svenska –
Türkçe –
Česky –
Ελληνικά –
Български –
Русский –
Српски –
Українська –
עברית –
ไทย –
日本語 –
正體中文 –
简体中文 –
Tiếng Việt –
한국어
More info