Page 1 of 1

Advancing animation frames manually?

Posted: Wed May 27, 2020 5:10 pm
by spaaaceghooost
Hi there! First post, so sorry in advance if I missed a guideline.

Some brief context on the project first: I'm a stop motion animator making a puppet inspired by a gameboy with an on-board Raspberry Pi so I can change it's facial expressions.

Here's an animation test showing my progress so far: https://twitter.com/CapsuleGhost/status ... 19488?s=20

I'm making some custom software to manage the puppets facial features and arrived at Love2d for it's RPi compatibility. My final goal is to have a program where i can control the macro location of the face on the screen, then fine tune the x,y of the individual facial features. Most importantly I need to be able cycle each mouth, eyebrow, and eye through it's respective spritesheet manually with a button input.

So here's my main question: Is there a way to assign each facial feature (with it's own spritesheet) to a different key input, and then have it advance one quad every time that key is pressed?

Here's what I have so far (I'm so sorry for the code gore.. this is literally my first experience coding and, as I'm sure you can tell, this is frankensteined together from a few tutorials and what i could figure out on my own.)

Code: Select all

--HEX FACE--
love.graphics.setDefaultFilter("nearest", "nearest")
le = {}
le_controller = {}
le_controller.le = {}
le_controller.image = love.graphics.newImage("oldHero.png")

function love. load()
	animation = newAnimation(love.graphics.newImage("oldHero.png"), 16, 18, 1)
  player = {}
	player.x = 400
	player.y = 400
	player.speed = 10
	le_controller:spawnLE(player.x - 100, player.y - 50)
end

function le_controller:spawnLE(x, y)
	le = {}
	le.x = x
	le.y = y
	table.insert(self.le, le)
end


----------------------------------CONTROLS----------------------------------
function love.update(dt)
  
  animation.currentTime = animation.currentTime + dt
    if animation.currentTime >= animation.duration then
        animation.currentTime = animation.currentTime - animation.duration
    end
  
  if love.keyboard.isDown("right") then
		player.x = player.x + player.speed
    le.x = le.x + player.speed
	elseif love.keyboard.isDown("left") then
		player.x = player.x - player.speed
    le.x = le.x - player.speed
	end

  
  if love.keyboard.isDown("up") then
		player.y = player.y - player.speed
    le.y = le.y - player.speed
	elseif love.keyboard.isDown("down")then
		player.y = player.y + player.speed
    le.y = le.y + player.speed
	end
end

-----------------------------GRAPHICS------------------------------------


function love.draw()
  love.graphics.setColor(255, 255, 255)
	love.graphics.rectangle("fill", player.x, player.y, 80, 20)
    local spriteNum = math.floor(animation.currentTime / animation.duration * #animation.quads) + 1
    love.graphics.draw(animation.spriteSheet, animation.quads[spriteNum], player.x - 100, player.y -100, 0, 4)
end
 
function newAnimation(image, width, height, duration)
    local animation = {}
    animation.spriteSheet = image;
    animation.quads = {};
 
    for y = 0, image:getHeight() - height, height do
        for x = 0, image:getWidth() - width, width do
            table.insert(animation.quads, love.graphics.newQuad(x, y, width, height, image:getDimensions()))
        end
    end
 
    animation.duration = duration or 1
    animation.currentTime = 0
 
    return animation
end
right now "le" (Left eye) is the only sprite, and the placeholder graphic is the oldHero.png from a love2d tutorial I found on quads. I'll be replacing him with a proper left eye sprite sheet once I figure out the mechanics of the program.

Thanks for your time and sorry again if I misread a posting guideline.

Re: Advancing animation frames manually?

Posted: Thu May 28, 2020 3:42 am
by hoistbypetard
Thanks for posting this. I was debugging something kind of adjacent to this, and reading/thinking about your question helped clarify things for me.

Even though I'm not 100% certain I understand your question.

Here's the little program I made to help me understand things:
key_quads.love
demo project
(190.55 KiB) Downloaded 145 times
Note that the graphics there are from Antifarea at opengameart and that the test program uses the knife and push libraries because those are part of what I was trying to understand (I'm also pretty new), I find them useful, and they are very, very small. The usefulness of the push library might be less obvious than the usefulness of the knife library... it's especially helpful here because it scales the sprites to make the behavior easy to see as you test.

You ask:
So here's my main question: Is there a way to assign each facial feature (with it's own spritesheet) to a different key input, and then have it advance one quad every time that key is pressed?
and the short answer is definitely "yes" if you haven't already determined that. The framework gives you access to all the key events, and the things that get drawn are fully under your control. You can change them at your whim, in response to key events, in response to the passage of time, not at all, whatever.

But I think the little test program I posted might also be helpful...

I started with a bunch of sprite sheets for different characters. Those are not mine. They come from Antifarea at opengameart. Delta a glitch or two, each image contains a series of 32x36 frames of character animations.

I start by loading them all:

Code: Select all

local _textures = {
    ['healer-f'] = love.graphics.newImage('images/rpgsprites1/healer_f.png'),
    ['healer-m'] = love.graphics.newImage('images/rpgsprites1/healer_m.png'),
    ['mage-f'] = love.graphics.newImage('images/rpgsprites1/mage_f.png'),
    ['mage-m'] = love.graphics.newImage('images/rpgsprites1/mage_m.png'),
    ['ninja-f'] = love.graphics.newImage('images/rpgsprites1/ninja_f.png'),
    ['ninja-m'] = love.graphics.newImage('images/rpgsprites1/ninja_m.png'),
    ['ranger-f'] = love.graphics.newImage('images/rpgsprites1/ranger_f.png'),
    ['ranger-m'] = love.graphics.newImage('images/rpgsprites1/ranger_m.png'),
    ['townfolk1-f'] = love.graphics.newImage('images/rpgsprites1/townfolk1_f.png'),
    ['townfolk1-m'] = love.graphics.newImage('images/rpgsprites1/townfolk1_m.png'),
    ['warrior-f'] = love.graphics.newImage('images/rpgsprites1/warrior_f.png'),
    ['warrior-m'] = love.graphics.newImage('images/rpgsprites1/warrior_m.png'),
}
Then I break each image up into tiles:

Code: Select all

local _frames = {}

for k,v in pairs(_textures) do
    _frames[k] = _GenerateFrames(_textures[k], SPRITE_WIDTH, SPRITE_HEIGHT)
end
(That _GenerateFrames() function is really naiive)

Code: Select all

local function _GenerateFrames(atlas, frameWidth, frameHeight)
    local tiles_across = atlas:getWidth() / frameWidth
    local tiles_down = atlas:getHeight() / frameHeight
    local sprite_sheet = {}
    local frame_num = 0
    for tileY = 0, tiles_down - 1 do
        for tileX = 0, tiles_across - 1 do
            frame_num = frame_num + 1
            sprite_sheet[frame_num] = love.graphics.newQuad(tileX * frameWidth, tileY* frameHeight, frameWidth, frameHeight, atlas:getDimensions())
        end
    end
    return sprite_sheet
end
My love.load function is where the most interesting bits happen:

Code: Select all

displayItems = {}
keysMapped = 'abcdefghijkl'

function love.load()
    local game_width, game_height = VIRTUAL_WIDTH, VIRTUAL_HEIGHT
    local window_width, window_height = love.window.getDesktopDimensions()
    window_width, window_height = window_width*.5, window_height*.5
    local animation_gap = SPRITE_WIDTH

    push:setupScreen(game_width, game_height, window_width, window_height, {fullscreen = false, resizeable = false})

    local char_x, char_y = animation_gap, animation_gap
    local curr_item = 0
    for k, character in pairs(_textures) do
        curr_item = curr_item + 1
        local entry =  {
            texture = _textures[k],
            frames = _frames[k],
            x = char_x,
            y = char_y,
            width = SPRITE_WIDTH,
            height = SPRITE_HEIGHT,
            currFrame = 1,
        }
        table.insert(displayItems, entry)
        entry.incrementFrame = function(self)
            local newFrame = self.currFrame + 1
            if newFrame > #self.frames then newFrame = 1 end
            self.currFrame = newFrame
        end
        Event.on('keypressed_'..string.sub(keysMapped, curr_item, curr_item),
            function()
                entry:incrementFrame() 
            end
        )
        char_x = char_x + SPRITE_WIDTH + animation_gap
        if char_x > game_width - animation_gap - SPRITE_WIDTH then
            char_x = animation_gap
            char_y = char_y + SPRITE_HEIGHT + animation_gap
        end
    end
end
Each item gets an incrementFrame() function attached to it. That can be called manually in response to keyboard events or in response to a timer firing. The knife event and timer libraries are used for this, but those are just convenient thin wrappers around standard love facilities. Feel free to look at their source and distill them down to just the minimum you need. I looked, and they're so thin I'm just using them.

In my draw function, I just draw whatever frame either the timer or the keypress has set as "current":

Code: Select all

function love.draw()
    push:start()
    for _,item in ipairs(displayItems) do
        love.graphics.setColor(1, 1, 1, 1)
        love.graphics.draw(item.texture, item.frames[item.currFrame], item.x, item.y)
    end
    push:finish()
end
And in my keypressed handler, I either quit, dispatch a specific event to advance a frame, start my timers or stop my timers depending on which keys were pressed:

Code: Select all

function love.keypressed(key)
    if key == 'escape' then
        love.event.quit()
    elseif string.find(keysMapped, key) then
        Event.dispatch('keypressed_' .. key)
    elseif key == 'lshift' then
        Timer.every(0.5, function()
            for _,item in ipairs(displayItems) do
                item:incrementFrame()
            end
        end)
    elseif key == 'rshift' then
        Timer.clear()
    end
end
So here, a through k increments that sprite's frame manually. left shift starts the automated animation timer. right shift stops it. I'm sure there's error handling I should be doing here that I'm not :). But this works for me for enough testing to help me understand the processing.

I hope it's helpful for you to see it broken down this way. It helped me. I'm also relatively new to this framework. Ask questions if you have them... I'm happy to try to answer, and trying to do so helps me too.

Re: Advancing animation frames manually?

Posted: Thu May 28, 2020 6:29 am
by spaaaceghooost
Thank you so much for your answer! This definitely points me in the right direction (though I don’t understand all of it yet). I’m glad this was applicable to your project. I’ll let you know if I have any questions :awesome: