Difference between revisions of "Tutorial:Efficient Tile-based Scrolling"

(Removed division by 2 in addq call, no longer applicable in love 0.6.2)
m (Remove duplicate line)
 
(10 intermediate revisions by 8 users not shown)
Line 4: Line 4:
  
 
== [[Quad]]s and [[SpriteBatch]] ==
 
== [[Quad]]s and [[SpriteBatch]] ==
The [[love.graphics.drawq]] method draws 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.
+
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.
 
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.
Line 22: Line 22:
 
     map[x] = {}
 
     map[x] = {}
 
     for y=1,mapHeight do
 
     for y=1,mapHeight do
       map[x][y] = math.random(0,3)
+
       map[x][y] = love.math.random(0,3)
 
     end
 
     end
 
   end
 
   end
Line 45: Line 45:
 
    
 
    
 
   tilesetImage = love.graphics.newImage( "tileset.png" )
 
   tilesetImage = love.graphics.newImage( "tileset.png" )
   tilesetImage:setFilter(1,0) -- this "linear filter" removes some artifacts if we were to scale the tiles
+
   tilesetImage:setFilter("nearest", "linear") -- this "linear filter" removes some artifacts if we were to scale the tiles
 
   tileSize = 32
 
   tileSize = 32
 
    
 
    
Line 71: Line 71:
 
   for x=0, tilesDisplayWidth-1 do
 
   for x=0, tilesDisplayWidth-1 do
 
     for y=0, tilesDisplayHeight-1 do
 
     for y=0, tilesDisplayHeight-1 do
       tilesetBatch:addq(tileQuads[map[x+mapX][y+mapY]], x*tileSize, y*tileSize)
+
       tilesetBatch:add(tileQuads[map[x+mapX][y+mapY]], x*tileSize, y*tileSize)
 
     end
 
     end
 
   end
 
   end
 +
  tilesetBatch:flush()
 
end
 
end
 
</source>
 
</source>
Line 99: Line 100:
 
end
 
end
  
function love.keypressed(key, unicode)
+
function love.keypressed(key)
   if key == "up") then
+
   if key == "up" then
 
     moveMap(0, -1)
 
     moveMap(0, -1)
 
   end
 
   end
   if key == "down") then
+
   if key == "down" then
 
     moveMap(0, 1)
 
     moveMap(0, 1)
 
   end
 
   end
Line 140: Line 141:
 
   for x=0, tilesDisplayWidth-1 do
 
   for x=0, tilesDisplayWidth-1 do
 
     for y=0, tilesDisplayHeight-1 do
 
     for y=0, tilesDisplayHeight-1 do
       tilesetBatch:addq(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
+
       tilesetBatch:add(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
 
         x*tileSize/2, y*tileSize/2)
 
         x*tileSize/2, y*tileSize/2)
 
     end
 
     end
 
   end
 
   end
 +
  tilesetBatch:flush()
 
end
 
end
 
</source>
 
</source>
Line 187: Line 189:
 
     map[x] = {}
 
     map[x] = {}
 
     for y=1,mapHeight do
 
     for y=1,mapHeight do
       map[x][y] = math.random(0,3)
+
       map[x][y] = love.math.random(0,3)
 
     end
 
     end
 
   end
 
   end
Line 204: Line 206:
 
function setupTileset()
 
function setupTileset()
 
   tilesetImage = love.graphics.newImage( "tileset.png" )
 
   tilesetImage = love.graphics.newImage( "tileset.png" )
   tilesetImage:setFilter(1,0)
+
   tilesetImage:setFilter("nearest", "linear") -- this "linear filter" removes some artifacts if we were to scale the tiles
  tileSize = 32
 
 
   tileSize = 32
 
   tileSize = 32
 
    
 
    
Line 230: Line 231:
 
   for x=0, tilesDisplayWidth-1 do
 
   for x=0, tilesDisplayWidth-1 do
 
     for y=0, tilesDisplayHeight-1 do
 
     for y=0, tilesDisplayHeight-1 do
       tilesetBatch:addq(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
+
       tilesetBatch:add(tileQuads[map[x+math.floor(mapX)][y+math.floor(mapY)]],
         x*tileSize/2, y*tileSize/2)
+
         x*tileSize, y*tileSize)
 
     end
 
     end
 
   end
 
   end
 +
  tilesetBatch:flush()
 
end
 
end
  
Line 275: Line 277:
 
{{#set:LOVE Version=0.6.0}}
 
{{#set:LOVE Version=0.6.0}}
 
{{#set:Description=Efficient Tile-based Scrolling}}
 
{{#set:Description=Efficient Tile-based Scrolling}}
 +
 +
== Other languages ==
 +
{{i18n|Tutorial:Efficient Tile-based Scrolling}}

Latest revision as of 20:47, 20 September 2016

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.

Screenshot of tile-based scrolling with a simple map generator.

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: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