Animated Sprite helpful class

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?

You're right, I might copy-paste similar configs for new animations and just tweak some parameters, then compile to see how it's doing...

1 Like

:exclamation: There was "pdxinfo is missing" problem with playdate sideload feature caused by this lib.
I've fixed that, I highly recommend updating from git repo or not adding test/ folder into your game.

First off, thanks for making such an incredible resource. I'm not sure if I'm thinking about it wrong, but I'm having a hard time figuring out how to use it to draw multiple instances of the same animation.

For example, in my game there are items scattered randomly around the level. Previously, I implemented this using the built-in Playdate animation loops, something like (using pseudocode)

if tile is onscreen then
   if tile.item ~= nil then
      draw tile.item at tile.position

I was planning on doing something similar with AnimatedSprite, however I don't see a way to not draw an AnimatedSprite once it's been created. My concern is I don't want to draw the animations when they're not on screen (since you mentioned there are performance concerns and I may have hundreds of items on the map). I suppose I could play and pause the animations when they enter/exit the screen, but would there still be a performance hit even if the animation is paused? Or is there a way of doing this that I'm missing?

1 Like

This is new use case for me, I didn't think about it before. For my endless runner game I just create new instance of the AnimatedSprite and delete it when it's out of the screen...

You can override default :update() function to empty function (don't call :updateAnimation()), you need to call :updateAnimation() manually then. + Maybe removing AnimatedSprite(32 line): self:add() will help with performance, but you need to call:updateAnimation() every frame (idk if it'll work)~

Mind if I ask how you delete it? I think that could work for my case as well.

Edit: Actually I think I see, you just add and delete from the list of sprites. But is there a good way to add the same Animated Sprite multiple times at different positions?

By delete I mean open AnimatedSprite.lua file and delete code) You can create copy of the original class and have AnimatedSprite.lua and AnimatedStaticSprite.lua (without automatic :add()). For normal animations use AnimatedSprite that will automatically animate and for your tile animations you can use AnimatedStaticSprite and invoke :updateAnimation() manually.

But is there a good way to add the same Animated Sprite multiple times at different positions?

I don't know what you're talking concterly, but you can use for cycle to create multiple sprites...

Thanks @Whitebrim for this library, I have an issue while using it

main.lua

function playdate.update()

    gfx.sprite.update()
    playdate.timer.updateTimers()

end

but this update is not calling the update function in the player.lua , as I can't able to move my sprite

class("Player").extends(AnimatedSprite)

function Player:init(x, y)
    imgtable = pd.graphics.imagetable.new("Images/flight-table-24-24")
    playerSprite = AnimatedSprite.new(imgtable)   -- creating animated sprite
    playerSprite:addState('idle', 1, 2, {tickStep = 2})
    playerSprite:playAnimation()
    playerSprite:setCollideRect(0, 0, playerSprite:getImage(1):getSize())

    playerSprite:moveTo(x, y)
    playerSprite:add()
end

function Player:update()
    print("update called ...")
    if pd.buttonIsPressed( pd.kButtonUp ) then 
        playerSprite:moveBy( 0, -2 )
    end
    if pd.buttonIsPressed( pd.kButtonRight ) then
        playerSprite:moveBy( 2, 0 )
    end
    if pd.buttonIsPressed( pd.kButtonDown ) then
        playerSprite:moveBy( 0, 2 )
    end
    if pd.buttonIsPressed( pd.kButtonLeft ) then
        playerSprite:moveBy( -2, 0 )
    end
end

As the class AnimatedSprite is extending sprites, I thought this will work, but its not getting called, any ideas ?

(edited : 26Jul)

Hi, @jae, here's:

Wiki

Update override case

You don't need to call playerSprite:add()

Timers aren't used in AnimatedSprite, if you don't use them too, you can remove playdate.timer.updateTimers() too.

I assume Player is not animated sprite class, Player:update() is not handled by this lib, if it was playerSprite:update() instead, then it will be handled by gfx.sprite.update() because playerSprite is sprite. I'm not sure if Player is sprite, therefore nobody invokes Player:update(). Your problem was that "update called ..." never appears in the logs, right?

Thanks @Whitebrim for your quick answer , I missed the vital part of the code where the Player class is extending AnimatedSprite class.

Thanks to your answer , I figured out I need to use (updated code)

function AnimatedSprite:update()
    print("update is called")
    if pd.buttonIsPressed( pd.kButtonUp )  then
        playerSprite:moveBy( 0, -2 )
    end
    .
    .
    .
end

And now the update function is called.

Also, are there any examples available to use collision along with the AnimatedSprite component ?

Here's my code from cranner that uses collisions (groups):
link