SpriteClip Sprite Sheet Animation Module

Introduction
Sprite sheets can be very useful for bypassing Roblox’s limits regarding detailed GUI animations, but most creators tend to avoid them due to them being hard to set up.

This module is my attempt to bypass this issue by packing this feature into a single easily manipulable class. The module itself only contains a single .new function which returns a SpriteClip object, to which any Instance with an “Image” property can be assigned.

There is no limit to how many sprite sheets can be run at the same time; however, the module will iterate through all created SpriteClips, so remember to use :Destroy() when they are no longer required.

Documentation
The documentation is also found in the module.

Properties:
			<Instance> Adornee				Def: nil				Desc: The image Instance to work on.
			<string> SpriteSheet			Def: nil				Desc: The asset URL of the sprite sheet. Check value below.
			<bool> InheritSpriteSheet		Def: true			Desc: Whether the SpriteSheet value will automatically take the Image value of a GUI object when the Adornee is set.
			<number> CurrentFrame			Def: 1
			<Vector2> SpriteSizePixel		Def: (100,100)			Desc: Size of individual sprite in pixels
			<Vector2> SpriteOffsetPixel		Def: (0,0)				Desc: Offset between sprites
			<Vector2> EdgeOffsetPixel 		Def: (0,0)				Desc: Offset from sprite sheet's edge
			<number> SpriteCount  			Def: 25					Desc: Global sprite count
			<number> SpriteCountX  			Def: 5					Desc: Horizontal sprite count
			<number> FrameRate  			Def: 15					Desc: Framerate that gets turned into FrameTime, needs to be a divisor of 60
			<number> FrameTime 				Def: 4					Desc: How many frames to skip-1
			<bool> Looped  					Def: true				Desc: If the CurrentFrame will reset after each cycle
			<bool> State					Def: false				Desc: Whether the animation is playing
		
Functions:
		(<bool>success) :Play()			Desc: Sets the State property to true
		(<bool>success) :Pause()		Desc: Sets the State property to false
		(<bool>success) :Stop()			Desc: Pauses the animation and resets the frames
		() :Advance(<number>count) 		Desc: Increments animation by 1 frame
		() :Destroy() 					Desc: Removes the animation from the list and clears its metatable.
		(<SpriteClip>clone) :Clone()	Desc: Creates a new SpriteClip with the same properties as the original. Doesn't copy Adornee.

NOTE: Roblox will automatically resize large images, so you will have to download the asset after uploading to check the real resolution.

Library link:
https://www.roblox.com/library/6505037974/SpriteClip

A ScreenGui model as an example of usage:
SpriteClipExample.rbxm (7.1 KB)

UPDATE:
I found an error that would cause the first frame to get skipped after one cycle. I reuploaded the module as a new asset to avoid potentially breaking any existing games and updated the example model.

UPDATE 2:
Roblox changed the link used to download assets. There are many threads describing how to do this that are obsolete. Use this link instead:
https://assetdelivery.roblox.com/v1/asset/?id=assetidhere

61 Likes

Hey There! I tried to use your module, and I have a problem and I’m not sure how to fix it.

Here I have a spritesheet of 6400x1600, which one row and four columns, looking like this

As it has four frames, I put the SpriteSizePixel to (6400/4, 1600), but then it just wasn’t visible (I made changes directly to your example code after making sure that the Instance References or the API wasn’t the problem)

local SpriteClip = require(script.SpriteClip)

local SpriteClipObject = SpriteClip.new()
local Label = script.Parent.SpriteLabel

SpriteClipObject.InheritSpriteSheet = true
SpriteClipObject.Adornee = Label

SpriteClipObject.SpriteSizePixel = Vector2.new(6400/4,1600)
SpriteClipObject.SpriteCountX = 4
SpriteClipObject.SpriteCount = 4

SpriteClipObject.FrameRate = 30
SpriteClipObject:Play()

Now, the code works, and I have verified this through checking the RectOffset for the ImageLabel, and that it is under a ScreenGui in StarterGui. Though I’m not sure how to fix this issue. You’ve written that large files gets downscaled, maybe that is it?

Max image size you can upload to Roblox is 1024x1024. Anything over that gets downscaled.

I’m working on an improved version which can use multiple images, but it’s not my priority.

4 Likes

Thanks! It works very well! Another question though, is it possible to flip a spritesheet through this module? Or will I have to import a flipped spritesheet?

There isn’t a way to properly flip images in Roblox AFAIK. Unless you use negative size.

Edit: Actually, that wont work either.

5 Likes

Yeah lol I tried negative and it broke. No Problem! Thanks for Replying!

1 Like

I was using this module and I needed to flip my sprites on the X axis so I made some minor changes that allowed for that here they are if you want them. Apologies if its not the best way of doing it just thought I would share it with you after reading this reply.

In methods.Advance

-- Before
img.ImageRectOffset = newVec2(x,y)
img.ImageRectSize = newVec2(sizex,sizey)
-- After
if not self.FlipX then
	img.ImageRectOffset = newVec2(x,y)
	img.ImageRectSize = newVec2(sizex,sizey)
else
	img.ImageRectOffset = newVec2(sizex + x,y)
	img.ImageRectSize = newVec2(-sizex,sizey)
end

In the SpriteClip.new tab table

	FlipX = false,
}

In the SpriteClip.new __newindex function

elseif i=="FlipX" then
	tab:Advance(tab.CurrentFrame)
end
1 Like

I am about to give multiple images a go! I have 84 or so frames over 4 images. Since the point of sprites is that they don’t overlap, it looks like the best way to go is to modify your “spriteclip example” to enable/disable multiple spriteclips as needed.

For one image, it runs beautifully!

This is just me overcomplicating things as always. I wanted a fancy loading wheel, ended up with a module. The example could definitely be done better, just like the module. I wanted to make a revised version that can combine multiple images, but abandoned the idea since I’d only use it once if ever and it turned out to be a lot more complicated than I thought.

1 Like

Nah, the module is great just as it is! First attempt, I got one sprite running. A little tinkering I had 4 running in sequence. Good work!

What I really want, and why I’m here, is to turn this into a health bar script that will fast forward and rewind on command. I will be using sprites everywhere in my GUI for the best reason…
because I can. Lol. There is a lot of untapped potential here!

Should have something to contribute in the next day or two.

That was also one of the planned features. Even if you can’t play the animations back and forth towards a fixed value, you should be able to use the module as a weird 9-slice helper and bind a custom function to heartbeat.

1 Like

Advancing by 1 and -1 is already working. Everything else is a function of that.

The hardest part so far is making sure that only 1 sprite sheet is ever visible at any point in time. Tricky.

I fell into a classic trap! Going from frame 1 to 25 is only 24 jumps… I was blaming visibility!

Programming is done for the time being (this is missing a revised “Play” but I don’t yet have any looped animations I can test). SpriteClip is untouched. I wrote a new HandlerScript (see below). My coding skills are improving!

Here is my revised folder setup. Handler automatically processes it.

And here is the code. My contribution to the SpriteClip Fan Club! Does not work without the SpriteClip module.

local SpriteClip = require(script.SpriteClip)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local healthUpdate = ReplicatedStorage:WaitForChild("HealthUpdate")

local SpriteClipObject = {}
local Label = {}
local framePosition ={}

function loadSprites ()
	for i, folder in pairs(script.Parent.SpriteSheets:GetChildren())  do
		if folder:IsA("Folder") then
			local spriteTask = folder.Name 
			SpriteClipObject[spriteTask] = {}
			Label[spriteTask] = {}
			framePosition[spriteTask] = 1 
			for j, sheet in ipairs (folder:GetChildren()) do
				if sheet:IsA("ImageLabel") then
					SpriteClipObject[spriteTask][j] = SpriteClip.new()
					Label[spriteTask][j] = sheet
					SpriteClipObject[spriteTask][j].InheritSpriteSheet = true
					SpriteClipObject[spriteTask][j].Adornee = Label[spriteTask][j]
					SpriteClipObject[spriteTask][j].SpriteSizePixel = Vector2.new(Label[spriteTask][j].AbsoluteSize.X,Label[spriteTask][j].AbsoluteSize.Y)
					SpriteClipObject[spriteTask][j].SpriteCountX = folder.SpriteCountX.Value
					if Label[spriteTask][j]:FindFirstChild("PartialSpriteSheet") then
						SpriteClipObject[spriteTask][j].SpriteCount = Label[spriteTask][j].PartialSpriteSheet.Value
					else		
						SpriteClipObject[spriteTask][j].SpriteCount = folder.SpriteCount.Value
					end	
					SpriteClipObject[spriteTask][j].FrameRate = folder.FrameRate.Value
					Label[spriteTask][j].Visible = false
				end
			end
		end
	end
end

function parseFrames(spriteTask, frame) -- returns which sheet and sprite belong to requested frame 
	local frameCount = 0
	local frameSheet = 0 
	local frameIndex = 0
	for i = 1, #SpriteClipObject[spriteTask] do
		local increment = SpriteClipObject[spriteTask][i].SpriteCount
		if increment >= frame then 
			frameSheet = i
			frameIndex = frame
			break
		else
			frame -= increment
		end	
	end	
	return frameSheet, frameIndex
end	

function switchClips (spriteTask, priorSheet, newSheet, clipDelay) --unhides new sheet before hiding old one, does not interrupt advanceSprite
	Label[spriteTask][newSheet].Visible = true
	wait(clipDelay)
	Label[spriteTask][priorSheet].Visible = false
end

function advanceSprite (spriteTask,toFrame) --primary function, current frame to requested frame
	local fromFrame = framePosition[spriteTask]
	local fromSheet, fromIndex = parseFrames (spriteTask, fromFrame)
	local toSheet, toIndex = parseFrames(spriteTask, toFrame)
	-- Reverse for loop code
	local advance = 1
	if toFrame < fromFrame then advance = -1 end
	--	
	local priorSheet = fromSheet
	for i = fromSheet, toSheet, advance do
		local iClip = SpriteClipObject[spriteTask][i]
		local iClipDelay = 1/iClip.FrameRate
		if i ~= priorSheet then
			switchClips(spriteTask,priorSheet,i,iClipDelay)	
		end		
		local startFrame = 0
		local endFrame = 0
		if i == fromSheet then
			startFrame = fromIndex
		else
			if advance == -1 then
				startFrame = iClip.SpriteCount
			else 
				startFrame = 1
			end
		end
		if i == toSheet then
			endFrame = toIndex
		else
			if advance == -1 then
				endFrame = 1
			else 
				endFrame = iClip.SpriteCount
			end
		end
		for j = startFrame+advance, endFrame, advance do
			iClip:Advance(advance)
			wait(iClipDelay)
		end
		priorSheet = i
		framePosition[spriteTask] = toFrame
	end
end	


loadSprites()	
-- everything below this line is game specific.  In my case, filling the health gauge then waiting for damage
wait(10)
Label["HealthBar"][1].Visible = true
advanceSprite("HealthBar", 70)

healthUpdate.OnClientEvent:Connect(function (percentHealth)
	advanceSprite("HealthBar", math.floor(percentHealth * 70))
end)
2 Likes

You can finally change the resample mode in ui objects so now it wont get blurry when you make small images, that can help a lot with this module for pixel art stuff


(you can see the pixels, and it isnt blurry)

can you do like a youtube explanation because i do not get a single bit of it

1 Like