Views
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.
Map Initialization
We initialize a map with the following. See the Tile-based Scrolling Tutorial for an explanation.
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/....
... --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.
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
.
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.
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.
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.
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.
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 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
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