1. When Vsync is enabled the game will be kept at a frame rate consistent with your monitor's refresh rate. In most cases this means that it will automatically be capped at 60fps. To test that this is the case, we can create a variable named frame
which will count the frame we're in. We start it at 0 and increase it by 1 every frame:
xfunction love.load()
frame = 0
end
function love.update(dt)
frame = frame + 1
print(frame)
end
And if you run this you should see the number printed go up by about 60 every second.
However, if you turn Vsync off by calling love.window.setMode(800, 600, {vsync = false})
in love.load
and then run it again, you'll see that it goes up much faster. This is because by default the love.run
function contains almost no code to keep the while true
loop from going super sonic fast, which means that it will do just that.
The only thing that does keep it in check somewhat is the love.timer.sleep(0.001)
call, which is explained in the love.run
page. As explained there, if Vsync is disabled, this call is used to keep the FPS capped at 1000, to decrease CPU usage and give the OS control for a bit every frame.
For the next exercises we'll assume that Vsync is enabled. The Fix Your Timestep article makes the same assumption, but also does a good job of outlining the problems with each method and also considering what happens with each when Vsync might be turned off.
2. We want to implement the Fixed Delta Time
loop. As the article states, this one uses a fixed delta of 1/60s. In the default love.run
function the delta changes based on the value returned by love.timer.getDelta
, and that changes based on the time taken between the two last frames. So the default implementation is definitely not a fixed delta of 1/60s. To achieve that, we need to first remove the sections that change the delta in any way. So this piece of code will be removed:
xxxxxxxxxx
if love.timer then
love.timer.step()
dt = love.timer.getDelta()
end
And then we need to define our dt
variable to the value we want it to have, which is 1/60:
xxxxxxxxxx
local dt = 1/60
And so the love.run
function would look like this:
xxxxxxxxxx
function love.run()
if love.math then love.math.setRandomSeed(os.time()) end
if love.load then love.load(arg) end
local dt = 1/60
-- Main loop time.
while true do
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end
The problems with this approach are explained in the article, but that's how you'd do it if you wanted to for some reason.
3. Now we want to implement the Variable Delta Time
loop. The way this is described is as follows:
Just measure how long the previous frame takes, then feed that value back in as the delta time for the next frame.
If we go back to the description of the love.timer.step
and love.timer.getDelta
functions that are used in the default love.run
implementation, it turns out that that's exactly what they do. So there seems to be no differences between the default love.run
implementation and the Variable Delta Time
loop. This is what it looks like:
xxxxxxxxxx
function love.run()
if love.math then love.math.setRandomSeed(os.time()) end
if love.load then love.load(arg) end
if love.timer then love.timer.step() end
local dt = 0
-- Main loop time.
while true do
-- Process events.
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
-- 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 and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end
The problems with this approach are also well explained in the article.
4. Now for the Semi-fixed Timestep
loop. We should now add an upper-bound for the dt
value as well as making sure that if that value goes above that limit, we divide the timestep into multiple parts. We can achieve that by simply changing what happens around the love.update
call:
xxxxxxxxxx
while dt > 0 do
local current_dt = math.min(dt, upper_dt)
if love.update then love.update(current_dt) end
dt = dt - current_dt
end
The dt
variable corresponds to frameTime
and current_dt
corresponds to deltaTime
. We also use upper_dt
, which can be defined where dt
is initialized like local upper_dt = 1/60
. And all that looks like this:
xxxxxxxxxx
function love.run()
if love.math then love.math.setRandomSeed(os.time()) end
if love.load then love.load(arg) end
if love.timer then love.timer.step() end
local dt = 0
local upper_dt = 1/60
while true do
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == 'quit' then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
if love.timer then
love.timer.step()
dt = love.timer.getDelta()
end
while dt > 0 do
local current_dt = math.min(dt, upper_dt)
if love.update then love.update(current_dt) end
dt = dt - current_dt
end
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end
5. Finally, the Free the Physics
loop, which is similar to the previous solution, except that we constrain ourselves to only go with fixed steps. The benefits that come from this is that our simulations are going to be more well behaved and predictable even under various different circumstances.
The only thing we really need to do is setup everything that happens around the love.update
call as the pseudocode from the article shows:
xxxxxxxxxx
accumulator = accumulator + dt
while accumulator >= fixed_dt do
if love.update then love.update(fixed_dt) end
accumulator = accumulator - fixed_dt
end
And so this follows the same logic as the previous exercise, where if the frame time exceeds a certain value, then we subdivide the timestep into multiple parts (but this time they're parts of fixed size). The accumulator
and fixed_dt
variables can be set up outside the while loop and it all looks like this:
xxxxxxxxxx
function love.run()
if love.math then love.math.setRandomSeed(os.time()) end
if love.load then love.load(arg) end
if love.timer then love.timer.step() end
local dt = 0
local fixed_dt = 1/60
local accumulator = 0
while true do
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == 'quit' then
if not love.quit or not love.quit() then
return a
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
if love.timer then
love.timer.step()
dt = love.timer.getDelta()
end
accumulator = accumulator + dt
while accumulator >= fixed_dt do
if love.update then love.update(fixed_dt) end
accumulator = accumulator - fixed_dt
end
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
love.graphics.present()
end
if love.timer then love.timer.sleep(0.001) end
end
end