Animated Sprite helpful class

I wrote sprite class extension that supports imagetables, animation, finite state machine and json config.
I’m open to feedback and suggestions. If during usage you feel that some function is lacking or you found bugs, then please write to me. :playdate_heart:

playdate-20211013-052753

playdate-20210917-005352

Discord: Whitebrim#4444
Telegram: https://tg.brim.ml

12 Likes

I’ve filled up wiki, you can find docs & examples here:

1 Like

Since SDK got release I think I should bump this post to let new developers know that there is an easier way to work with animated sprites~

2 Likes

Hey this is pretty great! My first thought as soon as I started poking around with my own game was "why can't I just give an image table / animation to the sprite class" :slight_smile: This is very helpful.

2 Likes

Hi there! This looks SUPER helpful, but I’m having trouble implementing it into my code (this speaks 1000% to my lack of programming skills). I added the animated sprites lua file to my project, and I’m trying to add the init lines from GitHub to my code, but I keep getting an assert error around line 30 of the animated lua file. Do you have suggestions? Could you potentially point me to an example of how to set up a full file?

Thank you!

@thefreakyZ
Can you please share your full init code and error screenshot?

If error is in 30 line, this error is because imagetable that you initialized sprite with is nil. It might be because you have incorrect path to the imagetable.

Thank you for the quick response!!

Yeah...I think it is likely a path issue. I have a folder in my project that contains one sprite sheet with two frames. All I want to do is play the two frames back to back on repeat.

import "AnimatedSprite.lua"

imagetable = playdate.graphics.imagetable.new("images/spr_car_new")
sprite = AnimatedSprite.new(imagetable, 1, 1)
sprite:addState("idle", 1, 2)


function playdate.graphics.sprite.update()
	sprite:playAnimation()
end

Oh! And here's the console output.

AnimatedSprite.lua:30: assertion failed!
stack traceback:
	[C]: in function 'assert'
	AnimatedSprite.lua:30: in field 'init'
	CoreLibs/object.lua:70: in function <CoreLibs/object.lua:64>
	(...tail calls...)
	main.lua:4: in main chunk
AnimatedSprite.lua:30: assertion failed!
stack traceback:
	[C]: in function 'assert'
	AnimatedSprite.lua:30: in field 'init'
	CoreLibs/object.lua:70: in function <CoreLibs/object.lua:64>
	(...tail calls...)
	main.lua:4: in main chunk
AnimatedSprite.lua:30: assertion failed!
stack traceback:
	[C]: in function 'assert'
	AnimatedSprite.lua:30: in field 'init'
	CoreLibs/object.lua:70: in function <CoreLibs/object.lua:64>
	(...tail calls...)
	main.lua:4: in main chunk

Can you please show me images folder?

Hi there! I have one png file (the sprite file) inside of the images folder, which is contained in my project. My other files are not contained in this folder.

Is it named spr_car_new-table-8-8.png? (If one tile is 8x8, if it's now, then just your numbers, not 8-8)

Fixed your problem in the code. You called .new with params states=1, animate=1.

import "AnimatedSprite.lua"

imagetable = playdate.graphics.imagetable.new("images/spr_car_new")
sprite = AnimatedSprite.new(imagetable) -- Creating AnimatedSprite instance
sprite:addState("idle", 1, 2) -- Adding custom animation state.
sprite:playAnimation() -- Play the animation


-- No need for overriding update function if you are not going to change anything
1 Like

Awesome!! Using the table sprite name and the code you included helped! Thank you so much!!

So... I think I get how to use your class, but I am not sure how to properly export the assets from aseprite in order to make this work.
Let's say I have a sprite with 2 frames (a button that is pushed and not pushed). I have an animation with time on it. How should I export this in order to maintain the time and states defined in aseprite?

Right now, I Export Sprite Sheet in a horizontal Strip and obtain a button-table-64-64.png and a button.json files.

Loading them with the following code creates the error below. What am I missing?

local buttonTable = gfx.imagetable.new("images/button")
assert(buttonTable)
local states = AnimatedSprite.loadStates("images/button.json")
local sprite = AnimatedSprite.new(buttonTable, states)
sprite:moveTo(200, 100)
sprite:playAnimation()
sprite:add()
update failed: libs/AnimatedSprite.lua:147: The animation state is unnamed!
stack traceback:
	[C]: in function 'assert'
	libs/AnimatedSprite.lua:147: in upvalue 'addState'
	libs/AnimatedSprite.lua:234: in local 'proceedState'
	libs/AnimatedSprite.lua:244: in method 'setStates'
	libs/AnimatedSprite.lua:69: in field 'init'
	CoreLibs/object.lua:70: in function <CoreLibs/object.lua:64>
	(...tail calls...)
	main.lua:15: in main chunk

Adding the files for reference.
images.zip (1.3 KB)

Thanks for the help!

This lib can't parse aseprite's animation config files, it has it's own. Docs are here: JSON configuration · Whitebrim/AnimatedSprite Wiki · GitHub

Main difference is that animation is based on playdate's update frames, not time in ms (like in aseprite)
For example if your game is targeting 30 frames per second, then each frame will take 33ms to draw. If you want to convert 100ms animation into frame animation, then you need to set tickStep to 3 (3*33.3ms=100ms).

Basically you need to write new config file based on your aseprite configuration. Please write what animation settings and states do you have and I'll create config.json for you as a little demo. The only problem that might occur is that AnimatedSprite lib currently don't support different animation speed for single state: the workaround is to use different states.

1 Like

Thanks, I will have a look and read this properly before getting back to you. What do you think is the best way to build these animations in your opinion (or what do you use)?

If you have only two images for pushed and not pushed states and this states are controlled by button, then you can create two states and call :changeState("pushed") on buttonA.Down event handler and :changeState("idle") on buttonA.Up event handler
Config for it will be:

[
    {
        "name":"idle",
        "firstFrameIndex": 1,
        "framesCount": 1
    },
    {
        "name":"pushed",
        "firstFrameIndex": "idle",
        "framesCount": 1
    }
]

If you're asking about complex animations, here's config for crab animations for my Cranner game:

ezgif.com-gif-maker

[
	{
		"name":"default",
		"tickStep": 3
	},
	{
		"name":"run",
		"firstFrameIndex":1,
		"framesCount":5,
		"yoyo": true
	},
	{
		"name":"jump",
		"firstFrameIndex": "run",
		"framesCount":6,
		"nextAnimation": "jump top"
	},
	{
		"name":"jump top",
		"firstFrameIndex":"jump",
		"framesCount":1
	},
	{
		"name":"fall",
		"firstFrameIndex":"jump top",
		"framesCount":5,
		"tickStep":2,
		"loop":false
	},
	{
		"name":"landing",
		"firstFrameIndex":"fall",
		"framesCount":2,
		"tickStep":4,
		"nextAnimation": "run"
	},
	{
		"name":"sit down",
		"firstFrameIndex":"landing",
		"framesCount":2,
		"tickStep": 2,
		"nextAnimation": "duck run"
	},
	{
		"name":"stand up",
		"firstFrameIndex":"landing",
		"framesCount":2,
		"reverse": true,
		"loop":false
	},
	{
		"name":"duck transition",
		"firstFrameIndex":"sit down",
		"framesCount":1,
		"nextAnimation": "duck run"
	},
	{
		"name":"duck run",
		"firstFrameIndex":"duck transition",
		"animationStartingFrame":3,
		"framesCount":3,
		"tickStep": 4,
		"yoyo":true
	},
	{
		"name":"fall idle",
		"firstFrameIndex":"jump top",
		"framesCount":2,
		"loop":false
	}
]

And here's crab controller lua class:

import 'libraries/AnimatedSprite/AnimatedSprite.lua'

class("Hero").extends(AnimatedSprite)

local xConst <const> = 121
local yConst <const> = 162
local zConst <const> = 10000

local mcPerFrame <const> = 25
local groundLevel <const> = 162
local gravity <const> = 0.3
local initialJumpVelocity <const> = -9
local dropVelocity <const> = -3
local minJumpHeight <const> = 110
local maxJumpHeight <const> = 50
local speedDropCoefficient <const> = 5

function Hero:init(imagetable, states, animate)
	Hero.super.init(self, imagetable, states, animate)

    self.states["stand up"].onAnimationEndEvent = function ()
		self:changeState(self.jumping and "fall idle" or "run")
	end
    self.alive = true
	self.velocity = 0
	self.reachedMinHeight = false
	self.speedDrop = false
	self.jumpVelocity = 0
	self.jumping = false
	self.ducking = false
	self.jumpEnded = false
	self.falling = false
	
	self:setCollideRect(0, 0, self:getSize())
	self:setCollidesWithGroups({1, 2})

	self:setCenter(0, 0)
	self:setZIndex(zConst)
	self:moveTo(xConst, yConst)

    table.insert(GameScene.sprites, self)
end

function Hero:update()
    if (not self.alive) then
        return
    end
    --#region jump
    if (self.jumping) then
        self:updateJump()
    end
    --#endregion
    --#region collisions
    local collisions = self:overlappingSprites()
    local amount = #collisions
    if (amount > 0) then
        for i = 1, amount do
            if (collisions[i]:getGroupMask() == 2) then
                GameScene.Slowdown()
                collisions[i]:setCollisionsEnabled(false)
                collisions[i].speed = GameScene.GetSpeed()
                goto continue
            end
            if (self:alphaCollision(collisions[i])) then
                GameScene.GameOver()
                --collisions[i]:moveTo(381, collisions[i].y)
            end
            ::continue::
        end
    end
    --#endregion
    self:updateAnimation()
end

function Hero:moveSideways(direction)
    newX = self.x + direction
    if (newX > 17 and newX < 335) then
        self:moveTo(newX, self.y)
    end
end

function Hero:startJump()
    if (not self.jumping) then
        GameScene.SpawnSandParticlesJump(self.x)
        self:changeState("jump")
        self.jumping = true
        self.jumpEnded = false
        self.velocity = initialJumpVelocity -- - (speed / 10)
        self.reachedMinHeight = false
        self.speedDrop = false
        self.falling = false
    end
end

function Hero:endJump()
    self.jumpEnded = true
    if (self.reachedMinHeight and (self.velocity < dropVelocity)) then
        self.velocity = dropVelocity
    end
end

function Hero:updateJump()
    if (not self.falling and self.velocity >= 0) then
        self.falling = true
        if (not self.speedDrop) then
            self:changeState("fall")
        end
    end
    local framesElapsed = deltaTime / mcPerFrame
    local newY = self.y
    if (self.speedDrop) then
        newY += self.velocity *	speedDropCoefficient * framesElapsed
    else
        newY += self.velocity * framesElapsed
    end		

    self.velocity += gravity * framesElapsed

    if ((newY < minJumpHeight) or self.speedDrop) then
        self.reachedMinHeight = true
    end

    if ((newY < maxJumpHeight) or self.speedDrop or self.jumpEnded) then
        self:endJump()
    end

    if (newY > groundLevel) then
        self:resetJump()
        return
    end

    self:moveTo(self.x, newY)
end

function Hero:resetJump()
    self:moveTo(self.x, groundLevel)
    GameScene.SpawnSandParticlesFall(self.x)
    self:changeState(self.speedDrop and "duck transition" or "landing")
    self.jumpVelocity = 0
    self.jumping = false
    self.jumpEnded = false
    self.speedDrop = false
end

function Hero:sitDown()
    self.ducking = true
    if (self.jumping) then
        self.speedDrop = true
        if (self.velocity < 1) then
            self.velocity = 1
        end
    end
    self:changeState("sit down")
end

function Hero:standUp()
    self.ducking = false
    if (self.jumping) then
        self.speedDrop = false
    end
    self:changeState("stand up")
end

Thanks, this is super useful! Oh and by the way, I really like what you did with the art / animation here!
Does it mean nonetheless you built your json files by hand?