Thursday, August 2, 2012

Lesson 5: Clipping Sprite Sheets

Deprecation Notice:

I've been updating the lessons now that SDL2 is officially released, please visit my new site on Github that has updated lessons and improved code.


It's very common in sprite based games to use a large image file containing many smaller images, such as tiles for a tileset, as opposed to having a separate image file for each tile. This type of image is known as a sprite sheet, and is very useful because we don't need to have a separate image file for each image in the game but instead can just draw the subset of the sheet that we want.

In this lesson we will learn how to use a simple sprite sheet in our program and how we can specify the desired subset, known as a clip, to draw. The sprite sheet for this lesson is a simple one containing four circles:
In a sprite sheet images are divided up into sections of some sort of basic shape that we can clip. In this sheet each circle is within a 100x100 rectangle that we can pick to draw, instead of the entire image.

The code from this lesson will be building upon the code from Lesson 4, if you haven't done Lesson 4 you should head back and do it. If you already know the material covered in Lesson 4 grab the code and let's get started.

Within our ApplySurface function from Lesson 4 you'll notice that there's one last NULL value being passed to SDL_RenderCopy.
This parameter is the source rect, more commonly known as the clip, and specifies a sub-rectangle of the image to draw, ie. the clip's position, width and height. In order to pass a clip for the image we'll want to add a clip parameter to ApplySurface.

But what if we had an image where we did want to draw the whole thing? Instead of forcing ourselves to pass a clip of the whole image, we can instead specify a default parameter and detect when we get it. We'll take the clip parameter as an SDL_Rect pointer with a default value of NULL, that way if no clip is passed we can still pass the parameter to RenderCopy and the call will be the same as it was previously when we passed NULL to draw the whole image.

We'll also want to add one more thing to our function: If we pass a clip parameter we clearly would want to use the clip's width and height as our destination values instead of the whole texture's width and height. If we used the texture's width and height our clip would be stretched to match the size of the original texture.
We'll load our image the same as before with or LoadImage function.

We now need to set up our clip rects, which we'll store inside an array. Instead of typing all of our clips out by hand which is very tedious for images with lots of clips, we can take advantage of the pattern in which our image file is constructed to automate the creation of the clip rects with a for loop. If you did the extra challenge from Lesson 2 this method should look quite familiar.

First we specify the width and height each clip should be, in this case 100x100 and we create an array to store four SDL_Rects to hold our clips. Next we use our knowledge of the arrangement of our clips to create a for loop with a column counter to appropriately set the x,y coordinate for each clip.
If you didn't do the Lesson 2 challenge or are feeling a bit lost, let me explain how this works. We want to create four clips, so we set our loop to run four times, from zero to three to match the array indices. We also need to track which column we're going down so that we can correctly set the x coordinate for our clip, we'll begin with column zero and will increment the column value when we move over, which would be at the third clip, ie. i % 2 == 0 since we start counting from zero. We also want to make sure we don't increment column when we first start out, because 0 % 2 is also 0, so we put a condition to ignore that case.

Now we'll want to calculate the x and y coordinates for our clip in the image. We want to increment the x coordinate by the image width each time we move over a column, and increase the y each time we move down one in the column. The x coordinate setting should be clear, we can just use tile width * column. The y coordinate setting uses our current loop iteration mod the number of clips per column to determine which row we're on. We then multiple this row number by the tile height to get the location in pixels. The width and height are uniform for all clips so we can just set them, without any extra calculations.

Thus when we run this loop we will create four clips and each clip will take the appropriate coordinate for the sub-rectangle. Still not sure how it works? Try running the for loop in your head and calculating the column, x and y values for each iteration and see where they line up on the sprite sheet.

The last step is to set a value which can track which clip we want to draw, in this case an integer corresponding to the clip's index in the array. Let's begin by drawing clip 0.
Before beginning our main loop we'll also need to set an x and y position to draw the image at. I chose to calculate the center position for the image using the same method as in previous lessons. We'll also want to create a bool quit variable and an SDL_Event.

In order to make sure our clips were set correctly and display correctly we'll want to be able to draw each one individually, to do this we can set up some event polling to change the value of useClip so that we can draw each clip.
Here we check if the input type is a key down event, and then use a switch statement on the key symbol to pick the appropriate response.

Finally in our render section we clear the screen, pass the clip's value to our ApplySurface function in the rendering section of our loop:
And finally, present the renderer to display the changes.

When you run the program you should be able to push the number keys 1-4 and see the different colored circles appear individually on screen!

End of Lesson 5

Thanks for joining me! I'll see you again soon in Lesson 6: True Type Fonts with SDL_ttf.

7 comments:

  1. I liked a lot the loop to track clipping, once I did an encoder/decoder to convert a matrix position to integer and retrieve the same matrix position, in this case the number 2 in your loop was the constant to determine everything in the calculations.
    Very well explained and creative tutorial!

    ReplyDelete
  2. The link to lesson six doesn't exist. I didn't realize you had more until days later when I clicked back to the "SDL 2.0 Tutorial Index" page. Time to go read the rest!

    ReplyDelete
  3. For extra credit, I decided to add the ability to move the circles using the arrow keys. I was stymied at first by the fact that keys seemed to auto-repeat and override each other, until I realized the solution was to track both SDL_KEYDOWN and SDL_KEYUP events, toggling "move in such-and-such direction" bools appropriately.

    I should have remembered this from porting an SDL-based game to the Wii, as I had to add Wii-specific SDL input event handler code :p

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. If I am right, you may use code "clips + useClip" instead of "&clips[useClip]" at [6.cpp]. Personally, I prefer that code, It seems to be more clear. This is a small joke.... ^_^

    ReplyDelete
    Replies
    1. Hah yea you can pass the pointer directly by computing the offset, it's equivalent.

      Delete