Tutorial:Animation

Before we begin. This is a tutorial for semi advanced users. You are expected to know about tables, loops and the basics of drawing in Löve2D. And of course how to run a Löve2D game.


Explenation

Alright. Let's get to it shall we?

I'm going to cover the basics of sprite based animation. This means you have a series of images which are displayed one after another. If you intent to create your own spritesheet make sure to leave at least 1px of pure transparency between the individual sprites. Otherwise you might see artifacts from the next or previous image.

For this tutorial we'll be using Tim. The protagonist from the game Braid because the artist generously made the sprite sheet available! Specifically the walk cycle which you can see below. Download the image and place it next to your main.lua in the folder.

braid.png

So. Now we have 27 individual images. First we wanna load the image.

function love.load()
    animation = {}
    animation.spriteSheet = love.graphics.newImage("braid.png");
end

Encapsulated within the variable "animation" because we'll add a few more values to that and don't want them to be accessible globally as they are only relevant to this animation.

You can try to just draw this directly ("love.graphics.draw(animation.spriteSheet)"). However. Then we'd just have all the images drawn next to each other. Certainly not what we want. Which is where Quad come in very handy!

They define a part of the image which will be drawn instead of the whole image. Exactly what we need!

function love.load()
    animation = {}
    animation.spriteSheet = love.graphics.newImage("braid.png");
    animation.quads = {
        love.graphics.newQuad( 0,   0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 450, 130, 150, animation.spriteSheet:getDimensions())
    }
end

Now we have 27 Quads ranging from index 1 - 27 in the table "quads". Awesome!

But we have the issue that we have essentially 27 images that we can draw individually. But we need to draw them one after another over time. Also we don't just want to play this animation. We might want to change the speed at which it plays.

To cover this we start by defining a variable which contains the duration. We also need to measure the time this animation has been running so far. Which requires another variable.

function love.load()
    animation = {}
    animation.spriteSheet = love.graphics.newImage("braid.png");
    animation.quads = {
            -- To keep this shorter I don't repeat the quad loading. All of them should still be in here!
    }

    animation.duration = 2
    animation.currentTime = 0
end

And our load part is done!

Next up we need to update our current time.

function love.update(dt)
    animation.currentTime = animation.currentTime + dt
    if animation.currentTime >= animation.duration then
        animation.currentTime = animation.currentTime - animation.duration
    end
end

First we simply add dt (delta time aka the time since the last frame) to our current time. We now count upwards continuously!

But we will use the current time to determine which frame should be shown. As such we will want it to be between 0 and the value of "duration". The if simply checks if "currentTime" is more than duration in which case we subtract the duration. "dt" will never be an exact number. So currentTime will also always be a number with a lot of decimal points. To not get inaccurate we subtract the duration from the current time rather than setting it to 0.

Now for the really interesting part!

How do we draw this?

Well. We have the duration and current time. With this info we can calculate a percentage! How much of the animation has passed so far?

If you've followed this tutorial correctly so far "currentTime / duration" will provide you with a number between 0 and 1. Which represents the percentage. 0.25 means 25% of the animation has passed.

With that. We can search for the correct image to use! Since we already have a number betwee 0 and 1 we can simply multiply this percentage with our total amount of images and get a number between 0 and 27!

currentTime / duration * #quads

However. If we try to get this from our table we will run into the issue that this is not a whole number. But our images are stored with whole numbers! So attempting to get the image at index "6.75" will give us nothing. Bummer!

Fear not. The solution is not too difficult.

"currentTime" will be a number between 0 and just below "duration" (because we reduce "currentTime" if it is larger or equal "duration")

To transform this value from our decimal point value to a whole number we do the following:

math.floor(currentTime / duration * #quads) + 1

"math.floor" provides us with the next lower number. Which means in our case a number between 0 and 26. We add one pushing it to a number between 1 and 27. All the sprites we have!

Lövely!

Alright. So all that's left is to draw the appropriate quad!

This simply requires us to provide "love.graphics.draw" with the image reference (our spriteSheet) and the quad we want to use. Simple enough!

    local spriteNum = math.floor(animation.currentTime / animation.duration * #animation.quads) + 1
    love.graphics.draw(animation.spriteSheet, animation.quads[spriteNum])

And we are done! You should have a walking Tim in the top left corner of your window when you execute this code!


Training

This is not a setup that should be used for the development of games. You want a function to handle all of what we just did and which simply returns the animation table. Including loading the quads automatically rather than individually defining them.

This can be done by creating a function which takes the parameters:

  • SpriteSheet (an image object created with "love.graphics.newImage(filepath)"
  • Duration (a number)
  • Total Quads (a number)
  • SpriteWidth (a number)
  • SpriteHeight (a number)

You can then get the size of the image with "SpriteSheet:getDimensions()". Until you reached the total amount of quads, you look if the image is large enough to fit another sprite ("current location on the spriteSheet" + "spriteWidth"), if so, create a quad with this location and test the next position. Otherwise check the next row. (the first and second parameter of "love.graphics.newQuad" are the location on the image. First x (sideways) and then y (upwards))

With only "animation" tables. You can then start a table containing only animation tables (table.insert(animations, newAnimation)) and iterate over all your animation objects and all of them, instead of just one like we did here.

All of this works just fine with our current sprite sheet.

This part will be up to you though. Good luck and have fun!

Sourcecode

Full sourcecode of this tutorial (excluding the task at the end):

function love.load()
    animation = {}
    animation.spriteSheet = love.graphics.newImage("braid.png");
    animation.quads = {
        love.graphics.newQuad( 0,   0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 0, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 150, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 780, 300, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 0,   450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 130, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 260, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 390, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 520, 450, 130, 150, animation.spriteSheet:getDimensions()),
        love.graphics.newQuad( 650, 450, 130, 150, animation.spriteSheet:getDimensions())
    }

    animation.duration = 2
    animation.currentTime = 0
end

function love.update(dt)
    animation.currentTime = animation.currentTime + dt
    if animation.currentTime >= animation.duration then
        animation.currentTime = animation.currentTime - animation.duration
    end
end

function love.draw()
    local spriteNum = math.ceil(animation.currentTime / animation.duration * #animation.quads)
    love.graphics.draw(animation.spriteSheet, animation.quads[spriteNum])
end