love2d / love

LÖVE is an awesome 2D game framework for Lua.
https://love2d.org
Other
5.22k stars 405 forks source link

Option for fixed time step updates (like XNA does) #717

Closed slime73 closed 11 years ago

slime73 commented 11 years ago

Original report by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


I tweaked love.run() in boot.lua to offer fixed update time steps. I didn't know where to put the configuration variables so I just hardcoded them in. This code will throttle to a specified frame rate, or allow multiple updates per frame to keep a target update rate if draw calls are slow. This works like XNA does. I even named the variables the same.

Here is Microsoft's reference: http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.game.isfixedtimestep.aspx

#!lua
function love.run()
    math.randomseed(os.time())
    math.random() math.random()

    if love.load then love.load(arg) end

    local isFixedTimeStep = true --put in config somewhere
    local targetElapsedTime = 1/30 --put in config somewhere
    local nextUpdateTime = 0
    if love.timer and love.update then
        nextUpdateTime=love.timer.getTime()
    else
        isFixedTimeStep=false
    end

    -- Main loop time.
    while true do
        -- Process events.
        if love.event then
            love.event.pump()
            for e,a,b,c,d in love.event.poll() do
                if e == "quit" then
                    if not love.quit or not love.quit() then
                        if love.audio then
                            love.audio.stop()
                        end
                        return
                    end
                end
                love.handlers[e](a,b,c,d)
            end
        end

        if isFixedTimeStep then
            --hopefully someone didn't delete love.timer
            if love.timer.getTime() > nextUpdateTime then
                -- Call update and draw
                if love.update then love.update(targetElapsedTime) end
                nextUpdateTime = nextUpdateTime+targetElapsedTime
                if love.timer.getTime()<nextUpdateTime then
                    love.timer.isRunningSlowly = false
                    if love.graphics then
                        love.timer.step() --only call for actual frames so get FPS is accurate
                        love.graphics.clear()
                        if love.draw then love.draw() end
                        love.graphics.present()
                    end
                else
                    love.timer.isRunningSlowly = true
                end
            end
        else
            -- Update dt, as we'll be passing it to update
            if love.timer then
                love.timer.step()
                dt = love.timer.getDelta()
            end
            -- Call update and draw
            if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

            if love.graphics then
                love.graphics.clear()
                if love.draw then love.draw() end
                love.graphics.present()
            end
        end
        if love.timer then love.timer.sleep(0.001) end

    end
end
slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


As you've already noticed, you already have the option because you can replace the run loop. Are you suggesting the default run loop should be changed to do this? If so, is there really much point considering you can change it anyway?

The fixed update step loop you've posted doesn't seem to deal with drawing interpolation, as discussed here: http://gafferongames.com/game-physics/fix-your-timestep/

slime73 commented 11 years ago

Original comment by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


It's my understanding that microsoft included it in XNA because fixed time steps are easier for beginners to understand. On a fixed time step you don't need to multiply everything by dt. Timers can just be increment by 1 every update call. Positions can be updated as x=x+1. I much prefer fixed time steps when prototyping because of the simplicity.

If I were forced to pick just one method, I would have it default to fixed timesteps because it's the better default for beginners. Beginners don't know it's possible to override the run loop (or what changes to make if they do know it's possible).

If you don't want to make it the default it would still make a good example on the wiki. Let me know if you'd prefer this and I'll rewrite it with that target in mind.

slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


Even with a fixed timestep it's good to put your units in terms of seconds rather than frames. It requires a bit more understanding at the start, but it becomes much more intuitive down the road ("the bullet moves at 8.3 pixels per frame" versus "the bullet moves at 500 pixels per second").

The fixed update method can either be simple to use but frame / update rate will not actually be consistent (as in your code), or it can be complex to implement due to rendering interpolation but it will look and behave very well (as in the final method discussed in the article I linked.)

In my opinion, neither of those two ways is particularly satisfactory as a default, and while the variable update model is a tiny bit tougher than a naive fixed update model to figure out at first (and the simulation will vary a tiny bit at very different framerates), it's smooth and not extremely complex, so it seems like a good default to me.

slime73 commented 11 years ago

Original comment by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


Your call. My intent was to mimic XNA for my own prototyping needs. I just thought I'd share. Though, it shouldn't be stuttering... does love.graphics.present() block until the buffer swap?

An an aside: that guys solution isn't the end all. By blending between two frames it either simulates physics one frame in the future without the knowing the future controller input, or the current frame lags behind as it gets interpolated in (I didn't take the time to figure out which). The better solution is to "guess" the future with dead reckoning. That way you don't have to write custom state interpolation code; you can just use the same dead reckoning code you wrote for network synchronization.

Either way, none of these techniques would generalize well for love.

slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


Drawing interpolation and extrapolation both have their own downsides, neither is better than the other in all ways. Which one is better for an individual game probably depends on a lot of factors specific to the game.

Calling a swap buffers function (love.graphics.present in LÖVE) won't always block. What happens is very dependent on the GPU, the driver, the driver settings, whether vsync is enabled in LÖVE, and how many frames are queued up.

With your code and with vsync enabled (on a 60Hz monitor) and targetElapsedTime set to 60, it'll probably bounce in between over- and under-shooting the target update rate, especially with that love.timer.sleep in there.

It makes it really easy for the 'perfect case' of updating and drawing at the target rate to not actually happen, since there are so many ways to make things take longer or shorter than you want, and some of them are completely out of the hands of the person writing the game.

slime73 commented 11 years ago

Original comment by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


I've been fiddling with this a bit, and I noticed that when vsync is on and the game is running at a solid 60hz the dt passed to update isn't consistent. Do you think it might be worthwhile to fudge the dt to be a constant 1/60 (or whatever the framerate is) as long as the game is vsync limited? If an object is moving at 1 pixel per frame (x=x+dt*60) there is an occasional stutter due to rounding error.

Also, have you considered exposing XXX_EXT_swap_control_tear.

slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


Do you think it might be worthwhile to fudge the dt to be a constant 1/60 (or whatever the framerate is) as long as the game is vsync limited?

No. vsync will not always be 60 (one of my monitors has a 60Hz refresh, another has 75Hz - I have both connected at once), and framerate can and will drop below the monitor's refresh rate, especially on lower end systems and with unoptimized code.

If an object is moving at 1 pixel per frame (x=x+dt*60) there is an occasional stutter due to rounding error.

Have you tried comparing fullscreen versus windowed? Window managers can often cause those sorts of stutters, it seems more likely than rounding error.

Also, have you considered exposing XXX_EXT_swap_control_tear.

That's used whenever available when vsync is enabled, as of earlier today. :)

slime73 commented 11 years ago

Original comment by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


Watch how the 10ths digit slowly creeps up and oscillates between two values. When it oscillates between .9 and 0 you get the stutter. (I'm still running love 0.8 if that makes a difference)

#!lua
local x=0
function love.update(dt)
    x=x+dt*60
end
function love.draw()
    love.graphics.print(x, 10, 320)
end

fullscreen =true vsync = true

slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


I'm not quite sure I understand - maybe you could make a .love that demonstrates the stuttering in fullscreen (an actual object moving, rather than some numbers)?

slime73 commented 11 years ago

Original comment by John Kaniarz (Bitbucket: jkaniarz, GitHub: jkaniarz).


Fixed the issue with 3 things:

It looks like 0.9 moved the precision of getMicroTime() into getTime() which should fix everything.

slime73 commented 11 years ago

Original comment by Alex Szpakowski (Bitbucket: slime73, GitHub: slime73).


Back to the original topic, I think the fact that love.run is easily replaceable is sufficient.

The wiki is editable for anyone who has a forum account, if you think some examples of custom run loops would be useful.