フレームレート

どれくらいあれば十分?

速ければいいというものではない

どれくらいあれば十分?

私が作ろうとしているタイプのゲームには、それほど高い FPS は必要ありません。でも、まず基本に戻りましょう。FPS とは何か? 1秒間に続けて表示されるフレーム(画面の画像)の数のことです。

30 FPS なら、1秒間に30枚の画像を続けて表示します。FPS が低すぎると、映像がカクカクしてなめらかに見えません(いつもそうとは限りませんが)。

今のところ目標は最低 60 FPS で、見てのとおり、ちゃんと達成できています 😄

ゲーム内の FPS

ゲーム内の FPS

コントロールを保つ

何がいちばん良くて簡単なのか、いくつも記事を読みました。ゲームの種類やプラットフォームによって違います。私は信頼性を重視していて、シンプルな2Dゲームなので、フレームレートを60に固定することにしました。

そのために https://github.com/bjornbytes/tick を使い、その上に自分で手を加えて、今はこうなっています。

local tick = {
  rate = 1 / 60
}

local timeAccumulator = 0

function love.run()
  love.load(love.arg.parseGameArguments(arg), arg)
  love.update(0)
  love.timer.step()

  return function()
    -- Handle events.
    love.event.pump()
    for name, a, b, c, d, e, f in love.event.poll() do
      if name == "quit" and not (love.quit and love.quit()) then
        return a or 0
      end
      love.handlers[name](a, b, c, d, e, f)
    end

    -- Get delta time.
    local _, _, flags = love.window.getMode()
    local dt = love.timer.step()

    if flags.vsync > 0 and flags.refreshrate > 0 then
      local perfectDt = 1 / flags.refreshrate

      if dt < perfectDt * 1.3 and dt > perfectDt / 1.3 then -- If dt is somewhat near perfect, force it to be actually perfect.
        dt = perfectDt
      else
        dt = math.min(dt, 1 / 20) -- In case the game is lagging, let it lag (but let the game think it's at least 20 FPS). Not clamping dt may put us in a situation where we can't do all required ticks in time and the game will more or less freeze.
      end
    end

    -- Update game.
    timeAccumulator = timeAccumulator + dt
    local didUpdate = false

    while timeAccumulator >= tick.rate do
      timeAccumulator = timeAccumulator - tick.rate
      didUpdate = true

      -- local updateStartTime = love.timer.getTime()
      love.update(tick.rate)
      -- love.timer.sleep(20*(love.timer.getTime()-updateStartTime)) -- DEBUG: Emulate a slower computer.
    end

    -- Draw game.
    if didUpdate then
      love.graphics.origin()
      love.graphics.clear(love.graphics.getBackgroundColor())
      love.draw()
      love.graphics.present()
    end

    if not (flags.vsync > 0 and didUpdate) then
      love.timer.sleep(.001) -- Not needed if vsync is on and we called present(), as present() will have slept for us.
    end
  end
end

こうしているのは、ゲームが常に 60 FPS で動くと前提にできるようにするため、そして誰かが 10000 FPS も出るグラフィックカードを使っていても、おかしなことが起きないようにするためです。この方法なら「常に」安定します。

Deltatime

動くものにはほぼすべて deltatime を使って、動きがずれないようにしています。

---Move point to destination
---@param dt number DeltaTime
---@return integer? x,integer? y,boolean? stopped
function Move:updateToDestination(dt)
    if self.vDestination == nil then
        return
    end

    local dist = self.vPosition:dist(self.vDestination)
    local step = self.speed * dt

Vsync

ほとんどのモニターには vsync のオプションもあり、少なくとも Love2d ではオンにしています。

t.window.vsync = 1 -- Vertical sync mode (number)

最後に

ここで考えたことが実際にうまくいくのか、まだ100%はわかりません。それは、いろいろなコンピューターやデバイスでテストしてみて初めてわかることです。すべてのシステムで完璧にすることは、たぶん無理でしょう。もちろん、遅すぎてゲームが最適に動かないコンピューターもあります。今は、そういう場合にメッセージを表示する方法を試しています。

    local checkFpsSeconds = .1
    local minimalFpsNeeded = 55
    local warningAfterSeconds = 4.0
    local startLoadingTime = love.timer.getTime()
    self.tick =
        Tick.recur(
        function()
            local fps = love.timer.getFPS()

            if tonumber(love.timer.getTime() - startLoadingTime) > warningAfterSeconds then
                loadingScreen.textObj:setText("You need " .. tostring(minimalFpsNeeded - fps) .. " FPS more.")
            end

            if fps > minimalFpsNeeded then
                self.tick:stop()

                -- everything is fast
            end
        end,
        checkFpsSeconds
    )