Frame per detik

Berapa yang cukup?

Lebih cepat tidak selalu lebih baik

Berapa yang cukup?

Jenis game yang akan saya buat tidak butuh banyak FPS. Tapi kembali ke awal dulu: apa itu? FPS adalah jumlah frame (gambar di layar) yang ditampilkan berturut-turut dalam 1 detik.

Kalau kamu bilang 30 FPS, berarti dalam 1 detik ditampilkan 30 gambar berturut-turut. Kalau FPS-nya terlalu rendah, gambarnya akan tersendat dan tidak mulus (walau tidak selalu begitu).

Saat ini target saya minimal 60 FPS, dan seperti yang kamu lihat, memang sudah tercapai 😄

FPS dalam game

FPS dalam game

Tetap terkendali

Saya sudah membaca beberapa artikel tentang cara yang paling baik dan paling sederhana. Ternyata tergantung jenis game dan platformnya. Karena saya memilih keandalan dan ini game 2D yang sederhana, saya memutuskan mengunci framerate di 60.

Untuk itu saya memakai https://github.com/bjornbytes/tick dengan sedikit penyesuaian dari saya, sehingga saat ini terlihat seperti ini.

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

Alasan saya melakukan ini adalah supaya saya bisa berasumsi game selalu berjalan di 60 FPS, dan tidak terjadi hal aneh kalau seseorang punya kartu grafis yang mencapai 10000 FPS. Dengan teknik ini semuanya ‘selalu’ stabil.

Deltatime

Hampir di semua yang bergerak saya memakai deltatime agar gerakan tidak keluar dari sinkron.

---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

Kebanyakan monitor juga punya opsi vsync, yang pokoknya sudah saya aktifkan di Love2d.

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

Penutup

Saya belum 100% tahu apakah yang saya rancang di sini sesuai dengan praktiknya. Itu baru akan ketahuan setelah diuji di berbagai komputer dan perangkat. Menurut saya tidak akan pernah bisa benar-benar pas untuk semua sistem. Tentu saja juga ada komputer yang terlalu lambat sehingga game tidak berjalan optimal. Saat ini saya sedang bereksperimen untuk menampilkan pemberitahuan dalam kasus seperti itu.

    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
    )