Fotogramas por segundo

¿Cuánto es suficiente?

Más rápido no siempre es mejor

¿Cuánto es suficiente?

El tipo de juego que voy a hacer no necesita muchos FPS. Pero volvamos al principio: ¿qué es? Es el número de fotogramas (imágenes en pantalla) que se muestran seguidos en 1 segundo.

Si dices 30 FPS, muestras 30 imágenes seguidas en 1 segundo. Si tienes demasiado pocos FPS, ves imágenes a tirones, que no son fluidas (no siempre es así).

Ahora mismo mi objetivo es usar como mínimo 60 FPS y, como ves, así es 😄

FPS en el juego

FPS en el juego

Mantener el control

Después de leer varios artículos sobre qué es lo mejor y lo más sencillo, resulta que depende del tipo de juego y de la plataforma. Como apuesto por la fiabilidad y como es un juego 2D sencillo, decidí fijar el framerate en 60.

Para eso uso https://github.com/bjornbytes/tick con una modificación mía encima, de modo que ahora se ve así.

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

Lo hago para poder dar por hecho que siempre funciona a 60 FPS y que no pasan cosas raras si alguien tiene una tarjeta gráfica que llega a 10000 FPS. Con esta técnica se mantiene ‘siempre’ estable.

Deltatime

En casi todo lo que se mueve uso deltatime para evitar que los movimientos se desincronicen.

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

La mayoría de los monitores también tienen la opción vsync, que en cualquier caso he activado en Love2d.

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

Para terminar

Todavía no sé al 100% si lo que he pensado aquí encaja con la práctica. Lo sabré cuando haya hecho pruebas en distintos ordenadores y dispositivos. Creo que nunca lo vas a tener perfecto para todos los sistemas. También hay, claro, ordenadores demasiado lentos en los que el juego no funciona de forma óptima. Ahora mismo estoy experimentando para mostrar un aviso en esos casos.

    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
    )