LÖVE (Love2D) 入門編:10.ジョイスティックを使う

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

 同じ内容のゲームでも、PC のキーボードを使う代わりにジョイスティックやゲームパッドでコントロールすると、途端にゲームらしくなる。LÖVE でももちろん、ジョイスティックなどをサポートする機能が用意されている。

 世の中にはいろいろなジョイスティック・ゲームパッドが売られている。今回動作を確認したのは下のような、スーパーファミコン風のゲームパッドです(バッファロー製)。XY 方向の十字キーと、A, B, X, Y, L, R, Select, Start の8つのボタンがあります。

1. ジョイスティックを認識する

 たいていのジョイスティックは USB で接続するようになっており、PC などに接続すると自動的に認識される。LÖVE プログラムからジョイスティックを認識するには、2つの方法がある。

 ここでは、2番目の方法を使って見よう。love.joystickadded(j) はコールバック関数である。「コールバック関数」とは、ある条件の元で自動的に呼び出される関数のことだった。この場合は、「ジョイスティックが接続されたとき」に、この関数が呼び出される。引数の j は、Joystick タイプのオブジェクトである。

 次のプログラムは、ジョイスティックが接続されると、そのジョイスティックの名前や仕様を表示する。

-- サンプルプログラム 10-01 main.lua
function love.load()
  love.graphics.setBackgroundColor(1, 1, 1)        -- 背景を白に
  love.graphics.setColor(0, 0, 0)                  -- 文字色を黒に
  love.graphics.setFont(love.graphics.newFont(24)) -- フォントサイズを24に
end
-- ジョイスティックが接続された時に呼ばれるコールバック関数
function love.joystickadded(j)
  joystick = j   -- 接続されたジョイスティックオブジェクトを joystick に記録
end
function love.draw()
  if joystick and joystick:isConnected() then -- ジョイスティックが接続されているなら
    local name = joystick:getName()           -- 名前
    local ac = joystick:getAxisCount()        -- 軸の数
    local bc = joystick:getButtonCount()      -- ボタンの数
    love.graphics.print("Name = " .. name, 10, 10)
    love.graphics.print("Axis count = " .. ac, 10, 40)
    love.graphics.print("Button count = " .. bc, 10, 70)
  end
end

 Joystick:getName()Joystick タイプのオブジェクトが持つメソッドで、ジョイスティックの名前を文字列で返す。実際に使う時は、Joystick のところを、オブジェクトに置き換えること。上のプログラムでは、joystick という変数に「現在接続されているジョイスティックを表すオブジェクト」が入っているので、これを使って joystick:getName() と書いている。メソッドなので、コロン : を使うことに注意。

 ジョイスティックの仕様は、「軸の数」と「ボタンの数」で表される。「軸の数」は、レバーを動かせる方向の数で、通常ゲームに使うジョイスティックでは2つ(上下方向と左右方向)である。ボタンの数や配置は製品によって違うので、自分が使う機器で実験する必要がある。

 実験用のプログラムを下に示す。上記のプログラムに加えて、各軸の値と、どのボタンが押されているかを表示する。

-- サンプルプログラム 10-02 main.lua
function love.load()
  love.graphics.setBackgroundColor(1, 1, 1)        -- 背景を白に
  love.graphics.setColor(0, 0, 0)                  -- 文字色を黒に
  love.graphics.setFont(love.graphics.newFont(24)) -- フォントサイズを24に
end
-- ジョイスティックが接続された時に呼ばれるコールバック関数
function love.joystickadded(j)
  joystick = j   -- 接続されたジョイスティックオブジェクトを joystick に記録
end
function love.draw()
  if joystick and joystick:isConnected() then -- ジョイスティックが接続されているなら
    local name = joystick:getName()           -- 名前
    local ac = joystick:getAxisCount()        -- 軸の数
    local bc = joystick:getButtonCount()      -- ボタンの数
    love.graphics.print("Name = " .. name, 10, 10)
    love.graphics.print("Axis count = " .. ac, 10, 40)
    love.graphics.print("Button count = " .. bc, 10, 70)
    s1 = "Axis"
    for i = 1, joystick:getAxisCount() do
      s1 = s1 .. string.format(" [%d]%-5d", i, joystick:getAxis(i))
    end
    love.graphics.print(s1, 10, 100)
    s1 = "Button"
    for i = 1, joystick:getButtonCount() do
      s1 = s1 .. string.format(" [%d]", i)
      if joystick:isDown(i) then
        s1 = s1 .. "O"
      else
        s1 = s1 .. "-"
      end
    end
    love.graphics.print(s1, 10, 130)
  end
end

 十字キーの下方向と、Aボタンを押すと、下のような表示になる。

2. レバーの向きを調べる

 上のプログラム例でも使用したが、レバー(ゲームパッド型の場合は十字キー)の向きを調べるには、Joystick:getAxis(n) を使う。n は軸の番号で、左右方向が 1, 上下方向が 2。これは今回使ったゲームパッドの場合だが、多くのジョイスティック/ゲームパッドで共通ではないかと思われる。

 十字キーや、レバーでスイッチを押すタイプのジョイスティックでは、Joystick:getAxis(n) で得られる値は +1(右/下)、0(押されていない)、-1(左/上)のいずれかになる。ポテンショメーターを内蔵するジョイスティックの場合は、倒す角度で値が変わるものもあるだろう。どのような値が得られるかは、実験してみる必要がある。

 「キー入力」のところで紹介した四角を動かすサンプルプログラムを、ジョイスティックでも動かせるように改造してみた。

-- サンプルプログラム 10-03 main.lua
-- 最初に1回だけ呼び出されるコールバック関数
function love.load()
  width = love.graphics.getWidth()    -- 現在の画面の横幅
  height = love.graphics.getHeight()  -- 現在の画面の高さ
  love.graphics.setBackgroundColor(0, 0.24, 0)  -- 背景を濃い緑色に
  rsize = width / 25                  -- 四角形のサイズ
  x = 0                               -- x 座標を0にする
  y = 0                               -- y 座標を0にする
  speed = (width - rsize) / 3         -- 移動速度:1秒間に横幅の1/3
end

-- ジョイスティックが接続された時に呼ばれるコールバック関数
function love.joystickadded(j)
  joystick = j   -- 接続されたジョイスティックオブジェクトを joystick に記録
end

-- 定期的に呼び出されるコールバック関数
function love.update(dt)
  local dx, dy, xx, yy  -- これらの変数はこの関数の中でしか使わない
  dx = 0
  dy = 0
  if joystick and joystick:isConnected() then -- ジョイスティックが接続されているなら
    local ax = joystick:getAxis(1)         -- 左右方向
    if ax > 0 then
      dx = speed * dt
    elseif ax < 0 then
      dx = -speed * dt
    end
    ax = joystick:getAxis(2)               -- 上下方向
    if ax > 0 then
      dy = speed * dt
    elseif ax < 0 then
      dy = -speed * dt
    end
  end
  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
  -- 新しい位置を計算する
  xx = x + dx
  yy = y + dy
  -- 画面の中に収まっていれば x, y を新しい位置で置き換える
  if xx >= 0 and xx < width - rsize then x = xx end
  if yy >= 0 and yy < height - rsize then y = yy end
end

-- 画面を書き換えるコールバック関数
function love.draw()
  love.graphics.setColor(1, 0.5, 0.5)  -- 淡い赤色
  love.graphics.rectangle("fill", x, y, rsize, rsize)
    -- 塗りつぶした四角形を描く
end

3. ボタンが押されたかどうかを調べる

 ボタンの状態を調べるには、Joystick:isDown(n) を使うことができる。この関数は、「n 番目のボタンが押された状態である」なら true を返し、そうでなければ false を返す。先ほどのサンプルプログラム 10-02 では、この関数を使って「今ボタンが押されているかどうか」を画面に表示した。

 しかし、ボタンについては、注意すべき点がある。例えば、Aボタンが「弾を撃つ」機能を持っているとしよう。そして、love.update() の中でボタンの状態を調べて処理したとする。このとき、love.update() が例えば1秒間に60回呼び出されたとすると、ボタンを押している間じゅう1秒間に60回の連射をすることになる。高橋名人やウメハラさんもびっくりである。

 これを避けるには、「直前に弾を撃った時刻」を覚えておいて、一定時間が経過するまでは次の弾を出さないようにすればよい。「現在の時刻」は love.timer.getTime() で取得できるので、例えば次のようになる。0.125秒=1/8秒ごとにすれば、8連射となる。

function love.load()
  ...
  lasttime = love.timer.getTime() -- 直前の時刻の初期値
end
function love.update(dt)
  local t = love.timer.getTime()  -- 現在の時刻
  if t - lasttime >= 0.125 then   -- 0.125秒以上経過したか?
    -- 弾を発生させる処理
    lasttime = t                  -- 直前に撃った時刻を更新
  end
  ...
end

 プログラム例を示す。A ボタン、またはキーボードのスペースキーが押されていれば弾を発射するようにした。

-- サンプルプログラム 10-04 main.lua
-- 最初に1回だけ呼び出されるコールバック関数
function love.load()
  width = love.graphics.getWidth()    -- 現在の画面の横幅
  height = love.graphics.getHeight()  -- 現在の画面の高さ
  love.graphics.setBackgroundColor(0, 0.24, 0)  -- 背景を濃い緑色に
  rsize = width / 25                  -- 四角形のサイズ
  x = 0                               -- x 座標を0にする
  y = 0                               -- y 座標を0にする
  speed = (width - rsize) / 3         -- 移動速度:1秒間に横幅の1/3
  bullets = {}                        -- 弾の位置:{x, y} の配列
  bsize = 3                           -- 弾のサイズ
  bspeed = (width - rsize) / 1.5      -- 弾の移動速度
  lasttime = love.timer.getTime()     -- 直前の時刻の初期値
end

-- ジョイスティックが接続された時に呼ばれるコールバック関数
function love.joystickadded(j)
  joystick = j   -- 接続されたジョイスティックオブジェクトを joystick に記録
end

-- 定期的に呼び出されるコールバック関数
function love.update(dt)
  local dx, dy, xx, yy  -- これらの変数はこの関数の中でしか使わない
  dx = 0
  dy = 0
  if joystick and joystick:isConnected() then -- ジョイスティックが接続されているなら
    local ax = joystick:getAxis(1)   -- 左右方向
    if ax > 0 then
      dx = speed * dt
    elseif ax < 0 then
      dx = -speed * dt
    end
    ax = joystick:getAxis(2)         -- 上下方向
    if ax > 0 then
      dy = speed * dt
    elseif ax < 0 then
      dy = -speed * dt
    end
  end
  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
  if (joystick and joystick:isDown(1)) or love.keyboard.isDown("space") then
    -- Aボタン、またはスペースバーが押されているなら
    local t = love.timer.getTime() -- 現在の時刻
    if t - lasttime >= 0.125 then   -- 0.125秒以上経過したか?
      local b = {x + rsize / 2 + bsize, y + rsize / 2}
      table.insert(bullets, b)     -- 弾の位置の配列に新しく追加
      lasttime = t                 -- 直前に撃った時刻を更新
    end
  else
    -- 何も押されていなければ lasttime を「今より1秒前」にしておく
    -- (次にボタンが押された時に確実に弾を出すため)
    lasttime = love.timer.getTime() - 1.0
  end
  -- 新しい位置を計算する
  xx = x + dx
  yy = y + dy
  -- 画面の中に収まっていれば x, y を新しい位置で置き換える
  if xx >= 0 and xx < width - rsize then x = xx end
  if yy >= 0 and yy < height - rsize then y = yy end
  -- 弾の位置を更新する(テーブルの末尾から順番に)
  for i = #bullets, 1, -1 do
    local b = bullets[i]
    xx = b[1] + bspeed * dt
    if xx >= width then           -- 画面の外に出た
      table.remove(bullets, i)    -- bullets から取り除く
    else
      b[1] = xx
    end
  end
end

-- 画面を書き換えるコールバック関数
function love.draw()
  love.graphics.setColor(1, 0.5, 0.5)  -- 淡い赤色
  love.graphics.rectangle("fill", x, y, rsize, rsize) -- 塗りつぶした四角形を描く
  love.graphics.setColor(1, 1, 1)      -- 白
  -- 弾を描く
  for i = 1, #bullets do
    local b = bullets[i]
    love.graphics.circle("fill", b[1], b[2], bsize)
  end
end

 急にゲームっぽくなってきました。といっても、敵も出てこないし背景もないので、射撃練習みたいな雰囲気ですが。


(※ 色は変更してあります)

目次