Tutorial:Efficient Tile-based Scrolling (Français)

Ce tutoriel introduit la classe SpriteBatch(lot de sprites) pour un système de défilement basé sur des tuiles plus efficace. Pour un tutoriel sur un tel système de défilement plus basique, voir Tutorial:Tile-based_Scrolling.

Capture d'écran d'un défilement basé sur des tuiles avec un générateur de carte simple.

Quads et SpriteBatch

La méthode love.graphics.draw peut dessiner une portion d'image spécifiée par un Quad. Si des Quads sont dessinés à partir de différentes parties d'une image unique, on peut render le système plus efficace en utilisant un SpriteBatch et en ne changeant pas les Quads à chaque frame.

Lorsqu'on créé un SpriteBatch, on doit spécifier l'image à partir de laquelle on prend des Quads, et le nombre maximum de Quads que l'on y ajoutera. En l’occurrence, ça sera le nombre maximum de tuiles visible en même temps sur l'écran.

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

Initialisation de la carte

On initialise une carte avec le code suivant. Voir le Tutoriel de défilement basé sur des tuiles pour une explication

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

Ajouter un SpriteBatch comme Tileset

Un exemple de tileset.

Ensuite, nous chargeons un tileset, on créé les quads pour les tuiles que l'on souhaite utiliser, et on créé un objet Spritebatch pour contenir les tuiles. Comme exemple, nous allons utiliser un tilset gratuit de http://silveiraneto.net/....

function love.load()
  ... -- initialization de la carte
 
  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
 
  -- l'herbe
  tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- carrelage de cuisine
  tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- parquet
  tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- milieu de tapis rouge
  tileQuads[3] = love.graphics.newQuad(3 * tileSize, 9 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
 
  tilesetBatch = love.graphics.newSpriteBatch(tilesetImage, tilesDisplayWidth * tilesDisplayHeight)
end

On souhaite seulement ajouter au SpriteBatch les tuiles qui sont présentement visible. Pour faire cela, on créé une fonction qui met à jour le tileset et on l'appelle dès que la vue de la carte change. On l'appelle aussi une fois à l'initialisation

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

Enfin, pour dessiner le SpriteBatch, on l'envoie juste à love.graphics.draw.

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

Déplacement discret (discontinu)

Le déplacement sur la carte est effectué de la même façon que dans le Tutoriel de défilement basé sur des tuiles; On vérifie si une touche est enfoncée et on met à jour la carte en conséquence. On doit aussi se souvenir de mettre à jour le 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

Mouvement continu

On peut render le mouvement un peu plus agréable en laissant mapX et mapY prendre des valeurs non-entières. Quand on ajoutera des Quads au SpriteBatch, on prendra seulement en compte la partie entière tandis que l'affichage décalera le SpriteBatch to prendre en compte la partie fractionnaire. On replace le callback love.keypressed avec un callback love.update et on déplace la carte par petits pas.

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

On ajoute un appel de la fonction partie entière (floor) à la mise à jour du SpriteBatch.

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

Enfin, on décale le SpriteBatch par la partie fractionnaire.

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

Mise en commun

On a également ajouté des variables zoomX et zoomY dans ce code pour permettre, par exemple, de dessiner des tuiles de 16x16 en 32x32

local map -- stock les données des tuiles de la carte
local mapWidth, mapHeight -- largeur et hauteur de la carte en tuiles
 
local mapX, mapY -- vue x,y en tuiles. Peut aussi être une valeur fractionnaire comme 3.25.
 
local tilesDisplayWidth, tilesDisplayHeight -- Nombre de tuiles à afficher
local zoomX, zoomY
 
local tilesetImage
local tileSize -- taille des tuiles en pixels
local tileQuads = {} -- parties du tileset utilisées pour différentes tuiles
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") -- ce filtre "linear" permet d'enlever des artefacts visuels si on changeait l'échelle des tuiles.
  tileSize = 32
 
  -- l'herbe
  tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- carrelage de cuisine
  tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- parquet
  tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- milieu de tapis rouge
  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
 
-- fonction centrale pour déplacer la carte
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)
  -- seulement si on a effectivement bougé
  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



Autre langages