LÖVE (Love2D) 入門編:11.背景をスクロールする

(2018.7.30. 公開) (2018.8.26. 対象をバージョン 11 系列に変更)

 シューティングゲームにしても、RPG タイプのゲームにしても、スクロールする背景の上でキャラクタが動く、という設定はよくある。これをやってみましょう。

 まず、背景の画像素材を用意する。ここがけっこうハードルが高かったりします。今回は、ぴぽや さんが「ぴぽや倉庫」(http://pipoya.net/sozai/) で無料公開されているマップ用素材を使いました。


ぴぽや http://blog.pipoya.net/

1. 画面サイズを特定する

 画像素材を並べて背景を作る時、画面サイズが特定されていないとデザインしにくい。そこで、ゲーム画面を固定サイズ(ここでは640x480)に設定する。

 PC や Mac の場合は love.window.setMode(640, 480) でウィンドウの大きさそのものを変更できるのだが、ラズパイで X window なしで動かした場合は、この命令が効かない。そこで、「画面サイズの設定」で紹介したテクニックを使って、ゲーム画面を 640x480 に限定する。ゲーム画面の外は、灰色の枠で埋めておく。

-- サンプルプログラム 11-01 main.lua
function set_screen_size()
  width = 640                         -- ゲーム画面の横幅
  height = 480                        -- ゲーム画面の高さ
  love.window.setMode(width, height)  -- これが効くなら問題なし
  swidth = love.graphics.getWidth()   -- 実画面の横幅
  sheight = love.graphics.getHeight() -- 実画面の高さ
  transx, transy = 0, 0
  scale = 1
  if swidth ~= width or sheight ~= height then
    -- setMode が効いてない場合
    if swidth / sheight > width / height then -- 実画面の方が横長
      scale = sheight / height          -- 縦方向で倍率を決める
      transx = math.floor((swidth - width * scale) / 2)  -- 空白の幅
      transy = 0
    else
      scale = swidth / width            -- 横方向で倍率を決める
      transx = 0
      transy = math.floor((sheight - height * scale) / 2) -- 空白の高さ
    end
  end
  love.graphics.setBackgroundColor(0.82, 0.82, 0.82) -- ゲーム画面の外は灰色の枠
end

function love.load()
  set_screen_size()
end

function love.draw()
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- 画面サイズを (width,height) と思って描画する
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, width, height)  -- ゲーム画面を白く塗る
end

2. 画像タイルを描く

 今回使う背景画像は、32x32 のタイルを並べて背景を作るようにデザインされている。これを画面に表示するには、「画像の一部を使う」で紹介した Quad を使うのがよい。まずはシンプルに、草原のバックグラウンドに木が並んでいる背景を作ってみた。背景画像の中で、「草原」は (0, 0) の位置に、「木」は (64, 32) の位置にあるので、そこから Quad を作っておき、love.draw() の中で必要な位置に描画する。

-- サンプルプログラム 11-02 main.lua
-- pipo-map001.png が必要
-- function set_screen_size() 〜 end までは 11-01 と同じ

function love.load()
  set_screen_size()
  -- マップ(0: 草原、1: 木)
  map = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,
    0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,1,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,
    0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
  atlas = love.graphics.newImage("pipo-map001.png") -- テクスチャアトラス
  wid0, high0 = atlas:getDimensions()
  quad1 = love.graphics.newQuad(0, 0, 32, 32, wid0, high0)   -- 草原
  quad2 = love.graphics.newQuad(64, 32, 32, 32, wid0, high0) -- 木
end

function love.draw()
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- 画面サイズを (width,height) と思って描画する
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, width, height)  -- ゲーム画面を白く塗る
  for i = 1, 20 do
    for j = 1, 15 do
      local x, y = (i - 1) * 32, (j - 1) * 32
      love.graphics.draw(atlas, quad1, x, y)  -- 草原のタイルを描く
      if map[(j - 1) * 20 + i] == 1 then
        love.graphics.draw(atlas, quad2, x, y) -- 木のタイルを描く
      end
    end
  end
end

3. 画像タイルを描く:SpriteBatch

 上のプログラムでは、背景を描くために、love.draw() の中でループを回して画像を1枚ずつ描いている。これらの画像は、すべて1つのテクスチャアトラスから取られている。LÖVE には、このように「1つの画像の一部を何度も繰り返して描く」ことに特化した仕組みがある。これを SpriteBatch(スプライトバッチ)と呼ぶ。

 1つのスプライトバッチには、画像1つと、その画像の「どの部分」を「どこに描くか」という情報が含まれている。

 上のプログラムで描いた「背景」をスプライトバッチで描くには、love.load() の中で、次のようにすればよい。

  atlas = love.graphics.newImage("pipo-map001.png") -- テクスチャアトラス
  batch = love.graphics.newSpriteBatch(atlas)        -- スプライトバッチ
  wid0, high0 = atlas:getDimensions()
  quad1 = love.graphics.newQuad(0, 0, 32, 32, wid0, high0)   -- 草原
  quad2 = love.graphics.newQuad(64, 32, 32, 32, wid0, high0) -- 木
  for i = 1, 20 do
    for j = 1, 15 do
      local x, y = (i - 1) * 32, (j - 1) * 32
      batch:add(quad1, x, y)   -- 草原のタイル
      if map[(j - 1) * 20 + i] == 1 then
        batch:add(quad2, x, y) -- 木のタイル
      end
    end
  end

 love.graphics.draw() でスプライトバッチを一斉に描画できる。

  love.graphics.draw(batch)

 プログラム例は次の通り。

-- サンプルプログラム 11-03 main.lua
-- pipo-map001.png が必要
-- function set_screen_size() 〜 end までは 11-01 と同じ
function love.load()
  set_screen_size()
  -- マップ(0: 草原、1: 木)
  map = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,
    0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,1,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,
    0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
  atlas = love.graphics.newImage("pipo-map001.png") -- テクスチャアトラス
  batch = love.graphics.newSpriteBatch(atlas)        -- スプライトバッチ
  wid0, high0 = atlas:getDimensions()
  quad1 = love.graphics.newQuad(0, 0, 32, 32, wid0, high0)   -- 草原
  quad2 = love.graphics.newQuad(64, 32, 32, 32, wid0, high0) -- 木
  for i = 1, 20 do
    for j = 1, 15 do
      local x, y = (i - 1) * 32, (j - 1) * 32
      batch:add(quad1, x, y)   -- 草原のタイル
      if map[(j - 1) * 20 + i] == 1 then
        batch:add(quad2, x, y) -- 木のタイル
      end
    end
  end
end

function love.draw()
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- 画面サイズを (width,height) と思って描画する
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, width, height)  -- ゲーム画面を白く塗る
  love.graphics.draw(batch)  -- 一気に描画
end

 プログラム例 11-02(画像を1枚ずつ描画)と 11-03(スプライトバッチ使用)で、love.draw() 1回の処理にかかる時間を比較してみた。これは大差がつきました。スプライトバッチを活用すべきですね。

機種
(CPU)
1枚ずつスプライトバッチ
MacBook
(2.4 GHz Core 2 Duo)
1.9 ミリ秒0.077 ミリ秒
Raspberry Pi Model A+
(700 MHz ARMv6)
23 ミリ秒0.26 ミリ秒
Raspberry Pi 3
(1.2GHz ARMv7)
16 ミリ秒0.036 ミリ秒

4. 背景をスクロール表示する

 背景をスクロール表示するにはどうすればいいだろうか。スプライトバッチは、描画する時に位置を指定することができる。下のように、2つのスプライトバッチを位置をずらして表示すれば、スクロールを実現することができる。

 ただ、ゲーム画面が実画面よりも小さい時は、スプライトバッチの表示がゲーム画面からはみ出さないようにする必要がある。これは、love.graphics.setScissor() という関数で実現できる。具体的には、love.draw() の最初に、描画範囲をゲーム画面の中だけに設定し、love.draw() から抜ける直前に、元に戻す。

function love.draw()
  love.graphics.setScissor(transx, transy, width * scale, height * scale) -- 描画範囲を設定
  ... -- 描画処理
  love.graphics.setScissor() -- 描画範囲をリセット
end

 これを使ったプログラム例を下に示す。

-- サンプルプログラム 11-04 main.lua
-- pipo-map001.png が必要
-- function set_screen_size() 〜 end までは 11-01 と同じ
function love.load()
  set_screen_size()
  -- マップ(0: 草原、1: 木)
  map = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,
    0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,1,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,
    0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
  atlas = love.graphics.newImage("pipo-map001.png") -- テクスチャアトラス
  batch = love.graphics.newSpriteBatch(atlas)        -- スプライトバッチ
  wid0, high0 = atlas:getDimensions()
  quad1 = love.graphics.newQuad(0, 0, 32, 32, wid0, high0)   -- 草原
  quad2 = love.graphics.newQuad(64, 32, 32, 32, wid0, high0) -- 木
  for i = 1, 20 do
    for j = 1, 15 do
      local x, y = (i - 1) * 32, (j - 1) * 32
      batch:add(quad1, x, y)   -- 草原のタイル
      if map[(j - 1) * 20 + i] == 1 then
        batch:add(quad2, x, y) -- 木のタイル
      end
    end
  end
  basex = 0    -- スクロール位置
end

function love.update(dt)
  basex = basex + 100 * dt
  if basex >= width then basex = basex - width end
end

function love.draw()
  love.graphics.setScissor(transx, transy, width * scale, height * scale) -- 描画範囲を設定
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- 画面サイズを (width,height) と思って描画する
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, width, height)  -- ゲーム画面を白く塗る
  love.graphics.draw(batch, -basex, 0)          -- 一気に描画
  love.graphics.draw(batch, -basex + width, 0)  -- 一気に描画
  love.graphics.setScissor() -- 描画範囲をリセット
end

5. キャラクタと背景を重ね書きする

 キャラクタを重ね書きして、動かしてみましょう。さすがに、草原+木の背景にさかなのキャラクタはおかしいので、「ぴぽや倉庫」さんから飛行艇のキャラクタをいただいてきました。


ぴぽや http://blog.pipoya.net/

-- サンプルプログラム 11-05 main.lua
-- pipo-map001.png, pipo-airship02.png が必要
-- function set_screen_size() 〜 end までは 11-01 と同じ
function love.load()
  set_screen_size()
  -- マップ(0: 草原、1: 木)
  map = {
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,
    0,0,0,0,1,1,1,1,1,1,1,0,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,1,
    0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,
    0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1,
    0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
    0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
  atlas = love.graphics.newImage("pipo-map001.png") -- テクスチャアトラス
  batch = love.graphics.newSpriteBatch(atlas)        -- スプライトバッチ
  wid0, high0 = atlas:getDimensions()
  quad1 = love.graphics.newQuad(0, 0, 32, 32, wid0, high0)   -- 草原
  quad2 = love.graphics.newQuad(64, 32, 32, 32, wid0, high0) -- 木
  for i = 1, 20 do
    for j = 1, 15 do
      local x, y = (i - 1) * 32, (j - 1) * 32
      batch:add(quad1, x, y)   -- 草原のタイル
      if map[(j - 1) * 20 + i] == 1 then
        batch:add(quad2, x, y) -- 木のタイル
      end
    end
  end
  basex = 0    -- スクロール位置
  ship = love.graphics.newImage("pipo-airship02.png")  -- 飛行艇
  quad3 = love.graphics.newQuad(0, 64, 32, 32, ship:getWidth(), ship:getHeight())
  sx, sy = 0, math.floor(height / 2) - 32 -- 飛行艇の位置
  speed = 200
end

function love.update(dt)
  basex = basex + 100 * dt
  if basex >= width then basex = basex - width end
  local dx, dy = 0, 0
  if love.keyboard.isDown("right") then    -- 右矢印キーが押されている
    dx = speed * dt
  elseif love.keyboard.isDown("left") then -- 左矢印キーが押されている
    dx = -speed * dt
  elseif love.keyboard.isDown("up") then   -- 上矢印キーが押されている
    dy = -speed * dt
  elseif love.keyboard.isDown("down") then -- 下矢印キーが押されている
    dy = speed * dt
  end
  sx = sx + dx
  sy = sy + dy
  if sx < 0 or sx > width - 32 then sx = sx - dx end
  if sy < 0 or sy > height - 32 then sy = sy - dy end
end

function love.draw()
  love.graphics.setScissor(transx, transy, width * scale, height * scale) -- 描画範囲を設定
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- 画面サイズを (width,height) と思って描画する
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle("fill", 0, 0, width, height)  -- ゲーム画面を白く塗る-
  love.graphics.draw(batch, -basex, 0)          -- 一気に描画
  love.graphics.draw(batch, -basex + width, 0)  -- 一気に描画
  love.graphics.draw(ship, quad3, sx, sy, 0, 2, 2)  -- 飛行艇(2倍拡大)
  love.graphics.setScissor()    -- 描画範囲をリセット
end

目次