297 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			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
 |