LÖVE (Love2D) 実践編・さめがめ:1.ルール説明・盤面の表示

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

 プログラムの作成例として、「さめがめ」(まきがめ)を作ってみます。同じコマの並びを見つけて消していくゲームです。昔からパソコンで遊んでいる人は知っているでしょう。(原作者さんたちのウェブサイトがあります:オリジナル作者のページMac 版「まきがめ」のページ。)

1. ルールの説明

2. どうやって作っていくか

 具体的に「見える」ところから進めていくと、作りやすいし、その先のイメージもわきやすい。だから、まずは初期盤面、「5種類のコマを格子状にランダムに並べて表示する」ところから始めましょう。

3. コマの画像を用意

 まず、コマの画像を用意しないといけない。32x32のドット絵が5つあればいいので、適当なものを調達する。今回は、無料素材倶楽部さんの「果物(フルーツ)イラストアイコン集」を使わせていただきました。5種類好きなのを選んで、32x32に縮小して、5つ並べる。この 160x32 の画像 (fruit32.png) を、テクスチャアトラスとして使います。

4. 画面デザイン

 画面デザインを楽にするため、盤面の大きさは 640x480 で固定にする。入門編の「背景をスクロールする」で作成した set_screen_size() 関数を使う。

 コマの大きさは 32x32 なので、画面いっぱいに並べると縦 15 個、横 20 個になる。このうち、縦を1個減らして、一番上の 32 ドット分をスコアや残りコマ数を表示するスペースにする。

 横・縦に並べる数をそれぞれ xx, yy で表す。この場合は xx = 20, yy = 14 となるけど、set_screen_size() の中で画面の横・縦サイズを決めているので、 xx, yy はそこから計算した方がよい。これは、love.load() の中で処理する。

  --  === 画面サイズ設定 ===
  set_screen_size()                 --  画面の大きさを設定
  xx = math.floor(width / 32)       --  盤面の幅
  yy = math.floor(height / 32) - 1  --  盤面の高さ

 math.floor() は、数値の小数点以下を切り捨てて整数にする関数。「画面サイズの設定」でも使った。

5. コマの情報をどうやって表すか

 盤面のコマを表すには、数値をキーとするテーブル(配列)を使えばよい。テーブルの名前を board として、board[1] から board[xx * yy] にコマの種類を入れる。コマの種類は、0 ならば空白、1..5 ならば1番目〜5番目のコマを表すものとする。

 LuaJIT でテーブルを作るには、board = {} と書く。

6. 初期盤面を作る

 初期盤面を作るには、board[1] から board[xx * yy] まで、1つずつ 1..5 の数値をランダムに入れてやればよい。ランダムな数を発生させるには、love.math.random(n) を使う。この関数は、引数 n を与えると、1 から n までの整数をランダムに1つ返してくれる。

 LuaJIT の組み込み関数に math.random(n) というのがあるが、乱数の品質が OS に依存する。love.math.random(n) だと、どの機種で動かしても同じ品質を保証できる。

 コマは5種類と決まっているが、あとで変更する可能性も考えて、ntiles という変数に入れることにする。ntiles は英語の "number of tiles"(コマの数)を省略したもの。変数名は何でもよいのだが、「何かの数」を表す変数には、このように "n" + 名前 + "s" という形の名前をつける人が多い。もちろん、ローマ字で koma_no_kazu とか komakazu などとしても全く問題ない。

  --  === 初期盤面を作る ===
  ntiles = 5                        --  コマの種類
  board = {}                        --  盤面
  for i = 1, xx * yy do             -- xx*yy 回繰り返し
    board[i] = love.math.random(ntiles)  -- ランダムにコマを配置
  end

7. スプライトバッチの初期化

 表示には「背景をスクロールする」で紹介したスプライトバッチを使いたい。love.load() の中で、コマの画像が入っているテクスチャアトラスを読み込み、これを画像として使うスプライトバッチを作成する。

 5種類のコマを表示するための Quad も作成しておく。

  --  === 画像関連初期化 ===
  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

 そして、スプライトバッチに Quad を加えていく。この時、ループの回し方に工夫の余地がある。盤面を作成した時は、単純に「xx * yy 回」繰り返せばよかった。しかし、今回は、それぞれのコマを表示する画面座標が必要である。テーブルのキーと座標は、下の図のような関係になっている。

 先ほどと同じように単純な一重(いちじゅう)ループで回すのであれば、座標はループ変数から計算する必要がある。(a % b は「ab で割った余り」を表す。)

 一方、まず x 方向についてループし、それをさらに y 方向についてループする、二重(にじゅう)ループも可能である。この場合は、座標はループ変数から直接得られるが、テーブルのキーを計算する必要がある。

 どちらのやり方でも構わないが、今回は二重ループを使ってみる。画面の最上部に 32 ドット分の空きをつくるため、コマの y 座標は (y + 1) * 32 で表されることに注意。

  --  === スプライトバッチの初期化 ===
  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

 なお、この二重ループは、y が外で x が内になっている。これを逆にすると、スプライトの順序がテーブルのキーとずれてしまうため、話が面倒になる。

8. 盤面の表示

 盤面の表示は love.draw() 中に書く。まず、set_screen_size() 関数に対応して、表示範囲を限定する love.graphics.setScissor() を使う必要がある(参考:「背景をスクロール表示する」)。

  -- === 描画範囲を設定 ===
  love.graphics.setScissor(transx, transy, width * scale, height * scale)
  love.graphics.translate(transx, transy) -- 原点移動
  love.graphics.scale(scale, scale) -- 拡大率を設定
    ... -- 描画処理
  -- === 描画範囲をリセット ===
  love.graphics.setScissor()

 次に、盤面を適当な色で塗りつぶす。ここでは、盤面を少し明るい青色、一番上のスコアなどを表示する部分を濃いめの青色にした。

  -- === 盤面を塗りつぶす ===
  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)  -- 盤面

 スコアの行は、こんな風に表示したい。左から順に、現在のスコア・残りコマ数・各コマの残り数・R(再開)、N(別の盤面)のキー操作説明。下に示した数字は、x 座標です。文字は 16 ポイントで書くつもりで、全角文字が 約 16 ドット、半角文字が約 8 ドットの幅と見積もっている。

  -- === スコア・残りコマ数など表示 ===
  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)

 string.format("文字列", 数値) は、LuaJIT の組み込み関数で、指定したフォーマットで数値を文字列に埋め込む。"%-d" というところがフォーマット指定で、マイナス記号は「左詰めにする」、d は「整数を文字列に変換する」という意味。今回は特に凝ったフォーマットは使っていないが、本来は「数字の右端を合わせて表示する」とか、「小数点以下2ケタ分だけ表示する」というような用途に使う。

 フォント指定を忘れてはいけない。これは love.load() の最初の方に書いておく。

  -- === フォント指定 (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

 そして、肝心のスプライトバッチの表示。これは1行で済む。

  -- === コマの表示 ===
  love.graphics.draw(batch, 0, 0)

9. プログラム

 以上を合わせたのが次のプログラム。set_screen_size() の中で、love.window.setTitle() を使ってウィンドウタイトルを設定しています。"LÖVE" がタイプできない場合は、"Love2D" でもいいでしょう。

-- サンプルプログラム 101-01 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  --  盤面の高さ
  -- === フォント指定 (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
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.draw(batch, 0, 0)
  -- === 描画範囲をリセット ===
  love.graphics.setScissor()
end

目次