Tutorial:Efficient Tile-based Scrolling (日本語)

この教本では、より効率的なタイル・ベースのスクローリングに対する SpriteBatch クラスの手ほどきです。基本的なタイル・ベースのスクローリングの教本については、タイル・ベースのスクロールを参照してください。

簡単なマップ生成器によるタイル・ベースのスクローリングの画面撮影。

Quads および SpriteBatch

love.graphics.draw メソッドは Quad により指定された画像を部分的に描画することができます。 Quad が単一画像の異なる部分から描画される場合は、SpriteBatch の使用およびフレームごとに Quad を変更しないことでシステムをより効率的にすることができます。

SpriteBatch の作成時に、 Quad ("タイル") が扱う画像および Quad へ追加する最大数を指定する必要があります。この場合は、画面上で可視化されるタイルの最大数です。

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

マップの初期化

下記により地図を初期化します。説明に関してはタイル・ベースのスクロールを参照してください。

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

SpriteBatch へのタイルマップ追加

タイルセットの一例。

次に、タイルセットを読み込み、 Quad に対して使用したいタイルを作成して、タイルを SpriteBatch オブジェクトを作成します。用例には、 http://silveiraneto.net/... にて配布されているフリーのタイルセットを使用します。

function love.load()
  ... -- マップの初期化
  
  tilesetImage = love.graphics.newImage( "tileset.png" )
  tilesetImage:setFilter("nearest", "linear") --  この "線形フィルタ" はタイルの大きさを変更した場合に発生する一部の人工生成物を削除します。
  tileSize = 32
  
  -- 芝生
  tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 台所の床タイル
  tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 床張り
  tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 赤い絨毯の中間
  tileQuads[3] = love.graphics.newQuad(3 * tileSize, 9 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())

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

SpriteBatch へ現在可視化されるタイルのみを追加したいとします。これを行うには、タイルセットを更新する関数を作成して、マップのフォーカスを変更するたびに呼び出します。

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

最後に、 SpriteBatch を描画するためにそれを love.graphics.draw へ送信します。

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

個別の移動

マップの中を移動することは タイル・ベースのスクロールと同様の方法で実現します。キーが押されたり、それに従ってマップを更新するかどうかを確認します。必ず 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)
  -- 実際に移動した場合のみ更新します。
  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

連続的な移動

mapX および mapY に非整数値を利用することで移動を少し改善します。 SpriteBatch への Quad 追加時に、整数部分のみ考慮を行う一方で描画では端数部分を扱うために SpriteBatch のシフトを行うことがあります。 love.keypressed コールバックを love.update へ置き換えることで少ない手順にてマップを移動します。

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

SpriteBatch の更新へ floor を追加します。

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

最後に、端数部分により SpriteBatch をシフトします。

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

まとめ

このコードにzoomX および zoomY 変数を挿入します。例えば 16x16 のタイルは 32x32 のタイルとして描画されます。

local map -- タイルデータの格納
local mapWidth, mapHeight -- タイルの幅と高さ

local mapX, mapY -- タイルにおける視点 x,y です。3.25 といった断片的な値です。

local tilesDisplayWidth, tilesDisplayHeight -- 表示するタイルの本数
local zoomX, zoomY

local tilesetImage
local tileSize -- ピクセル単位でのタイルの大きさ
local tileQuads = {} -- 異なるタイルに対して使用されたタイルの部品
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") -- この "線形フィルタ" はタイルの大きさを変更した場合に発生する一部の人工生成物を削除します。
  tileSize = 32
  
  -- 芝生
  tileQuads[0] = love.graphics.newQuad(0 * tileSize, 20 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 台所の床タイル
  tileQuads[1] = love.graphics.newQuad(2 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 床張り
  tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize,
    tilesetImage:getWidth(), tilesetImage:getHeight())
  -- 赤い絨毯の中間
  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

-- マップの移動において重要な関数
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)
  -- 実際に移動した場合のみ更新します。
  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



そのほかの言語