Animation and Time
A simple translation animation
Translation is an overly fancy word for moving an object along one or more axis. Here’s an example where we draw a single point traveling across the screen:
love.load()
-- note that the lack of a "local" keyword at the beginning makes
-- this a global variable by default in lua, which is why we can
-- reference it from the other callbacks.
point = {
x = 10,
y = 10
}
end
love.update(dt)
point.x = point.x + 1
end
love.draw()
love.graphics.points(point.x, point.y)
end
If you were to copy this code into main.lua
and run LÖVE right now, it would work; it would draw a single tiny point across the screen, one point to the right every time it draws a frame. But you’ll notice that I’ve not actually used the dt
value from love.update()
. So Jim’s potato computer from the 90s moves really slowly, while Leslie’s 480Hz gaming PC moves so fast she just thinks she’s looking at a blank screen. To make sure that it runs the same on everyone’s machines, we need to do math with dt
.
Time-based smooth animations
In order to understand how to use time, you have to understand dt
or whatever your framework’s concept of delta time is. Almost universally, the symbol for delta time will represent a fraction of a second.
If you’re good at algebra, you know all the properties of numbers that will make everything make sense immediately. But I’m an idiot, so I will explain it in idiot terms.
Since dt
is a fraction of a second, if you sum up every update callback’s dt, when that sum reaches 1, one second will have passed. So let’s imagine that we ahave a screen that refreshes 10 frames per second. So our delta time will be one tenth of a second, or in absolute terms, 0.1.
The easiest way to think of translation animations is going to be in terms of pixels per second. Let’s say we want that pixel to move 100 pixels per second. You would use this line instead:
point.x = point.x + 100 * dt
For another way to visualize what’s happening in our hypothetical you can imagine you’re doing 100 * 1 / 10
- which will give us 10 pixels. Since we are moving 10 frames per second, we can multiply that by 10 to get where we will be after one second and we will get 100 - the number we multiplied by dt
.
Of course, our target devices will not be running 10 frames per second. They will be running any number of different frequencies, and our game code will take time to run, too, so dt
could be any number. And that means that the value of point.x
could be fractional even. But don’t worry, pretty much every framework will handle this.
This math doesn’t just work for translations. You can use them for any number smooth frame-by-frame animations, such as scale, rotation, shearing, etc. Of course, we can also apply limits very easily
while point.x < 500 do
point.x = point.x + 100 * dt
end
Though this solution has an obvious flaw: we can overshoot where we want this to go! A potential better solution is as thus:
if point.x >= 500 then
point.x = 500
else
point.x = point.x + 100 * dt
end
That way when it reaches or exceeds our destination, it will just make sure that it’s at that specific point.
Frame-triggered animations
So now that you understand how you use time math to make animations stay the same speed, but what do you do if you have time-based animation? Say, for instance, a sprite’s idle animation. You’re not going to want fractional pixel differences, you’re going to want to have animation frames switch out at specific times. If I give you a minute you might figure out how to do it yourself.
Go ahead. I’ll wait. Come back and we can compare answers.
….
You done? Good.
You realized you have to keep track of the time, right?
Here’s a basic frame animation example
love.load()
frames = {
-- frames go here
}
currentTime = 0
finishTime = 2 -- 2 seconds, that is
frameIndex = 1 -- Lua tables are 1-indexed, so we start here
end
love.update(dt)
currentTime = currentTime + dt
if currentTime >= finishTime then
-- reset currentTime
currentTime = 0
--increment frameIndex
frameIndex = frameIndex + 1
-- if we passed the number of frames, we loop to the beginning
-- "#" is the operator that returns the length of a table in lua
if frameIndex > #frames then
frameIndex = 1
end
end
end
love.draw()
love.graphics.draw(frames[frameIndex], 20, 20)
end
I think the code speaks for itself (or it better with all those damn comments). But you might notice something really unideal if you know about how Lua scopes variables by default. All of the variables we defined in love.load()
are global! Between the one object we are animating, the background we’ll need, and the UI, the code will really be spaghetti before we even have a game ready! With that in mind, you should already figured out that code organization will be extremely important when making a game engine. It’s such a critical issue that I’d honestly say it’s the single most important part of this book.
Next: Objects as Objects