Introduction
SpriteClip2 is a module that simplifies the process of creating and animating both very simple and complex sprites in Roblox.
This is a direct successor to my older SpriteClip module. It was fairly popular, but I was never happy with its lack of features and the amount of quirks. The API is very similar, but uses a different case for personal reasons. Paused sprites are now properly handled and no longer have to be destroyed to avoid looping through hundreds of sprites every frame.
To quickly see it in action, place the module into StarterPlayerScripts, open the config module inside the SpriteClip2 module, and set IsDemoMode to true.
Getting started
Roblox doesn’t allow users to upload images bigger than 1024x1024. If your image is either taller or wider than 1024 pixels, it will be scaled down.
If you are not sure, use this link to download your uploaded image and check its final resolution:
https://assetdelivery.roblox.com/v1/asset/?id=assetidhere
A more detailed tutorial can be found here.
And here’s the place file from that tutorial:
SpriteClip2 github example.rbxl (67.5 KB) (everything important is in StarterGui)
To achieve proper alignment, you’ll have to find these values:
- spriteSize - Exact size of the sprites in pixels.
- edgeOffset - Distance between the top-left sprite and the top-left corner of the image in pixels.
- spriteOffset - Distance between individual sprites in pixels.
- spriteCount - Exact number of sprites.
- columnCount - How many sprites are next to each other horizontally.
Once you’ve uploaded your image, create a new ImageLabel. Require the module and create a new ImageSprite, while providing it with the correct properties. Then call :Play() to run the animation.
Example code
local SpriteClip = require(script.Parent);
local label = Instance.new("ImageLabel");
label.Size = UDim2.fromScale(300,300);
label.Parent = Instance.new("ScreenGui", game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui"));
local sprite = SpriteClip.ImageSprite.new({
adornee = label,
spriteSheetId = "rbxassetid://104239803996382";
spriteSize = Vector2.new(150,150);
spriteCount = 12;
columnCount = 6;
frameRate = 3;
isLooped = true;
});
sprite:Play();
Documentation
There are 5 different types of sprites you can create, as well as some other QOL features.
The 3 basic sprites are ImageSprite, EditableSprite and CompatibilitySprite:
- ImageSprite - Automatically applies the spritesheet to its adornee ImageLabel/ImageButton as it plays.
- EditableSprite - Does the same but with EditableImages (view notes below).
- CompatibilitySprite - Its behavior is almost identical to the old SpriteClip module, so it can serve as a drop-in replacement.
The 2 more advanced ones are ScriptedImageSprite and ScriptedEditableSprite. Unlike the previous 3, these do not apply the spritesheet automatically and will instead call an onRenderCallback function every frame. This way, you get to chose what portion of the spritesheet you want to render at any time, which makes them especially useful for dynamic sprites, such as for characters.
The Scheduler is an object used internally by the sprite classes, but can also be used to pause/resume render calls for all sprites (e.g. can be used for pause menus) as well as to get their framerate signals.
API
CompatibilitySprite API is identical to the older module.
ImageSprite
-- creates a new ImageSprite
MainModule.ImageSprite.new(props:ImageSpriteProps)->(ImageSprite)
-- provides initial properties to the sprite, all properties are optional
ImageSpriteProps = {
adornee: ImageLabel|ImageButton?;
spriteSheetId: string?;
currentFrame: number?;
spriteSize: Vector2?;
spriteOffset: Vector2?;
edgeOffset: Vector2?;
spriteCount: number?;
columnCount: number?;
frameRate: number?;
isLooped: boolean?;
}
-- format: name: type -- (READONLY) [default] description
ImageSprite = {
-- properties
adornee: ImageLabel|ImageButton?; -- [nil] the image label/button to apply the sprite to, sprite does nothing if nil
spriteSheetId: string; -- [""] the asset id of the sprite sheet, sprite does nothing if ""
currentFrame: number; -- READONLY [1] index of the frame that is currently visible (starts from 1)
spriteSize: Vector2; -- [0,0] size of the individual sprites in pixels
spriteOffset: Vector2; -- [0,0] offset between individual sprites in pixels
edgeOffset: Vector2; -- [0,0] offset from the image's (0,0) corner in pixels
spriteCount: number; -- [0] total number of sprites
columnCount: number; -- [0] total number of columns (left-to-right sprite count)
frameRate: number; -- [30] max frame rate the sprite can achieve when playing (can be any number, but will be clamped by RenderStepped frame rate)
isLooped: boolean; -- [true] if the sprite will loop while playing (stops at last frame otherwise)
isPlaying: boolean; -- READONLY [false] whether the sprite is playing or not
-- methods
Play: (self:ImageSprite, playFrom:number?)->(boolean); -- plays the animation
Pause: (self:ImageSprite)->(boolean); -- pauses the animation
Stop: (self:ImageSprite)->(boolean); -- pauses the animation and resets the current frame to 1
SetFrame:(self:ImageSprite, frame:number)->(); -- manually sets the current frame
Advance:(self:ImageSprite)->(); -- manually advances to the next frame, or 1 if last
GetSignal:(self:ImageSprite, signalType:SignalType)->(RBXScriptSignal) -- view the SignalTypes category
}
EditableSprite
-- creates a new EditableSprite
MainModule.EditableSprite.new(props:EditableSpriteProps)->(EditableSprite)
-- provides initial properties to the sprite, all properties are optional
EditableSpriteProps = {
inputImage: EditableImage|string?;
outputImage: EditableImage?;
outputPosition: Vector2?;
currentFrame: number?;
spriteSize: Vector2?;
spriteOffset: Vector2?;
edgeOffset: Vector2?;
spriteCount: number?;
columnCount: number?;
frameRate: number?;
isLooped: boolean?;
}
-- format: name: type -- (READONLY) [default] description
EditableSprite = {
-- properties
inputImage: EditableImage?; -- READONLY [nil] EditableImage to read the pixel data from, change using LoadInputImage
outputImage: EditableImage; -- [nil] EditableImage to write the pixel data to, can be replaced with a different EditableImage
outputPosition: Vector2; -- [0,0] render offset for the output image, useful for storing multiple sprites as an atlas
currentFrame: number; -- READONLY [1] index of the frame that is currently visible (starts from 1)
spriteSize: Vector2; -- [0,0] size of the individual sprites in pixels
spriteOffset: Vector2; -- [0,0] offset between individual sprites in pixels
edgeOffset: Vector2; -- [0,0] offset from the image's top-left corner in pixels
spriteCount: number; -- [0] total number of sprites
columnCount: number; -- [0] total number of columns (left-to-right sprite count)
frameRate: number; -- [30] max frame rate the sprite can achieve when playing (can be any number, but will be clamped by RenderStepped frame rate)
isLooped: boolean; -- [true] if the sprite will loop while playing (stops at last frame otherwise)
isPlaying: boolean; -- READONLY [false] whether the sprite is playing or not
-- methods
Play: (self:EditableSprite, playFrom:number?)->(boolean); -- plays the animation
Pause: (self:EditableSprite)->(boolean); -- pauses the animation
Stop: (self:EditableSprite)->(boolean); -- pauses the animation and resets the current frame to 1
SetFrame:(self:EditableSprite, frame:number)->(); -- manually sets the current frame
Advance:(self:EditableSprite)->(); -- manually advances to the next frame, or 1 if last
LoadInputImage: (self:EditableSprite, newInput:EditableImage|string)->(); -- async if given a string, replaces the input image with a new one
GetSignal:(self:ImageSprite, signalType:SignalType)->(RBXScriptSignal) -- view the SignalTypes category
};
CompatibilitySprite
-- creates a new CompatibilitySprite
MainModule.CompatibilitySprite.new()->(CompatibilitySprite)
CompatibilitySprite = {
-- properties
Adornee :Instance?;
SpriteSheet :string?;
InheritSpriteSheet :true;
CurrentFrame :number;
SpriteSizePixel :Vector2;
SpriteOffsetPixel :Vector2;
EdgeOffsetPixel :Vector2;
SpriteCount :number;
SpriteCountX :number;
FrameRate :number;
FrameTime :number;
Looped :boolean;
State :boolean;
-- methods
Play:(self:CompatibilitySprite)->(boolean);
Pause:(self:CompatibilitySprite)->(boolean);
Stop:(self:CompatibilitySprite)->(boolean);
Advance:(self:CompatibilitySprite, advanceby:number)->();
Destroy:(self:CompatibilitySprite)->();
Clone:(self:CompatibilitySprite)->(CompatibilitySprite);
}
ScriptedImageSprite
-- creates a new ScriptedImageSprite
MainModule.ScriptedImageSprite.new(props:ScriptedImageSpriteProps)->(ScriptedImageSprite)
-- format: name: type -- (READONLY) [default] description
ScriptedImageSprite = {
-- properties
adornee: ImageLabel|ImageButton?; -- [nil] the image label/button to apply the sprite to, sprite does nothing if nil
spriteSheetId: string; -- [""] the asset id of the sprite sheet, sprite does nothing if ""
currentFrame: number; -- READONLY [1] index of the frame that is currently visible (starts from 1)
spriteSize: Vector2; -- [0,0] size of the individual sprites in pixels
spriteOffset: Vector2; -- [0,0] offset between individual sprites in pixels
edgeOffset: Vector2; -- [0,0] offset from the image's (0,0) corner in pixels
frameRate: number; -- [30] max frame rate the sprite can achieve when playing (can be any number, but will be clamped by RenderStepped frame rate)
isPlaying: boolean; -- READONLY [false] whether the sprite is playing or not
-- methods
Play: (self:ScriptedImageSprite)->(boolean); -- plays the animation
Pause: (self:ScriptedImageSprite)->(boolean); -- pauses the animation
SetFrame: (self:ScriptedImageSprite, frame:Vector2)->(); -- manually sets the current frame
Advance: (self:ScriptedImageSprite)->(); -- manually advances to the next frame, or 1 if last
-- callbacks
onRenderCallback: (self:ScriptedImageSprite)->()?; -- called every frame when playing
};
-- provides initial properties to the sprite, all properties are optional
ScriptedImageSpriteProps = {
adornee: ImageLabel|ImageButton?;
spriteSheetId: string?;
currentFrame: number?;
spriteSize: Vector2?;
spriteOffset: Vector2?;
edgeOffset: Vector2?;
frameRate: number?;
isLooped: boolean?;
onRenderCallback: (self:ScriptedImageSprite)->()?;
}
ScriptedEditableSprite
-- creates a new ScriptedEditableSprite
MainModule.ScriptedEditableSprite.new(props:ScriptedEditableSpriteProps)->(ScriptedEditableSprite)
ScriptedEditableSprite = {
-- properties
inputImage: EditableImage?; -- READONLY [nil] EditableImage to read the pixel data from, change using LoadInputImage
outputImage: EditableImage; -- [nil] EditableImage to write the pixel data to, can be replaced with a different EditableImage
outputPosition: Vector2; -- [0,0] render offset for the output image, useful for storing multiple sprites as an atlas
currentFrame: number; -- READONLY [1] index of the frame that is currently visible (starts from 1)
spriteSize: Vector2; -- [0,0] size of the individual sprites in pixels
spriteOffset: Vector2; -- [0,0] offset between individual sprites in pixels
edgeOffset: Vector2; -- [0,0] offset from the image's top-left corner in pixels
frameRate: number; -- [30] max frame rate the sprite can achieve when playing (can be any number, but will be clamped by RenderStepped frame rate)
isPlaying: boolean; -- READONLY [false] whether the sprite is playing or not
-- methods -- removed: Stop,
Play: (self:ScriptedEditableSprite, playFrom:number?)->(boolean); -- plays the animation
Pause: (self:ScriptedEditableSprite)->(boolean); -- pauses the animation
SetFrame:(self:ScriptedEditableSprite, frame:number)->(); -- manually sets the current frame
Advance:(self:ScriptedEditableSprite)->(); -- manually advances to the next frame, or 1 if last
LoadInputImage: (self:ScriptedEditableSprite, newInput:EditableImage|string)->(); -- async if given a string, replaces the input image with a new one
-- callbacks
onRenderCallback: (self:ScriptedEditableSprite)->()?; -- called every frame when playing
};
-- provides initial properties to the sprite, all properties are optional
ScriptedEditableSpriteProps = {
inputImage: EditableImage|string?;
outputImage: EditableImage?;
outputPosition: Vector2?;
currentFrame: number?;
spriteSize: Vector2?;
spriteOffset: Vector2?;
edgeOffset: Vector2?;
frameRate: number?;
isLooped: boolean?;
onRenderCallback: (self:ScriptedEditableSprite)->()?;
}
Scheduler
GetPreRenderSignal : (self, framerate:number)->(RBXScriptSignal) -- fires right before sprites are rendered
GetOnRenderSignal : (self, framerate:number)->(RBXScriptSignal) -- fires at the same time sprites are rendered
GetPostRenderSignal : (self, framerate:number)->(RBXScriptSignal) -- firest after sprites are rendered
GetSignal: (self, framerate:number)->(RBXScriptSignal) -- deprecated, use GetOnRenderSignal instead
Pause: (self)->() -- pauses render events for all sprites
Resume: (self)->() -- resumes paused render events for all sprites
SignalTypes
(ImageSprite | EditableSprite):GetSignal(signalType:SignalType) -> RBXScriptSignal
-- format: "SignalName" | -- (callback arguments) - description
type SignalType =
"FrameLast" | -- () - fires when the sprite hits its last frame
"Looped" | -- () - fires when a looped sprite goes from its last to first frame
"FrameChanged" |-- () - fires on frame change
"PlayCalled" | -- () - fires when :Play() is called if the sprite isn't playing
"PauseCalled" | -- () - fires when :Pause() is called if the sprite is playing
"StopCalled" | -- () - fires when :Stop() is called if the sprite is playing
"StaticChanged" -- (propName:string) - fires when a static property has changed, such as: such as: inputImage, outputImage, outputPosition, spriteSize, spriteOffset, edgeOffset, spriteCount, columnCount, frameRate, isLooped
-- An example that prints the current frame every time it's advanced
local signal = sprite:GetSignal("FrameChanged");
signal:Connect(function() print(signal.currentFrame); end);
Links
Github: GitHub - nooneisback/luau-spriteclip2
Store: https://create.roblox.com/store/asset/111579633111377/SpriteClip2
The github tutorial place file: tester.rbxl (62.8 KB) (everything important is in StarterGui)
Notes
Please report any bugs or other issues you find.
Updates
- Added GetPreRenderSignal, GetOnRenderSignal and GetPostRenderSignal methods to the Scheduler.
- Added a link to a more detailed tutorial in the Getting started section
- Updated the module to support the new EditableImages API
- Now sprites will call :Stop if their adornee is destroyed (does not trigger if the adornee property is set to nil)
- Added a signal system that doesn’t add any overhead if unused