fpv/source/libraries/noble/modules/Noble.Animation.lua
2024-06-01 16:52:11 +03:00

297 lines
12 KiB
Lua

--- Animation states using a spritesheet/imagetable. Ideal for use with `NobleSprite` objects. Suitable for other uses as well.
-- @module Noble.Animation
--
Noble.Animation = {}
--- Setup
-- @section setup
--- Create a new animation "state machine". This function is called automatically when creating a new `NobleSprite`.
-- @string __view This can be: the path to a spritesheet image file or an image table object (`Graphics.imagetable`). See Playdate SDK docs for imagetable file naming conventions.
-- @return `animation`, a new animation object.
-- @usage
-- local myHero = MyHero("path/to/spritesheet")
-- @usage
-- -- When extending NobleSprite (recommended), you don't call Noble.Animation.new(),
-- -- but you do feed its __view argument into MySprite.super.init()...
-- MyHero = {}
-- class("MyHero").extends(NobleSprite)
--
-- function MyHero:init()
-- MyHero.super.init(self, "assets/images/Hero")
-- -- ...
-- -- A new NobleSprite creates a Noble.Animation object named "self.animation"
-- self.animation:addState("idle", 1, 30)
-- self.animation:addState("jump", 31, 34, "float")
-- self.animation:addState("float", 35, 45)
-- self.animation:addState("turn", 46, 55, "idle")
-- self.animation:addState("walk", 56, 65)
-- -- ...
-- end
-- @usage
-- local myAnimation = Noble.Animation.new("path/to/spritesheet")
-- @usage
-- -- When extending playdate.graphics.Sprite, Noble.Animation.new() must be called manually...
-- MyHero = {}
-- class("MyHero").extends(Graphics.sprite)
--
-- function MyHero:init()
-- MyHero.super.init(self)
-- -- ...
-- self.animation = Noble.Animation.new("assets/images/Hero")
-- self.animation:addState("idle", 1, 30)
-- self.animation:addState("jump", 31, 34, "float")
-- self.animation:addState("float", 35, 45)
-- self.animation:addState("turn", 46, 55, "idle")
-- self.animation:addState("walk", 56, 65)
-- -- ...
-- end
-- @see NobleSprite:init
function Noble.Animation.new(__view)
local animation = {}
--- Properties
-- @section properties
--- The currently set animation state.
--
-- This is intended as `read-only`. You should not modify this property directly.
-- @see setState
animation.current = nil
--- The name of the current animation state. Calling this instead of `animation.current.name` is <em>just</em> a little faster.
--
-- This is intended as `read-only`. You should not modify this property directly.
animation.currentName = nil
--- The current frame of the animation. This is the index of the imagetable, not the frame of the current state.
--
-- Most of the time, you should not modify this directly, although you can if you're feeling saucy and are prepared for unpredictable results.
-- @see draw
animation.currentFrame = 1
--- This controls the flipping of the image when drawing. DIRECTION_RIGHT is unflipped, DIRECTION_LEFT is flipped on the X axis.
-- @usage
-- function MyHero:goLeft()
-- self.animation.direction = Noble.Animation.DIRECTION_LEFT
-- -- ...
-- end
animation.direction = Noble.Animation.DIRECTION_RIGHT
--- This animation's spritesheet. You can replace this with another `playdate.graphics.imagetable` object, but generally you would not want to.
-- @see new
if (type(__view) == "userdata") then
animation.imageTable = __view
else
animation.imageTable = Graphics.imagetable.new(__view)
end
-- The current count of frame durations. This is used to determine when to advance to the next frame.
animation.frameDurationCount = 1
-- The previous number of frame durations in the animation
animation.previousFrameDurationCount = 1
local empty = true
--- Setup
-- @section setup
--- Add an animation state. The first state added will be the default set for this animation.
--
-- <strong>NOTE:</strong> Added states are first-degree member objects of your Noble.Animation object, so do not use names of already existing methods/properties ("current", "draw", etc.).
-- @string __name The name of the animation, this is also used as the key for the animation.
-- @int __startFrame This is the first frame of this animation in the imagetable/spritesheet
-- @int __endFrame This is the final frame of this animation in the imagetable/spritesheet
-- @string[optional] __next By default, animation states will loop, but if you want to sequence an animation, enter the name of the next state here.
-- @bool[opt=true] __loop If you want a state to "freeze" on its final frame, instead of looping, enter `false` here.
-- @param[optional] __onComplete This function will run when this animation is complete. Be careful when using this on a looping animation!
-- @int[opt=1] __frameDuration This is the number of ticks between each frame in this animation. If not specified, it will be set to 1.
-- @usage
-- -- You can reference an animation's state's properties using bog-standard lua syntax:
--
-- animation.idle.startFrame -- 30
-- animation.walk.endFrame -- 65
-- animation.["walk"].endFrame -- 65
-- animation.jump.name -- "jump"
-- animation.["jump"].next -- "float"
-- animation.idle.next -- nil
function animation:addState(__name, __startFrame, __endFrame, __next, __loop, __onComplete, __frameDuration)
local loop = true
local frameDuration = 1
if (__loop ~= nil) then loop = __loop end
if(__frameDuration ~= nil) then frameDuration = __frameDuration end
self[__name] = {
name = __name,
startFrame = __startFrame,
endFrame = __endFrame,
next = __next,
loop = loop,
onComplete = __onComplete,
frameDuration = frameDuration,
}
-- Set this animation state as default if it is the first one added.
if (empty == true) then
empty = false
self.currentFrame = __startFrame
self.current = self[__name]
self.currentName = __name
self.frameDuration = frameDuration
end
end
--- Methods
-- @section methods
--- Sets the current animation state. This can be run in a object's `update` method because it only changes the animation state if the new state is different from the current one.
-- @tparam string|Noble.Animation __animationState The name of the animation to set. You can pass the name of the state, or the object itself.
-- @bool[opt=false] __continuous Set to true if your new state's frames line up with the previous one's, i.e.: two walk cycles but one is wearing a cute hat!
-- @tparam string|Noble.Animation __unlessThisState If this state is the current state, do not set the new one.
-- @usage animation:setState("walk")
-- @usage animation:setState(animation.walk)
-- @usage
-- animation:setState(animation.walkNoHat)
-- --
-- animation:setState(animation.walkYesHat, true)
-- @usage
-- function MyHero:update()
-- -- Input
-- -- ...
--
-- -- Physics/collisions
-- -- ...
--
-- -- Animation states
-- if (grounded) then
-- if (turning) then
-- self.animation:setState(self.animation.turn)
-- elseif (math.abs(self.velocity.x) > 15) then
-- self.animation:setState(self.animation.walk, false, self.animation.turn)
-- else
-- self.animation:setState(self.animation.idle, false, self.animation.turn)
-- end
-- else
-- self.animation:setState(self.animation.jump, false, self.animation.float)
-- end
--
-- groundedLastFrame = grounded
-- end
function animation:setState(__animationState, __continuous, __unlessThisState)
if (__unlessThisState ~= nil) then
if (type(__unlessThisState) == "string") then
if (self.currentName == __unlessThisState) then return end
elseif (type(__unlessThisState) == "table") then
if (self.current == __unlessThisState) then return end
end
end
local newState = nil
if (type(__animationState) == "string") then
if (self.currentName == __animationState) then return end
newState = self[__animationState]
self.currentName = __animationState
elseif (type(__animationState) == "table") then
if (self.current == __animationState) then return end
newState = __animationState
self.currentName = __animationState.name
end
local continuous = Utilities.handleOptionalBoolean(__continuous, false)
if (continuous) then
local localFrame = self.currentFrame - self.current.startFrame
self.currentFrame = newState.startFrame + localFrame
else
self.currentFrame = newState.startFrame
end
self.current = newState
end
--- Draw the current frame.
--
-- When attached to a NobleSprite, this is called by `NobleSprite:draw()` when added to a scene. For non-NobleSprite sprites, put this method inside your sprite's `draw()` method, or inside @{NobleScene:update|NobleScene:update}.
-- @number[opt=0] __x
-- @number[opt=0] __y
-- @bool[opt=true] __advance Advances to the next frame after drawing this one. Noble.Animation is frame-based, not "delta time"-based, so its speed is dependent on your game's framerate.
-- @usage
-- function MySprite:draw()
-- animation:draw()
-- end
-- @usage
-- function MyScene:update()
-- animation:draw(100,100)
-- end
function animation:draw(__x, __y, __advance)
--print(self.currentName .. " > " .. self.currentFrame .. " >> " .. tostring(self.current.loop))
if (__advance == nil) then __advance = true end
if(self.currentFrame < self.current.startFrame or self.currentFrame > self.current.endFrame + 1) then
self.currentFrame = self.current.startFrame -- Error correction.
elseif(self.currentFrame == self.current.endFrame + 1) then -- End frame behavior.
if (self.current.next ~= nil) then
self.currentFrame = self.current.next.startFrame -- Set to first frame of next animation.
self.frameDurationCount = 1 -- Reset ticks.
self.previousFrameDurationCount = self.frameDuration
self:setState(self.current.next) -- Set next animation state.
elseif (self.current.loop == true) then
self.currentFrame = self.current.startFrame -- Loop animation state. (TO-DO: account for continuous somehow?)
self.frameDurationCount = 1 -- Reset ticks.
self.previousFrameDurationCount = self.frameDuration
elseif(__advance) then
self.currentFrame = self.currentFrame - 1 -- Undo advance frame because we want to draw the same frame again.
end
if (self.current.onComplete ~= nil) then
self.current.onComplete()
end
end
local x = __x or 0
local y = __y or 0
self.imageTable:drawImage(self.currentFrame, x, y, self.direction)
if (__advance == true) then
self.frameDurationCount += 1
if((self.frameDurationCount - self.previousFrameDurationCount) >= self.current.frameDuration) then
self.currentFrame = self.currentFrame + 1
self.previousFrameDurationCount += self.current.frameDuration
end
end
--previousAnimationName = self.currentName
end
--- Sometimes, you just want to draw a specific frame.
-- Use this for objects or sprites that you want to control outside of update loops, such as score counters, flipbook-style objects that respond to player input, etc.
-- @int __frameNumber The frame to draw from the current state. This is not an imagetable index. Entering `1` will draw the selected state's `startFrame`.
-- @string[opt=self.currentName] __stateName The specific state to pull the __frameNumber from.
-- @number[opt=0] __x
-- @number[opt=0] __y
-- @param[opt=self.direction] __direction Override the current direction.
function animation:drawFrame(__frameNumber, __stateName, __x, __y, __direction)
local x = __x or 0
local y = __y or 0
local stateName = __stateName or self.currentName
local direction = __direction or self.direction
local frameNumber = self[stateName].startFrame - 1 + __frameNumber
self.imageTable:drawImage(frameNumber, x, y, direction)
end
return animation
end
--- Constants
-- @section constants
--- A re-contextualized instance of `playdate.graphics.kImageUnflipped`
Noble.Animation.DIRECTION_RIGHT = Graphics.kImageUnflipped
--- A re-contextualized instance of `playdate.graphics.kImageFlippedX`
Noble.Animation.DIRECTION_LEFT = Graphics.kImageFlippedX