LÖVE (Love2D) 実践編・さめがめ:2.現在位置・つながったコマを表示

(2018.9.2. 公開)

1. 現在位置を表示する

 消すコマを選ぶため、「現在位置」を表示することにする。現在位置を矢印キーで動かして、リターン(エンター)キーを押したら、そのコマと上下左右につながっているコマを消す、という仕様で作っていこう。

 まず、「現在位置」を表す変数を決めておく。これは love.load() の中に置く。

  -- === 現在位置初期化 ===
  cx = 0
  cy = 0

 画面上で現在位置を表示する方法は、いろいろ考えられる。今回は、背景の色を変えることにした。1つのコマだけ背景の色を変えるには、全体の背景を塗りつぶしてからコマを描くまでの間に、そのコマの分だけ別の色で塗りつぶしておけばよい。

  -- === 現在位置の表示 ===
  love.graphics.setColor(1, 1, 1)  -- 白色
  love.graphics.rectangle("fill", cx * 32, (cy + 1) * 32, 32, 32, 8, 8) -- 角丸四角形を塗りつぶす

2. 矢印キーで現在位置を動かす

 キー入力は「キー入力と衝突判定」で扱ったが、その時は「今キーが押されているかどうか」を判定していた。このゲームの場合は、矢印キーが「押された瞬間」にだけその方向に移動するようにしたい。この場合は、love.keypressed() というコールバックを使えばよい。

  function love.keypressed(key, scancode, isrepeat)
    -- key: キーの名前
    -- scancode: スキャンコード
    -- isrepeat: オートリピートなら true
  end

 何も設定しなければ、キーを押しっぱなしにしても love.keypressed() は一度しか呼ばれない。つまり、オートリピートは効かない。オートリピートを効かせるには、どこか(普通は love.load() の中)で love.keyboard.setKeyRepeat(true) を実行する必要がある。

  love.keyboard.setKeyRepeat(true)  -- キーのオートリピートを有効にする

 右矢印キーが押されたら、cx を1増やす。この値が xx(横に並べたコマ数)以上になったら、右端を越えたということになる。この場合は、cx を0に戻して、左端から出てくるようにする。また、左矢印キーが押されたら、cx を1減らす。この値が -1 になったら、右端から出てくるようにする。

 以上をまとめて、次のように書くことができる。余り演算子 % の使い方に注意。Lua/LuaJIT では、正の数で割った時の余りは、「ゼロ以上、除数未満」の数になる。(Lua/LuaJIT では、余りは常に除数と同じ符号になる。他のプログラム言語では余りが被除数と同じ符号になる場合もある。)

  dx = 0
  if key == "right" then      -- 右矢印キーが押されている
    dx = 1
  elseif key == "left" then   -- 左矢印キーが押されている
    dx = -1
  end
  cx = (cx + dx) % xx -- 画面からはみ出したら、反対側から出てくる

 上下方向の処理も合わせると、love.keypressed(key, scancode, isrepeat) は下のようになる。

function love.keypressed(key, scancode, isrepeat)
  local dx, dy = 0, 0
  if key == "right" then      -- 右矢印キーが押されている
    dx = 1
  elseif key == "left" then   -- 左矢印キーが押されている
    dx = -1
  elseif key == "up" then     -- 上矢印キーが押されている
    dy = -1
  elseif key == "down" then   -- 下矢印キーが押されている
    dy = 1
  end
  if dx ~= 0 or dy ~= 0 then
    -- 新しい位置を計算する。画面からはみ出したら、反対側から出てくる
    cx = (cx + dx) % xx
    cy = (cy + dy) % yy
  end
end

3. つながったコマを探す

 つながったコマは board の値に 100 を加えて表すことにする。

 cx, cy からつながったコマを探すには、いろいろな方法が考えられる。最も単純なコードは、次のようなものだろう。現在位置から4方向をそれぞれ調べて、「同じコマ」で「まだ処理していない」ものがあれば、そこに移動して同じ処理を繰り返す。関数の中で自分自身を呼び出す、「再帰呼び出し」と呼ばれる手法である。この関数は、つながっているコマの数を返す。

-- (x, y) とつながっているコマをマークする(再帰呼び出し)
function mark_from_here(x, y)
  local c, n
  c = board[y * xx + x + 1]
  board[y * xx + x + 1] = c + 100
  n = 1
  if x > 0 and board[y * xx + x] == c then
    -- 左のマスに同じコマがある
    n = n + mark_from_here(x - 1, y)
  end
  if x < xx - 1 and board[y * xx + x + 2] == c then
    -- 右のマスに同じコマがある
    n = n + mark_from_here(x + 1, y)
  end
  if y > 0 and board[(y - 1) * xx + x + 1] == c then
    -- 上のマスに同じコマがある
    n = n + mark_from_here(x, y - 1)
  end
  if y < yy - 1 and board[(y + 1) * xx + x + 1] == c then
    -- 下のマスに同じコマがある
    n = n + mark_from_here(x, y + 1)
  end
  return n
end

 mark_from_here() を呼ぶ前に、全部のマークを外しておかないといけない。つながったコマにマークをつける処理は、「最初に現在位置を初期化したとき」と、「キーで現在位置を動かした時」の2箇所から呼び出されるので、関数にしておくのがよい。

-- (cx, cy) とつながっているコマをマークする
function mark()
  -- === すべてのマークを外す ===
  for i = 1, xx * yy do
    board[i] = board[i] % 100
  end
  -- === (cx, cy) にコマがあれば、そことつながっているコマをマークする ===
  if board[cy * xx + cx + 1] ~= 0 then
    cont = mark_from_here(cx, cy)  -- つながっているコマの数
  else
    cont = 0
  end
end

 そして、mark() の呼び出しを2箇所に挿入する。

  -- === 現在位置初期化 ===
  cx = 0
  cy = 0
  mark()  -- マークをつける
  if dx ~= 0 or dy ~= 0 then
    -- 新しい位置を計算する。画面からはみ出したら、反対側から出てくる
    cx = (cx + dx) % xx
    cy = (cy + dy) % yy
    mark()  -- マークをつけ直す
  end

 なお、再帰呼び出しは、「現在の関数の状態」をメモリ上に保存した上で自分自身を呼び出すため、あまり再帰を繰り返すと stack overflow というエラーを出して落ちてしまう。試しに下の関数を love.load() から呼んでみたところ、Mac では 32739, ラズパイでは 32740 でエラーが出た。

function recur(n)
  if n == 0 then return 0 end
  return n + recur(n - 1)
end

4. つながったコマを表示する

 つながったコマの表示も、背景色を変えて対応する。ここでも角丸四角形を使うが、単純に並べると、下の図のようにすき間ができてしまう。

 これを防ぐには、同じコマが隣にある時は、角を丸めないようにすればよい。具体的には、角丸を覆う細長い四角形を塗る。

 上・左・下についても同じように処理すればよい。コードは下のようになる。少し長くなったので、これも関数にした。

-- つながっているコマを強調表示
function show_mark()
  local r = 8
  for x = 0, xx - 1 do
    for y = 0, yy - 1 do
      local i = y * xx + x + 1
      if board[i] > 100 then
        local x1, y1 = x * 32, (y + 1) * 32
        love.graphics.rectangle("fill", x1, y1, 32, 32, r, r)
        if x > 0 and board[i - 1] > 100 then
          love.graphics.rectangle("fill", x1, y1, r, 32)
        end
        if x < xx - 1 and board[i + 1] > 100 then
          love.graphics.rectangle("fill", x1 + 32 - r, y1, r, 32)
        end
        if y > 0 and board[i - xx] > 100 then
          love.graphics.rectangle("fill", x1, y1, 32, r)
        end
        if y < yy - 1 and board[i + xx] > 100 then
          love.graphics.rectangle("fill", x1, y1 + 32 - r, 32, r)
        end
      end
    end
  end
end

 この関数の呼び出しは、現在位置の表示よりも前に置く。(後ろに置くと、現在位置の表示がこの表示で覆い隠されてしまう。)

  -- === マークの表示 ===
  love.graphics.setColor(0.39, 1, 1)  -- 水色
  show_mark()
  -- === 現在位置の表示 ===
  love.graphics.setColor(1, 1, 1)  -- 白色
  love.graphics.rectangle("fill", cx * 32, (cy + 1) * 32, 32, 32, 8, 8) -- 角丸四角形を塗りつぶす

5. プログラム

 以上を合わせたのが次のプログラム。

-- サンプルプログラム 101-02 main.lua
-- fruits32.png, ipag.ttf が必要
function set_screen_size()
  width = 640                         -- ゲーム画面の横幅
  height = 480                        -- ゲーム画面の高さ
  love.window.setMode(width, height)  -- これが効くなら問題なし
  love.window.setTitle("さめがめ on LÖVE")
  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()                 --  画面の大きさを設定
  xx = math.floor(width / 32)       --  盤面の幅
  yy = math.floor(height / 32) - 1  --  盤面の高さ
  love.keyboard.setKeyRepeat(true)  -- キーのオートリピートを有効にする
  -- === フォント指定 (IPAゴシック 16ポイント) ===
  font = love.graphics.newFont("ipag.ttf", 16)
  love.graphics.setFont(font)
  --  === 初期盤面を作る ===
  ntiles = 5                        --  コマの種類
  board = {}                        --  盤面
  point = 0                         --  スコア
  num = xx * yy                     --  残りコマ数
  rest = {}                         --  種類ごとの残りコマ数
  for i = 1, ntiles do              --  残りコマ数をゼロクリア
    rest[i] = 0
  end
  for i = 1, xx * yy do             -- xx*yy 回繰り返し
    board[i] = love.math.random(ntiles)  -- ランダムにコマを配置
    rest[board[i]] = rest[board[i]] + 1  -- 残りコマ数を1増やす
  end
  --  === 画像関連初期化 ===
  tiles = love.graphics.newImage("fruits32.png") -- コマ画像を読み込む
  wid0, high0 = tiles:getDimensions()            -- コマ画像のサイズ
  batch = love.graphics.newSpriteBatch(tiles)    -- スプライトバッチ
  quads = {}                        -- Quad を保持しておくテーブル
  for i = 1, ntiles do              -- Quad をコマの種類分だけ作る
    quads[i] = love.graphics.newQuad((i - 1) * 32, 0, 32, 32, wid0, high0)
  end
  --  === スプライトバッチの初期化 ===
  for y = 0, yy - 1 do
    for x = 0, xx - 1 do
      local i = y * xx + x + 1
      batch:add(quads[board[i]], x * 32, (y + 1) * 32)
    end
  end
  -- === 現在位置初期化 ===
  cx = 0
  cy = 0
  mark()  -- マークをつける
end

-- (x, y) とつながっているコマをマークする(再帰呼び出し)
function mark_from_here(x, y)
  local c, n
  c = board[y * xx + x + 1]
  board[y * xx + x + 1] = c + 100
  n = 1
  if x > 0 and board[y * xx + x] == c then
    -- 左のマスに同じコマがある
    n = n + mark_from_here(x - 1, y)
  end
  if x < xx - 1 and board[y * xx + x + 2] == c then
    -- 右のマスに同じコマがある
    n = n + mark_from_here(x + 1, y)
  end
  if y > 0 and board[(y - 1) * xx + x + 1] == c then
    -- 上のマスに同じコマがある
    n = n + mark_from_here(x, y - 1)
  end
  if y < yy - 1 and board[(y + 1) * xx + x + 1] == c then
    -- 下のマスに同じコマがある
    n = n + mark_from_here(x, y + 1)
  end
  return n
end

-- (cx, cy) とつながっているコマをマークする
function mark()
  -- === すべてのマークを外す ===
  for i = 1, xx * yy do
    board[i] = board[i] % 100
  end
  -- === (cx, cy) にコマがあれば、そことつながっているコマをマークする ===
  if board[cy * xx + cx + 1] ~= 0 then
    cont = mark_from_here(cx, cy)  -- つながっているコマの数
  else
    cont = 0
  end
end

-- マークされているコマの位置を塗る
function show_mark()
  local r = 8
  for x = 0, xx - 1 do
    for y = 0, yy - 1 do
      local i = y * xx + x + 1
      if board[i] > 100 then
        local x1, y1 = x * 32, (y + 1) * 32
        love.graphics.rectangle("fill", x1, y1, 32, 32, r, r)
        if x > 0 and board[i - 1] > 100 then
          love.graphics.rectangle("fill", x1, y1, r, 32)
        end
        if x < xx - 1 and board[i + 1] > 100 then
          love.graphics.rectangle("fill", x1 + 32 - r, y1, r, 32)
        end
        if y > 0 and board[i - xx] > 100 then
          love.graphics.rectangle("fill", x1, y1, 32, r)
        end
        if y < yy - 1 and board[i + xx] > 100 then
          love.graphics.rectangle("fill", x1, y1 + 32 - r, 32, r)
        end
      end
    end
  end
end

function love.keypressed(key, scancode, isrepeat)
  local dx, dy = 0, 0
  if key == "right" then      -- 右矢印キーが押されている
    dx = 1
  elseif key == "left" then   -- 左矢印キーが押されている
    dx = -1
  elseif key == "up" then     -- 上矢印キーが押されている
    dy = -1
  elseif key == "down" then   -- 下矢印キーが押されている
    dy = 1
  end
  if dx ~= 0 or dy ~= 0 then
    -- 新しい位置を計算する。画面からはみ出したら、反対側から出てくる
    cx = (cx + dx) % xx
    cy = (cy + dy) % yy
    mark()  -- マークをつけ直す
  end
end

function love.draw()
  -- === 描画範囲を設定 ===
  love.graphics.setScissor(transx, transy, width * scale, height * scale)
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
  -- === 盤面を塗りつぶす ===
  love.graphics.setColor(0.39, 0.42, 0.92)
  love.graphics.rectangle("fill", 0, 0, width, 32)  -- 点数、残りコマ数など
  love.graphics.setColor(0.47, 0.50, 1.00)
  love.graphics.rectangle("fill", 0, 32, width, height - 32)  -- 盤面
  -- === スコア・残りコマ数など表示 ===
  love.graphics.setColor(1, 1, 1)
  love.graphics.print(string.format("スコア %-d", point), 4, 8)
  love.graphics.print(string.format("残り %-d", num), 120, 8)
  for i = 1, ntiles do
    love.graphics.draw(tiles, quads[i], 140 + i * 60, 0)
    love.graphics.print(string.format("%-d", rest[i]), 140 + 36 + i * 60, 8)
  end
  love.graphics.print("[R]etry [N]ew", 220 + ntiles * 60, 8)
  -- === マークの表示 ===
  love.graphics.setColor(0.39, 1, 1)  -- 水色
  show_mark()
  -- === 現在位置の表示 ===
  love.graphics.setColor(1, 1, 1)  -- 白色
  love.graphics.rectangle("fill", cx * 32, (cy + 1) * 32, 32, 32, 8, 8) -- 角丸四角形を塗りつぶす
  -- === コマの表示 ===
  love.graphics.draw(batch, 0, 0)
  -- === 描画範囲をリセット ===
  love.graphics.setScissor()
end

目次