Monday, July 14, 2014

Animated Sprites in Löve2D

Hello Again,

Today, we are going to talk about how to render animated sprites with the Löve2D framework (v0.9.1). The principle is simple, you store the image and shared animation data and keep the animation control data somewhere. Whenever you need to render the sprite, you get the control data, apply the "fix" animation data and in the end render the correct portion of the image with the correct rotation, flips and scale.

The difficulty on this guide here is much higher than that of the introduction. I suggest you master that previous one before venturing any further.

This is what we want to achieve today (source code download at the bottom):



The first thing that comes to mind is that we have data that is shared between all our animated sprites, as the spritesheet, while there is data that is unique for every single animated sprite, as the elapsed time. So, let's determine the necessary data and differentiate what we can share from what we need to duplicate on each instance.

Shared or Instance Data

Straight out, the first things that come to mind are the spritesheet and a name. Every animation needs the access to an image/texture (or maybe the steps to render it with primitives) and a name to identify it, uniquely would be preferable. This data wouldn't change if you had, say, two separate goblins being animated. The sprite's size and speed are also fixed, the number of animations, total frames for each of them and these frames' offset in the texture are also static information, that would not change between the goblins walking side by side.

So, for the fix data, we have:
  1. Sprite Name (or an ID);
  2. Image (may be shared between sprites);
  3. Frame Duration;
  4. Animations (list); and for each animation
    • Size;
    • Frames Offsets (a list or a way to calculate these);
    • Frames Sizes.
Basically, this is all we need to have an animated sprite. If you want, you can store the size and frame duration for each animation or even for every single frame, if you want to; this would add some more flexibility. But here I won't do that since it detracts the code's readability.

Ok, with all these in place, we can calculate what frame to draw at a given time, using an elapsed time and the specific sprite and animation we want to render. So, that is the basics of what we'll need, a variable to control the animation time and another to store what animation is being rendered. We also need to know where on the window we'll render the sprite, and also give it some scale and rotation.

And this is what we need for every copy of the animation being rendered:
  1. Sprite Name (or ID, so we know what static data to use);
  2. Current Animation;
  3. Current Frame;
  4. Elapsed Time (from the last frame change);
  5. Position (x, y);
  6. Scale (in size and speed);
  7. Rotation (in degrees);
  8. Flip (horizontal, vertical, both or none);
  9. Alpha.

Alternatively, you could store just the elapsed time and refrain from keeping the current frame. This would save you a variable, but you'd need to use the % operator for every sprite on every frame. I prefer to use the extra variable and keep the math a little lighter.

That is basically what we'll need. The first list is what we need to have a single copy in memory despite of how many animations are running simultaneously. The second list's variables is what we need to keep a copy for every single instance. Sometimes, it doesn't make sense having rotation, scale, alpha or even flip when your game wouldn't use it.

Sometimes, some of these items can be left off the sprite instance, such as its position, scale, alpha, rotation, flip, as these would actually be stored in your game's entities, not in the rendering information.

As for the data, that is all. Let's move on to the serialization of the static part.


Simple Sprite Serialization with Lua

Lua makes our lives really easier when it comes to data serialization. Lots of content creation tools such as the Tiled map editor export directly to Lua, and all we need to do is call a single function and it is all in memory, ready to use.

SerializedData

return { -- Do you remember? These {} make a table.
    --Insert all the stuff here!
    first_name = "Not",
    last_name = "Sure"
}

GetTheData

myData = dofile("SerializedData.lua")

print(myData["first_name"], myData["last_name"])


Our first script with more than one file. The first file -GetTheData- gets the serialized data from the second one -SerializedData- by executing it with Lua's dofile() function. It then prints the contents of the received table.

The dofile() function simply calls the script file as if it was a single function and returns to us whatever the script file returns. Did you notice the file has a return statement outside of any functions? In Lua, you can treat a file as a single function and add return in its main body, outside of any blocks. That is really useful for serialized data, and we'll do that with almost every serialized table we have.

There's also some other similar functions to load external files: require(...), loadfile(...), load(...), and some others. You can find them here, under the sections 5.1 through 5.3.

This is how we are going to serialize the static data for all our sprites. We start the data file with return { and add all the data we need before closing with the respective }. The importance of the serialization can't be overstated.

Löve2D Note:
In L2D, you can zip your entire game in a single .zip file and rename it to .love. This way people can run it directly with less hassle (info here). The problem is that these loadfile() and dofile() functions do not work, you need to use the love.filesystem counterparts.

Real Example


To create our serialization fie, we'll need a sprite to serve as example. For that, I'll be using some graphics available here. I will need to make some changes so it fits our use better.

Lava Man from Capcom's Breath of Fire 3



Our first step will be serializing the information we need in order to animate the Lava Man. We will do that by creating a new Lua file named LavamanSprite.lua. This file will contain all the fix information for the sprite as described earlier. This is how we are starting our file (warning - this file will be relatively big).

LavamanSprite.lua:


local image_w = 739 --This info can be accessed with a Löve2D call
local image_h = 438 --      after the image has been loaded.
                    --I'm creating these for readability.

return {
    serialization_version = 1.0 -- The version of this serialization process

    sprite_sheet = "images/Sheet.png", -- The path to the spritesheet
    sprite_name = "lavaman", -- The name of the sprite

    frame_duration = 0.10, --How much time on each frame (default)


As you can see, we are using the return {}, that means return the following table; and I start listing miscellaneous information on the sprite. The image_w and image_h variables hold the dimensions of the entire spritesheet. We could get these by loading the image and checking its size or adding these directly as I did here.

The serialization_version variable is very important, as usually the serialization process may change while your game is in active development. Having this version control will allow you to create backward compatibility if no breaking changes are made. So, even if you think you won't use it, I advise you to use this control (or a better one for that matter).

The sprite_sheet and sprite_name won't be used on every frame, but are really important nevertheless. The name is mostly for internal data control, something mnemonic you can use as reference. The sheet should be the path to the image (absolute or relative) so we can get it ready.

The frame_duration here is basically the time our sprite will spend on each frame (in seconds). In here, we'll be using this value for the entire sprite, but you can use one for each animation or even one for each frame.

    --This will work as an array.
    --So, these names can be accessed with numeric indexes starting at 1.
    --If you use < #sprite.animations_names > it will return the total number
    --      of animations in in here.
    animations_names = {
        "idle",
        "attack",
        "get_hit",
        "idle_back",
        "attack_back",
        "get_hit_back"
    },

So, here we list all the animations our sprite will have. If you have seen the video on the top, you have seen that it has 3 different animations in two points of view (6 animations). So, I list their names here so we can access them by index: mySprite.animations_names[1] would return the string 'idle'.

    --The list with all the frames mapped to their respective animations
    --  each one can be accessed like this:
    --  mySprite.animations["idle"][1], or even

    --   ### NOT FINAL CODE ###

    animations = { 
        idle = {},  --These animation entries (arrays) are empty here,
                    --  but we'll fill them with the frames soon.
        idle_back = {},
        attack = {},
        attack_back = {},
        get_hit = {},
        get_hit_back = {}
        
    }
Note: the code above is not in its final form. It is here just to make it clearer for now.
This animations map holds arrays with the frames' offsets and respective sizes. Every animation has one of these arrays.

Now, how can we add the rectangles (as in offset+size) we need in order to render the desired portion of the image? One approach would be to simply add the coordinates to that array, as in here:

        -- ### NOT FINAL CODE ###
        idle = {
            --{X offset, Y offset, Width, Height}
            {100, 100, 50, 30},
            {200, 300, 50, 30}
        },

What is the problem with this approach? None really. It is just not taking advantage of one of Lua's strongest points, the fact that it isn't necessary to parse data serialized with it. If we add the numbers, we will still need to convert these into a Löve2D structure called a quad (docs here).

Creating this quad structure takes some runtime effort, so it is not good to create a quad for every time we'd render a sprite. We could get these numbers and parse them into quads when we first load the sprite, that is much better. But Lua gives us a third option - serializing the quads commands directly. So, we'll need to use the love.graphics.newQuad( x, y, width, height, source_w, source_h ) function (docs here).

So, this is the resulting idle table:
        idle = {
        --  love.graphics.newQuad( X, Y, Width, Height, Image_W, Image_H)
            love.graphics.newQuad( 1, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 83, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 165, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 247, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 329, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 411, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 493, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 575, 1, 81, 64, image_w, image_h )
        },

The first two numbers are the offset for the frame. The origin of the rectangle we want to draw from the image. The 3rd and 4th represent the size of that rectangle. The last two parameters are actually the size of the spritesheet. I have no idea why Löve2D needs this information, but it does. You can do as I did and insert this information manually or you can do it as follows:

local SourceImage = love.graphics.newImage("images/Sheet.png")

local image_w = SourceImage:getWidth() --or SourceImage.getWidth(SourceImage)
local image_h = SourceImage:getHeight()

So, this is how we would list the animations map of arrays in its final form (with one entry per frame on the arrays):

    animations = {
        idle = {
        --  love.graphics.newQuad( X, Y, Width, Height, Image_W, Image_H)
            love.graphics.newQuad( 1, 1, 81, 64, image_w, image_h ),
            --...
        },
        
        idle_back = {
            love.graphics.newQuad( 1, 73, 83, 58, image_w, image_h ),
            --...
        },
        
        attack = {
            love.graphics.newQuad( 1, 137, 122, 71, image_w, image_h ),
            --...
        },
        
        attack_back = {
            love.graphics.newQuad( 1, 217, 103, 83, image_w, image_h ),
            --...
        },
        
        get_hit = {
            love.graphics.newQuad( 1, 309, 69, 58, image_w, image_h ),
            --...
        },
        
        get_hit_back = {
            love.graphics.newQuad( 1, 373, 64, 64, image_w, image_h ),
            --...
        }
        
    } --animations

} --return (end of file)

What takes us to our final version of the LavamanSprite.lua:

LavamanSprite.lua:
--[[
    LavamanSprite.lua - 2014
    
    Copyright Dejaime Antônio de Oliveira Neto, 2014
    
    Released under the MIT license.
    Visit for more information:
    http://opensource.org/licenses/MIT
]]
print("LavamanSprite.lua loaded")

require "love.graphics"


local image_w = 739 --This info can be accessed with a Löve2D call
local image_h = 438 --      after the image has been loaded. I'm creating these for readability.


return {
    serialization_version = 1.0, -- The version of this serialization process

    sprite_sheet = "images/Sheet.png", -- The path to the spritesheet
    sprite_name = "lavaman", -- The name of the sprite

    frame_duration = 0.10,
    
    
    --This will work as an array.
    --So, these names can be accessed with numeric indexes starting at 1.
    --If you use < #sprite.animations_names > it will return the total number
    --      of animations in in here.
    animations_names = {
        "idle",
        "attack",
        "get_hit",
        "idle_back",
        "attack_back",
        "get_hit_back"
    },

    --The list with all the frames mapped to their respective animations
    --  each one can be accessed like this:
    --  mySprite.animations["idle"][1], or even
    animations = {
        idle = {
        --  love.graphics.newQuad( X, Y, Width, Height, Image_W, Image_H)
            love.graphics.newQuad( 1, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 83, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 165, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 247, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 329, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 411, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 493, 1, 81, 64, image_w, image_h ),
            love.graphics.newQuad( 575, 1, 81, 64, image_w, image_h )
        },
        
        idle_back = {
            love.graphics.newQuad( 1, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 85, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 169, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 253, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 337, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 421, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 505, 73, 83, 58, image_w, image_h ),
            love.graphics.newQuad( 589, 73, 83, 58, image_w, image_h )
        },
        
        attack = {
            love.graphics.newQuad( 1, 137, 122, 71, image_w, image_h ),
            love.graphics.newQuad( 124, 137, 122, 71, image_w, image_h ),
            love.graphics.newQuad( 247, 137, 122, 71, image_w, image_h ),
            love.graphics.newQuad( 370, 137, 122, 71, image_w, image_h ),
            love.graphics.newQuad( 493, 137, 122, 71, image_w, image_h ),
            love.graphics.newQuad( 616, 137, 122, 71, image_w, image_h )
        },
        
        attack_back = {
            love.graphics.newQuad( 1, 217, 103, 83, image_w, image_h ),
            love.graphics.newQuad( 105, 217, 103, 83, image_w, image_h ),
            love.graphics.newQuad( 209, 217, 103, 83, image_w, image_h ),
            love.graphics.newQuad( 313, 217, 103, 83, image_w, image_h ),
            love.graphics.newQuad( 417, 217, 103, 83, image_w, image_h ),
            love.graphics.newQuad( 521, 217, 103, 83, image_w, image_h )
        },
        
        get_hit = {
            love.graphics.newQuad( 1, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 71, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 141, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 211, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 281, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 351, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 421, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 491, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 561, 309, 69, 58, image_w, image_h ),
            love.graphics.newQuad( 631, 309, 69, 58, image_w, image_h )
        },
        
        get_hit_back = {
            love.graphics.newQuad( 1, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 66, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 131, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 196, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 261, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 326, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 391, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 456, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 521, 373, 64, 64, image_w, image_h ),
            love.graphics.newQuad( 586, 373, 64, 64, image_w, image_h )
        }
        
    } --animations

} --return (end of file)

Phew! Now our serialization file is ready to be used. And all this hard work paid off, to get all the information serialized in this file in a table is now as simple as this:
lavamanSprite = dofile("LavamanSprite.lua")
And the lavamanSprite variable now holds all the static information on the sprite.

If you think about it, this code is very long, but relatively simple. It is simple enough to be automatically generated with a simple content creation tool. But we'll be doing these manually for now.

Sprites Manager

Now that we have our sprite properly serialized in its own file, we need to manage it somehow. In order to be able to use all that static data, we need to use some dynamic data for every sprite, as well as we need to load the spritesheet into memory. It would also be wise to do some error checking and automate the instantiation of the dynamic data. So, how do we start?

AnimatedSprite.lua

local ManagerVersion = 1.0

sprite_bank = {} --Map with all the sprite definitions
image_bank = {} --Contains all images that were already loaded

So, first thing first, we define the manager version. As of now, it is the same version as our LavamanSprite.lua serialization, but it will come in handy if we need to make changes on either file and need to create some backwards compatibility.

The tables sprite_bank and image_bank both store data mapped to unique indentifier keys, the sprites' being their definition file path and the images' being their own path. (note: if you load them with relative and absolute paths, it'll store one duplicate)

With these tables in hand, we can check whether a given sprite or image has already been loaded and, if not, load it properly. To be able to draw our sprites, we'll need the following functions:
function LoadSprite (sprite_def)
function GetInstance (sprite_def)
function UpdateInstance (sprite_instance, dt)
function DrawInstance (sprite_instance, x, y)

LoadSprite - Loads a sprite from a definition file (such as LavamanSprite.lua)
GetInstance - Gets an instance (dynamic data) for a given sprite
UpdateInstance - Updates an instance with the elapsed time from the last call
DrawInstance - Draws the instance using its own dynamic data at [x, y]

If you call GetInstance in a sprite that is not on memory, it will be loaded automatically. We will always try to pre-load them though, so we avoid loading anything at runtime. LoadSprite and GetInstance can return nil if there is an error (file doesn't exist or something like that).


function LoadSprite (sprite_def)

    if sprite_def == nil then return nil end

    --Load the sprite definition file to ensure it exists
    local definition_file = loadfile(sprite_def)

    --If the file doesn't exist or has syntax errors, it'll be nil.
    if definition_file == nil then
        --Spit out a warning and return nil.
        print("Attempt to load an invalid file (inexistent or syntax errors?): "
                ..sprite_def)
        return nil
    end

We start by checking if the user has passed us a nil value, and aborting if that's the case. Following, we use loadfile(file) and then check whether the file was successfully loaded or not, as it returns nil on error (file not found, bad syntax, etcetera). If we get a nil value, then there is an error. We report the problem by printing to the console and return nil (so the game can continue even on failure).

If the loadfile function return non-nil, then we are good to go as the file exists and is valid. So we need to run the file and check its version.

    local old_sprite = sprite_bank [sprite_def]
    sprite_bank [sprite_def] = definition_file()
    
    --Check the version to verify if it is compatible with this one.
    if sprite_bank[sprite_def].serialization_version ~= ManagerVersion then
        print("Attempt to load file with incompatible versions: "..sprite_def)
        print("Expected version "..ManagerVersion..", got version "
                ..sprite_bank[sprite_def].serialization_version.." .")
        sprite_bank[sprite_def] = old_sprite -- Undo the changes due to error
        -- Return old value (nil if not previously loaded)
        return sprite_bank[sprite_def]
    end

We start by saving the previous content of the sprite. If it wasn't loaded previously, that will be nil. This is done in case the loading fails and we need to revert it at a future point.

Then the second line runs the previously loaded file by calling definition_file as if it was a function. It stores the returned value in our sprite_bank map under an index that is the file path passed to this function. If there was another sprite here, it is overwritten, this allows for hot-reloads while a game is still running, really handy. Just remember to reset the instance if you hot-reload something (or you may crash your game). We could also use dofile() as we did before, but dofile loads the file before running it and this file was already loaded when we checked its validity.

We then compare the versions of both (Manager and Sprite) and report any problems in the console, aborting if they are incompatible. If it is invalid, we need to undo the changes and report it in the console.

The last resource we need to make sure exists and is valid is the image.
    --Storing the path to the image in a variable (to add readability)
    local sprite_sheet = sprite_bank[sprite_def].sprite_sheet

    --Load the image.
    local old_image = image_bank [sprite_sheet]
    image_bank [sprite_sheet] = love.graphics.newImage(sprite_sheet)
        
    --Check if the loaded image is valid.
    if image_bank[sprite_sheet] == nil then
        -- Invalid image, reverting all changes
        image_bank [sprite_sheet] = old_image   -- Revert image
        sprite_bank[sprite_def] = old_sprite    -- Revert sprite
        
        print("Failed loading sprite "..sprite_def..", invalid image path ( "
                ..sprite_sheet.." ).")
    end
    
    return sprite_bank [sprite_def]
end --LoadSprite

As the last step we need to load the image. We first cache the sprite_sheet path (so I don't need to write sprite_bank[sprite_def].sprite_sheet every time) and backup the old image, there's no problem if it is nil. Now we try to load the image by using the Löve2D newImage function (docs here).

Once the image is loaded, we need to check if Löve2D returned nil. If it did, our image couldn't be loaded properly. So, we need to undo what changes have been made by reverting both the image and the sprite to their previous state. We then return the new value (or the old in case of error) to the user.

And that is it for the LoadSprite function. As you can see, you can call it several times for the same sprite and it will only load it once, overwriting if duplicate. That's what we wanted, as this is the static data, fix data, that we will share between all the instances of a given sprite. Being able to hot-reload is a plus.


Sprite Instances


Now I ask you, what does the LoadSprite function do? It grabs all of a sprite's static data and prepares the necessary resources (the image). We can finally start creating a lot of different instances, each with their own set of dynamic values.

But how do we prepare a new instance? If you remember from the beginning of this guide there is some data that we need for every instance. Creating a new instance is as easy as preparing a table with that data inside in their default values. We also want to make sure a sprite is loaded before creating the instance. This what the GetInstance function looks like:
function GetInstance (sprite_def)

    if sprite_def == nil then return nil end -- invalid use
    
    if sprite_bank[sprite_def] == nil then
        --Sprite not loaded attempting to load; abort on failure.
        if LoadSprite (sprite_def) == nil then return nil end
    end
    
    --All set, return the default table.
    return {
        sprite = sprite_bank[sprite_def], --Sprite reference
        --Sets the animation as the first one in the list.
        curr_anim = sprite_bank[sprite_def].animations_names[1],
        curr_frame = 1,
        elapsed_time = 0,
        size_scale = 1,
        time_scale = 1,
        rotation = 0,
        flip_h = 1,
        flip_v = 1
    }
end

Much simpler than the last one isn't it? First we do some simple error checking to check for misuse and if the sprite is ready and, if it is not, try and load it. Abort if necessary, returning nil.


Updating an Sprite Instance

Of course, we need our sprite instances to change their current frame. And this is what the UpdateInstance is here for. We simply call it, pass along the elapsed time from the last update and it will get everything updated.

function UpdateInstance(spr, dt)

    --Increment the internal counter.
    spr.elapsed_time = spr.elapsed_time + dt
    
    --We check we need to change the current frame.
    if spr.elapsed_time > spr.sprite.frame_duration * spr.time_scale then
    
        --Check if we are at the last frame.
        --  # returns the total entries of an array.
        if spr.curr_frame < #spr.sprite.animations[spr.curr_anim] then
            -- Not on last frame, increment.
            spr.curr_frame = spr.curr_frame + 1
        else
            -- Last frame, loop back to 1.
            spr.curr_frame = 1
        end
        
        -- Reset internal counter on frame change.
        spr.elapsed_time = 0
    end

end

The first thing we do when updating an instance is incrementing its internal counter. When this counter exceeds the frame duration (adjusted with the instance's time_scale) we skip to the next frame. If this frame is the last one, we revert back to frame one. Whenever we change the frame, we reset the counter to 0. Note that the time_scale multiplies the frame_duration. So, if it is higher than 1 it will slow the animation down, while values smaller than 1 will make it faster. Any negative values or 0 sets the speed to the maximum (1:1 with the game's fps).

That's it, update counter and check time then change frame and reset counter if needed.

Drawing an Instance


Ok, now we have all our static data in place, we have our image loaded, a central map where we can get our static data reference from, the same with our image. We can also get instances of our unique sprites easily as well as update them with a single function call. There's just on thing missing: drawing. We still can't draw our sprite!

With all that data so neatly organized, drawing the correct rectangle is really straight forward:
function DrawInstance (spr, x, y)

    love.graphics.draw (
        image_bank[spr.sprite.sprite_sheet], --The image
        --Current frame of the current animation
        spr.sprite.animations[spr.curr_anim][spr.curr_frame],
        x,
        y,
        spr.rotation,
        spr.size_scale,
        spr.size_scale,
        flip_h,
        flip_v
    )

end

A single call to Löve2D draw function and we are done! I mean it! That is all it takes! The walls of code on the previous sections have reduced the last two ones into really small chunks with no loops at all. That's good, since this is the part that'll be called repeatedly at runtime.

This is our final AnimatedSprite.lua file:

AnimatedSprite.lua:
--[[
    AnimatedSprite.lua - 2014
    
    Copyright Dejaime Antônio de Oliveira Neto, 2014
    
    Released under the MIT license.
    Visit for more information:
    http://opensource.org/licenses/MIT
]]
print("AnimatedSprite.lua loaded")

local ManagerVersion = 1.0

sprite_bank = {} --Map with all the sprite definitions
image_bank = {} --Contains all images that were already loaded

function LoadSprite (sprite_def)

    if sprite_def == nil then return nil end
    

    --Load the sprite definition file to ensure it exists
    local definition_file = loadfile(sprite_def)

    --If the file doesn't exist or has syntax errors, it'll be nil.
    if definition_file == nil then
        --Spit out a warning and return nil.
        print("Attempt to load an invalid file (inexistent or syntax errors?): "
                ..sprite_def)
        return nil
    end

    --[[Loading the sprite definition as an entry in our table.

        We can execute the file by calling it as a function
            with these () as we loaded with loadfile previously.
        If we used dofile with an invalid file path our program
            would crash. 
        At this point, executing the file will load all the necessary
            information in a single call. There's no need to parse
            this of serialization.
    ]]
    local old_sprite = sprite_bank [sprite_def]
    sprite_bank [sprite_def] = definition_file()
    
    --Check the version to verify if it is compatible with this one.
    if sprite_bank[sprite_def].serialization_version ~= ManagerVersion then
        print("Attempt to load file with incompatible versions: "..sprite_def)
        print("Expected version "..ManagerVersion..", got version "
                ..sprite_bank[sprite_def].serialization_version.." .")
        sprite_bank[sprite_def] = old_sprite -- Undo the changes due to error
        -- Return old value (nil if not previously loaded)
        return sprite_bank[sprite_def]
    end
    
    
    --All we need to do now is check if the image exist
    --  and load it.
    
    --Storing the path to the image in a variable (to add readability)
    local sprite_sheet = sprite_bank[sprite_def].sprite_sheet

    --Load the image.
    local old_image = image_bank [sprite_sheet]
    image_bank [sprite_sheet] = love.graphics.newImage(sprite_sheet)
        
    --Check if the loaded image is valid.
    if image_bank[sprite_sheet] == nil then
        -- Invalid image, reverting all changes
        image_bank [sprite_sheet] = old_image   -- Revert image
        sprite_bank[sprite_def] = old_sprite    -- Revert sprite
        
        print("Failed loading sprite "..sprite_def..", invalid image path ( "
                ..sprite_sheet.." ).")
    end
    
    return sprite_bank [sprite_def]
end

function GetInstance (sprite_def)

    if sprite_def == nil then return nil end -- invalid use
    
    if sprite_bank[sprite_def] == nil then
        --Sprite not loaded attempting to load; abort on failure.
        if LoadSprite (sprite_def) == nil then return nil end
    end
    
    --All set, return the default table.
    return {
        sprite = sprite_bank[sprite_def], --Sprite reference
        --Sets the animation as the first one in the list.
        curr_anim = sprite_bank[sprite_def].animations_names[1],
        curr_frame = 1,
        elapsed_time = 0,
        size_scale = 1,
        time_scale = 1,
        rotation = 0,
        flip_h = 1,
        flip_v = 1
    }
end

function UpdateInstance(spr, dt)
    --Increment the internal counter.
    spr.elapsed_time = spr.elapsed_time + dt

    --We check we need to change the current frame.
    if spr.elapsed_time > spr.sprite.frame_duration * spr.time_scale then
        --Check if we are at the last frame.
        --  # returns the total entries of an array.
        if spr.curr_frame < #spr.sprite.animations[spr.curr_anim] then
            -- Not on last frame, increment.
            spr.curr_frame = spr.curr_frame + 1
        else
            -- Last frame, loop back to 1.
            spr.curr_frame = 1
        end
        -- Reset internal counter on frame change.
        spr.elapsed_time = 0
    end
end

function DrawInstance (spr, x, y)

    love.graphics.draw (
        image_bank[spr.sprite.sprite_sheet], --The image
        --Current frame of the current animation
        spr.sprite.animations[spr.curr_anim][spr.curr_frame],
        x,
        y,
        spr.rotation,
        spr.size_scale,
        spr.size_scale,
        flip_h,
        flip_v
    )

end

Using the Sprite


Now that we have our sprite information serialized as well as means of loading all its resources, checking for errors and instantiating new independent sprites, all we need to do is use it!

The simplest way to do it would be creating it and updating/drawing it on every frame:
--main.lua
require "AnimatedSprite" --Including the file

function love.load()
    lavaman = GetInstance ("LavamanSprite.lua")
end

function love.update(dt)
    UpdateInstance(lavaman, dt)
end

function love.draw()
    DrawInstance (lavaman, 50, 50)
end

With only this handful of lines you get a sprite on its default animation (idle), scale and speed rendering at [50, 50] in the screen. This is what it looks like:


Nice huh? But this is what we can do with just one line of code on each Löve2D callbacks, as simple as it can be. Our simple sprite system can do more than that, can scale, flip, accelerate, rotate and all that stuff. It can also render more than one of the same sprite at a given time without them having to be synchronized. This is the main.lua I used to make the video at the top of the post:

example sprite usage:
--[[
    main.lua - 2014
    
    Copyright Dejaime Antônio de Oliveira Neto, 2014
    
    Released under the MIT license.
    Visit for more information:
    http://opensource.org/licenses/MIT
]]

local draw_x = 0
local draw_y = 0
local next_animation = 2
local enter = 'return' -- Löve2D calls the enter key return.

function love.load()
    
    require "AnimatedSprite"

    LoadSprite ("Smile.lua") --Will print an error.

    Lavaman = GetInstance ("LavamanSprite.lua")

end

function love.update (dt)
    UpdateInstance(Lavaman, dt)
end

function love.draw ()
    DrawInstance (Lavaman, draw_x, draw_y)
    
    love.graphics.print("Frame Rate: "..love.timer.getFPS(), 500, 450)
    love.graphics.print("PgUp & PgDown to change size: "..Lavaman.size_scale, 500, 470)
    love.graphics.print("Home & End to change speed: "..string.format("%.7f",Lavaman.time_scale), 500, 490)
    love.graphics.print("Insert & Delete to Rotate: "..string.format("%.3f",Lavaman.rotation), 500, 510)
    love.graphics.print("Enter to change animation: "..Lavaman.curr_anim, 500, 530)
    love.graphics.print("Backspace to reset the sprite", 500, 550)
    love.graphics.print("Esc to quit", 600, 570)
end

function love.mousepressed (x, y, button)
    draw_x = x
    draw_y = y
end

function love.keypressed (k)

    if k == 'pageup' then
        Lavaman.size_scale = Lavaman.size_scale * 1.25
    elseif k == 'pagedown' then
        Lavaman.size_scale = Lavaman.size_scale * 0.8

    elseif k == 'end' then
        Lavaman.time_scale = Lavaman.time_scale * 1.25
    elseif k == 'home' then
        Lavaman.time_scale = Lavaman.time_scale * 0.8

    elseif k == 'insert' then
        Lavaman.rotation = Lavaman.rotation + math.rad(15)
    elseif k == 'delete' then
        Lavaman.rotation = Lavaman.rotation - math.rad(15)

    elseif k == enter then
        Lavaman.curr_anim = Lavaman.sprite.animations_names[next_animation]
        Lavaman.curr_frame = 1
        next_animation = next_animation + 1
        if next_animation > #Lavaman.sprite.animations_names then
            next_animation = 1
        end


    elseif k == 'backspace' then
        Lavaman = GetInstance ("LavamanSprite.lua")

    elseif k == 'escape' then
        love.event.quit()
    end
end

function love.keyreleased (k)
end


You can download the entire source code (with the image) through this link here.
This is it!
Our next steps are to change this sprite system to use what's called a SpriteBatch, return an error sprite when something goes wrong (something like a red square) instead of returning nil. And then creating a tiled map rendering system.

Looking forward for more!
Over and Out.