Sprite Clipping and Stretching
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
And we'll clip them from the sprite sheet and render them individually.
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?
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
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.
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
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
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.
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.