Unexpected results when setting animation.loop.delay in update

I have a player sprite walk cycle using an imagetable and an animation.loop. I am adjusting \animation.loop.delay on update() based on the player's speed. However I am getting unexpected results. The apparent animation speed when delay is set to certain values on update does not match the animation speed if I set the animation delay to the same values statically.

I've put together a minimum example which shows this behavior. The three characters on the bottom are animated using a fixed delay.

The character at the top has its animation.delay set every frame using a sin wave. The line and text help to visualize the delay changing.

Note that what the top animation looks like at various values, e.g. min (200ms) and max (600ms) values, does not match the static reference animations on the bottom.

Example:
https://gloryfish.s3.amazonaws.com/games/adventure/animation-delay-issue.zip

Code for main.lua (images are in the ZIP above).

import 'CoreLibs/animation'
import 'CoreLibs/graphics'
import 'CoreLibs/math'

local gfx <const> = playdate.graphics

local imagetable
local animDynamic
local animStatic200
local animStatic400
local animStatic600

function loadGame() 
  imagetable, error = gfx.imagetable.new('images/player-walk-right')
  assert(imagetable, error)

  animDynamic = gfx.animation.loop.new(200, imagetable)
  animStatic200 = gfx.animation.loop.new(200, imagetable)
  animStatic400 = gfx.animation.loop.new(400, imagetable)
  animStatic600 = gfx.animation.loop.new(600, imagetable)
end

function playdate.update()
  gfx.clear()
  
  -- Setting `delay` every frame produces unexpected results
  animDynamic.delay = playdate.math.lerp(200, 600, (math.sin(playdate.getCurrentTimeMilliseconds() * 0.0005) + 1) / 2)
  
  gfx.drawRect(30, 80, animDynamic.delay / 2, 2)
  gfx.drawText(math.floor(animDynamic.delay)..'ms', 30, 90)

  animDynamic:draw(30, 30)
  animStatic200:draw(30, 120)
  animStatic400:draw(100, 120)
  animStatic600:draw(170, 120)

  gfx.drawText('200ms', 30, 160)
  gfx.drawText('400ms', 100, 160)
  gfx.drawText('600ms', 170, 160)
end

loadGame()

Looking at the source for updateLoopAnimation() in animation.lua it definitely seems like it's not really meant to be updated continually. The looper assumes that delay is constant across the current run of the animation in order to calculate the current frame from the total elapsed time. If you change delay while an animation is going it changes the calculation for how many frames have been displayed since startFrame an d gives inconsistent results.

I've built implementations which use delay to set a frame interval which gets consumed on each update by deltaTime. Once the entire interval is consumed the frame advances and the available interval is set to whatever delay is currently set to (less any extra consumed time from the previous frame). This means that delay can be adjusted continually but only impacts the timing of the next frame in sequence.

1 Like

I wrote a small class that implements the behavior I'm looking for:

import 'CoreLibs/object'

class('AnimationLooper').extends(Object)

function AnimationLooper:init(imageTable) 
  self.imageTable = imageTable
  self.frame = 1
  self.paused = false
  self.lastUpdateTime = playdate.getCurrentTimeMilliseconds()
  self.currentFrameDuration = 0  
  self.interval = 100
end

function AnimationLooper:image()
  return self.imageTable[self.frame]
end

function AnimationLooper:setInterval(interval)
  self.interval = interval
end

function AnimationLooper:incrementFrame()
  self.frame += 1
  if self.frame > #self.imageTable then
    self.frame = 1
  end
end

function AnimationLooper:update()
	if self.paused == true then
		return
	end

  local currentTime = playdate.getCurrentTimeMilliseconds()
  local deltaTime = currentTime - self.lastUpdateTime

  self.currentFrameDuration += deltaTime

  if self.currentFrameDuration > self.interval then
    self:incrementFrame()
    self.currentFrameDuration = self.currentFrameDuration - self.interval
  end 

  self.lastUpdateTime = currentTime
end

function AnimationLooper:draw(x, y, flipped)
	local img = self:image()
  img:draw(x, y, flipped)
end

walk-loop

For the Playdate SDK, I think the best thing would be to add a small note to the docs for Animation loop which clarifies that you shouldn't set delay every frame.

Maybe delay should be read-only after it is initially set?

Read-only really would have been the easier way to go, but it was bugging me that this isn't working the way you expected it to, so I added some code to recalculate the time offset when a new delay is set, and I think it's working well! Try dropping in this replacement for CoreLibs/animation.lua if you want to test it out before the next SDK release:
animation.lua.zip (2.1 KB)

2 Likes

This is excellent. Thanks so much for taking the time. The new version does exactly what I expect and resolves the unexpected behavior I was seeing in my original example without any other modifications. Very cool.

Oh good, thanks for testing it out!

1 Like