Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Sprite Clipping and Stretching

Sprite Clipping and Stretching screenshot

Last Updated: Oct 13th, 2024

In this tutorial we'll be taking some sprites on a sprite sheet and drawing them at varying sizes.
For this tutorial we'll have these four circle sprites
circle sprites

And we'll clip them from the sprite sheet and render them individually.
/* Class Prototypes */
class LTexture
{
public:
    //Symbolic constant
    static constexpr float kOriginalSize = -1.f;

    //Initializes texture variables
    LTexture();

    //Cleans up texture variables
    ~LTexture();

    //Loads texture from disk
    bool loadFromFile( std::string path );

    //Cleans up texture
    void destroy();

    //Draws texture
    void render( float x, float y, SDL_FRect* clip = nullptr, float width = kOriginalSize, float height = kOriginalSize );

    //Gets texture dimensions
    int getWidth();
    int getHeight();

private:
    //Contains texture data
    SDL_Texture* mTexture;

    //Texture dimensions
    int mWidth;
    int mHeight;
};
At the top of our LTexture class, we're going to define a symbolic constant for when we want to render the sprites to their original size.

Symbolic constants may seem unnecessary but they make your code much more readable. Which of these is clearer, this?
                    else if( e.type == SDL_EVENT_KEY_DOWN )
                    {
                        if( e.key.key == SDLK_U )
                        {
                            action = 1;
                        }
                        else if( e.key.key == SDLK_I )
                        {
                            action = 2;
                        }
                        else if( e.key.key == SDLK_J )
                        {
                            action = 3;
                        }
                        else if( e.key.key == SDLK_K )
                        {
                            action = 4;
                        }
                    }
or this?
                    else if( e.type == SDL_EVENT_KEY_DOWN )
                    {
                        if( e.key.key == SDLK_U )
                        {
                            action = kActionPrevWeapon;
                        }
                        else if( e.key.key == SDLK_I )
                        {
                            action = kActionNextWeapon;
                        }
                        else if( e.key.key == SDLK_J )
                        {
                            action = kActionFireWeapon;
                        }
                        else if( e.key.key == SDLK_K )
                        {
                            action = kActionReloadWeapon;
                        }
                    }

For our texture rendering when the user gives a size less than 0 (and they probably don't want to render an image of a negative size), we're going to assume they want the original image size. Using width = kOriginalSize is more intuitive than saying width = -1.f.

For our texture rendering function, we'll be passing in a clip rectangle to define the sprite we're using (if any) in addition to the image dimensions we want to use.
void LTexture::render( float x, float y, SDL_FRect* clip, float width, float height )
{
    //Set texture position
    SDL_FRect dstRect = { x, y, static_cast( mWidth ), static_cast( mHeight ) };

    //Default to clip dimensions if clip is given
    if( clip != nullptr )
    {
        dstRect.w = clip->w;
        dstRect.h = clip->h;
    }

    //Resize if new dimensions are given
    if( width > 0 )
    {
        dstRect.w = width;
    }
    if( height > 0 )
    {
        dstRect.h = height;
    }

    //Render texture
    SDL_RenderTexture( gRenderer, mTexture, clip, &dstRect );
}
For our new rendering function, we'll check if a clip rectangle was passed in. If it was we set the clip rectangle as the image default size. If size overrides were given resize the image to that override.

Now that we have the clip rectangle defined and the destination rectangle defined we render the texture with SDL_RenderTexture.
                //Fill the background white
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );
                
                //Init sprite clip
                constexpr float kSpriteSize = 100.f;
                SDL_FRect spriteClip = { 0.f, 0.f, kSpriteSize, kSpriteSize };
                
                //Init sprite size
                SDL_FRect spriteSize = { 0.f, 0.f, kSpriteSize, kSpriteSize };
Before we start our rendering code in the main loop we want to initialize the clip rectangle and size rectangle. All the sprites are 100 x 100 pixels so the clip each sprite is just a matter of repositioning the clip rectangle.
                //Use top left sprite
                spriteClip.x =         0.f;
                spriteClip.y =         0.f;

                //Set sprite size to original size
                spriteSize.w = kSpriteSize;
                spriteSize.h = kSpriteSize;

                //Draw original sized sprite
                gSpriteSheetTexture.render(                         0.f,                          0.f, &spriteClip, spriteSize.w, spriteSize.h );
For the top left sprite we position the clip rectangle at the top left and use the original size and draw it with our render function.
                //Use top right sprite
                spriteClip.x = kSpriteSize;
                spriteClip.y =         0.f;

                //Set sprite to half size
                spriteSize.w = kSpriteSize * 0.5f;
                spriteSize.h = kSpriteSize * 0.5f;

                //Draw half size sprite
                gSpriteSheetTexture.render( kScreenWidth - spriteSize.w,                          0.f, &spriteClip, spriteSize.w, spriteSize.h );
For the top right sprite we reposition the clip rectangle and set the sprite size to half the original because we want to draw a smaller sprite.
                //Use bottom left sprite
                spriteClip.x =         0.f;
                spriteClip.y = kSpriteSize;

                //Set sprite to double size
                spriteSize.w = kSpriteSize * 2.f;
                spriteSize.h = kSpriteSize * 2.f;
                
                //Draw double size sprite
                gSpriteSheetTexture.render(                         0.f, kScreenHeight - spriteSize.h, &spriteClip, spriteSize.w, spriteSize.h );


                //Use bottom right sprite
                spriteClip.x = kSpriteSize;
                spriteClip.y = kSpriteSize;

                //Squish the sprite vertically
                spriteSize.w = kSpriteSize;
                spriteSize.h = kSpriteSize * 0.5f;
                
                //Draw squished sprite
                gSpriteSheetTexture.render( kScreenWidth - spriteSize.w, kScreenHeight - spriteSize.h, &spriteClip, spriteSize.w, spriteSize.h );
                

                //Update screen
                SDL_RenderPresent( gRenderer );
For the bottom right sprite, we're going to render the sprite double size. For the bottom right, we're going to squish the sprite vertically. Once all the sprites are rendered we update the screen with SDL_RenderPresent

Addendum: YAGNI

I am not a fan of how this code is structured. Yes, I was the person who designed this code but making code you don't like is something that will happen in your professional career.

See in a real game engine designed to scale, I would decouple the sprite definition (which would consist of a clip rectangle and a pointer to the sprite sheet it comes from) and the object's position/scaling transformation into their own individual classes. However, to do so would require making two additional classes and probably a game object class to bring it all together. This demo manages to do it all by adding a few lines to the LTexture class and not having to juggle multiple classes. For these tutorials I will sacrifice code that is technically better designed but more complex for code that is smaller and easier to keep track of even if it's clunky.

If you were making a platformer that was going to have multiple animations across multiple sprite sheets you would probably want to decouple sprite definition from sprite transfromation. For a Nasty Tetris Project, it's better to just keep your code to be minimal because you just want to get it done. This is the principle of You Aren't Gonna Need It.

Your Nasty Tetris Project is going to be nasty. Trying to make it flexible or well designed is just going to lead to it being over engineered. Just focus on getting it done with as little code as possible.