SpriteClip2 - A Free Versatile Spritesheet Animation Module

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

The same limit applies to EditableImages. If you try setting a size over (1024,1024), it will just clamp it back with no warning.

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
    }

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
    };
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

Links

Github: GitHub - nooneisback/luau-spriteclip2
Store: https://create.roblox.com/store/asset/111579633111377/SpriteClip2
The github tutorial place file: SpriteClip2 github example.rbxl (67.5 KB) (everything important is in StarterGui)

Notes

EditableImages are currently a beta Roblox feature and not usable outside Studio.
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
7 Likes

This is simmilar to my paper2d plugin and framework for 2d game development

Exactly the same or the same idea?

I can`t access the link for the model

1 Like

Interesting, for whatever reason Roblox didn’t apply the change to distribute on the marketplace when I uploaded the update. Should be fixed now.

Yeah it works now, would you mind making a visual tutorial that goes over this module if you got the time?

The main post already has a link to a more detailed tutorial with images

My connection is impossibly slow, so it’d take about 30 minutes just to upload a 640p 10min video.