• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Sprite Atlas Tutorial —
Now that we have a texture atlas created, we can put it to use in an SDL2 application. This tutorial will demonstrate how that's done, by rendering a demo scene using some of the sprites available in the atlas. Compile the code using make, and then run it with ./atlas04. An SDL2 window will open, displaying the scene above. To exit, close the window.
A quick word on the file layout changes. You'll notice that the gfx directory now only contains the atlas.png file, and that the atlas.json file lives in the data directory. In addition, the individual sprites have been moved into the dev directory. In order to create the sprite atlas, we now run a script using ./createAtlas.sh. This script uses the files in dev/gfx to generate the atlas, then moves the resulting atlas files into gfx and data:
#!/bin/bash -e cd dev ../gen04 -dir gfx mv atlas.png ../gfx/ mv atlas.json ../data/
The reason for this is so that we don't have both the atlas and the individual files in the gfx directory, which would make building an archive for distribution more difficult. The shell script helps us in this regard, as we need just ignore the entire dev directory.
Inspecting the code
There have been quite a few new files added to this tutorial. However, as plenty of bits and pieces were already covered in previous tutorials (such as setting up SDL, etc.) we're going to skip over those parts. Instead, we'll focus on what has gone into using our sprite atlas. We've also added a number of new structs to structs.h. We're only really interested in one, however:
This is the most important struct we're going to need, as it will hold the data for an entry into our sprite atlas. The filename, coordinates, and a pointer to the atlas itself are all included. Now, let's move onto loading the atlas data. This is all done in atlas.c. The initAtlas function sets things up:
The first thing we're doing is zeroing the memory for an array of AltasImages. This array of AtlasImages will act as the head to each bucket in your hashmap. The reason we're doing this is to speed up the lookups when fetching an image. While our demo is simple, having only 33 images, a larger atlas (for example, of 4096x4096), could contain 100s (or even 1000s of images). Testing up to this many strings while looking up a single sprite isn't all that efficient, so we break them down into smaller lists (we'll see more on this in a moment).
We're also loading the main atlas image (gfx/atlas.png) using SDL_Image's IMG_LoadTexture function. With the image and the hashmap prepared, we can then call loadAtlasData to load the meta data for the sprites.
It might look a bit complicated, but it's actually very straightforward:
To begin with, we want to do is load the atlas meta data JSON file, by calling our readFile function (see util.c). With the text data loaded, we're going to once again make use of cJSON to parse it into a JSON object. This is as easy as calling cJSON_Parse, passing in the loaded text. We'll get back a JSON object to work with (in this case, an array).
We'll then loop through the cJSON array, by selecting the root node's child and processing each sibling in turn. We're grabbing the filename, x, y, w, h, values of each JSON object, which represent an entry in our sprite atlas. With those grabbed, we want to find out which bucket of our hashmap we'll be adding it into. We create a hashcode from the filename, by passing it to the hashcode function (see util.c), and then calculate of the modulo using our NUM_ATLAS_BUCKETS constant. With the bucket determined, we step through it until we find the final entry (the one acting as the tail in the linked list), which our new atlas image will follow on from.
A new AtlasImage is malloc'd and appended to the tail of the bucket. The filename is then set, along with the x, y, w, and h. Each AtlasImage has an SDL_Rect that will hold the coordinates of the area the sprite occupies. Finally, the texture of the atlas itself is set into the AtlasImage. This will come into play later.
That's the loading done, so now we can clean up the resources we've used, by calling cJSON_Delete and free, to destroy the loaded JSON and text data.
Our atlas data is loaded! What happens when we want to fetch a sprite, though? Well, grabbing an AtlasImage by filename is easy. It simple involves specifying the filename, determining the hashmap bucket it belongs to, and then searching the entries for a match. Our getAtlasImage function does exactly that:
A very simply function to understand. You'll notice we're taking the extreme measure of exiting if we're unable to find the image we're after. You may or may not want that in your own code, as you could well have a situation where the AtlasImage is optional for what you're doing (for example, loading a tileset where some entries might not exist). In such a case, an extra parameter to say the image is required and then testing the logic would fix this for you.
It's finally time to draw a scene using our sprite atlas. demo.c contains all the code for setting up and drawing such a scene, so let's take a look at that next.
The initDemo function essentially sets up some spheres, blocks, and tiles for our scene, calling upon the getAtlasImage function to grab the appropriate image to use:
We won't talk any more about this function, as it's not the focus of this tutorial; it's really just setting up a random scene out of spheres, blocks, and some tiles. It's the drawing we want to focus on. The draw function draws our tiles, blocks, and spheres:
The drawSpheres function is easy to follow:
Looping through all our spheres, we call upon a new function: blitAtlasImage. If you've worked through previous tutorials, you'll be familiar with the blit functions there. This one is a little different, but not greatly so. In fact, it's only 17 lines long. This is the main function that deals with the rendering of an AtlasImage:
The function accepts an AtlasImage, the destination x and y coordinates, and a flag to say whether it should be centered. The first thing we do is prepare an SDL_Rect (dest) that will be used to specify the destination area to render into. dest's x and y are set to the x and y we passed into the function, while the w and h (the width and height) are set to the AtlasImage's rect's w and h, ensuring that the sprite at the destination is drawn the same size as the original.
We test if we want to center to image, and if so, we shift the dest's x and y by half the dest's width and height. Nothing special there.
Finally, we call the standard SDL_RenderCopy function, to draw the sprite. Notice that the source texture is the texture member of the AtlasImage. We set this earlier when loading the atlas data. The reason for this is to give us some flexibility, in case we have more than one atlas image (which isn't beyond the realms of possibility, if you have lots and lots of images). The source rectangle is given as the AtlasImage's rect (&atlasImage->rect) and the destination rect as the dest rect we set up earlier.
Essentially, we are telling SDL to copy one rectangular area into another rectangular area, using the AtlasImage's coordinates in the atlas. Very easy!
The draw functions for the tiles and blocks use the same blitAtlasImage function, the only difference being that they do not centre their images.
That's it for loading and blitting the AtlasImages. One quick word on SDL's batch rendering support. It is controlled by the hint SDL_HINT_RENDER_BATCHING, and is enabled by default (there is actually no need to specify "1" in the line below):
Keep in mind that this behaviour was added into SDL 2.0.10, and is not available in prior releases. Even so, creating a sprite atlas to deal with batch rendering is all in all a grand idea.
The source code for all parts of this tutorial (including assets) is available here:
It is also available as part of the SDL2 tutorial bundle (with on-going updates):
If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.
Share your comments and thoughts below. All comments are anonymous and cannot be edited.