Since @WoozyNate is now using this resource for DIG, I’ve remade the module so it has more functionality and is easier to use.
Hopefully everything here should be self explanatory.
Constructor:
Sprite.new(params: {
images: {string}, -- asset ids in order
imageSize: UDim2,
xFrames: number, -- number of horizontal frames in a single image of the spritesheet
framesPerImage: number,
totalAnimationFrames: number?, -- ignore this unless your spritesheet has empty frames
fps: number?, -- defaults to 24
cycles: number?, -- number of times animation plays, defaults to -1 (infinite)
imageTransparency: number?, -- defaults to 0
resampleMode: Enum.ResamplerMode?, -- defaults to Default
}): SpriteObject
Methods:
SpriteObject:Play() -- can only be called once
SpriteObject:Pause()
SpriteObject:Unpause()
SpriteObject:Destroy() -- if called the SpriteObject cannot be brought back
SpriteObject:GetContainer() -- returns the Frame that holds all the ImageLabels used under the hood
Example usage:
local sprite = require(script:WaitForChild("Sprite"))
local gui = script.Parent
local spriteObject = sprite.new{
images = {"rbxassetid://108492549606073","rbxassetid://115432300557277","rbxassetid://108107306918025","rbxassetid://123902261605426"},
imageSize = UDim2.fromOffset(200,200),
xFrames = 5,
framesPerImage = 25,
fps = 100
}
local container = spriteObject:GetContainer()
container.AnchorPoint = Vector2.one
container.Position = UDim2.new(1,-10,1,-10)
container.Parent = gui
spriteObject:Play()
Result:
SOURCE CODE:
local ContentProvider = game:GetService("ContentProvider")
local RunService = game:GetService("RunService")
local Sprite = {}
Sprite.__index = Sprite
type SpriteParameters = {
images: {string},
imageSize: UDim2,
xFrames: number,
framesPerImage: number,
totalSpriteFrames: number?,
fps: number?,
cycles: number?,
imageTransparency: number?,
resampleMode: Enum.ResamplerMode?
}
function Sprite.new(params: SpriteParameters)
local self = setmetatable({}, Sprite)
self.images = params.images
self.imageSize = params.imageSize
self.xFrames = params.xFrames
self.framesPerImage = params.framesPerImage
self.totalSpriteFrames = params.totalSpriteFrames or (#self.images * self.framesPerImage)
self.fps = params.fps or 24
self.cycles = params.cycles or -1
self.imageTransparency = params.imageTransparency or 0
self.resampleMode = params.resampleMode or Enum.ResamplerMode.Default
ContentProvider:PreloadAsync(self.images)
local container = Instance.new("Frame")
container.Size = self.imageSize
container.ClipsDescendants = true
container.BackgroundTransparency = 1
self.container = container
self._imageLabels = {}
for _, imageId in ipairs(self.images) do
local img = Instance.new("ImageLabel")
img.Size = UDim2.new(self.xFrames, 0, math.floor(self.framesPerImage / self.xFrames), 0)
img.Position = UDim2.new(0, 0, 0, 0)
img.Image = imageId
img.BackgroundTransparency = 1
img.ImageTransparency = self.imageTransparency
if self.resampleMode then
img.ResampleMode = self.resampleMode
end
img.Visible = false
img.Parent = container
table.insert(self._imageLabels, img)
end
self._isPaused = true
self._destroyed = false
self._currentImage = 1
self._currentCycle = 0
self._playThread = nil
self:_preloadImages()
return self
end
function Sprite:_preloadImages()
task.spawn(function()
repeat
local allLoaded = true
for _, img in ipairs(self._imageLabels) do
if not img.IsLoaded then
allLoaded = false
break
end
end
if allLoaded then break end
task.wait()
until false
for _, img in ipairs(self._imageLabels) do
img.Visible = false
end
end)
end
function Sprite:Play()
if self._playThread then return end
self._isPaused = false
self._currentCycle = 0
self._currentImage = 1
self._playThread = task.spawn(function()
local counter = 0
while not self._destroyed and (self.cycles == -1 or self._currentCycle < self.cycles) do
if self._currentImage == 1 then counter = 0 end
if self._isPaused then
RunService.Heartbeat:Wait()
continue
end
local currentImageLabel = self._imageLabels[self._currentImage]
currentImageLabel.Visible = true
local yFrames = math.ceil(self.framesPerImage / self.xFrames)
for y = 0, yFrames - 1 do
for x = 0, self.xFrames - 1 do
if self._isPaused or self._destroyed then break end
counter += 1
if counter > self.totalSpriteFrames then break end
currentImageLabel.Position = UDim2.new(-x, 0, -y, 0)
task.wait(1 / self.fps)
end
if counter > self.totalSpriteFrames then
counter = 0
break
end
end
currentImageLabel.Visible = false
self._currentImage = (self._currentImage % #self._imageLabels) + 1
self._currentCycle += 1
end
end)
end
function Sprite:Pause()
self._isPaused = true
end
function Sprite:Unpause()
self._isPaused = false
end
function Sprite:Destroy()
self._destroyed = true
if self._playThread then
task.cancel(self._playThread)
self._playThread = nil
end
for _, img in ipairs(self._imageLabels) do
img:Destroy()
end
if self.container then
self.container:Destroy()
end
end
function Sprite:GetContainer()
return self.container
end
return Sprite
When using this resource, a good practice is to preload any sprite images a couple seconds before they actually get used by the module to prevent flickering when the sprite switches images (since the images aren’t loaded.) If you follow this practice, you can delete the _preloadImages() method and the line where it’s called in the constructor.
It’s worth noting that I haven’t tested every single edge case so there is a chance that some less standard sprites may behave weirdly. I’ve tested the more common use cases and they all work fine and I will edit this post in the event of one of these edge cases being an issue. Enjoy!