Part 5: The Texture Manager
Drawing Images and creating a class to store and retrieve them.
What would a 2D game be without images right? Almost impossible (someone probably found a way) so to handle storing, retrieving and drawing images we’re best of creating something called a Texture Manager (or you can call it whatever). The Texture Manager will have the ability to store images, retrieve them and draw them to the screen using our previously created renderer in the Game class.
First things first to create the Texture Manager we need to create a new class to build it with. So go ahead and create a new class called TextureManager (look back over the tutorials to find out how to create a class if you can’t remember). Once created, open the file named TextureManager.h and strip it to its bare bones, then we can set it up like so:
First things first to create the Texture Manager we need to create a new class to build it with. So go ahead and create a new class called TextureManager (look back over the tutorials to find out how to create a class if you can’t remember). Once created, open the file named TextureManager.h and strip it to its bare bones, then we can set it up like so:
#pragma once
class TextureManager { public: private: }; |
Now we have our basic header file we can go ahead and start setting it up, first things first we’re going to start with our includes at the top between #pragma once and the actual class definition. The includes we’re going to use are as follows, so go ahead and add them:
#include <string>
#include <map> #include <SDL.h> #include <SDL_image.h> |
We include <string> because we’re using string variables as IDs for our textures. <map> is the array type we’re going to use to store our textures as you can retrieve an object in the map using any variable as a unique identifier (in our case, string). The inclusion of <SDL.h> is to cover any SDL functions we may use and the final inclusion of <SDL_image.h> is our first time using the library (which we set up in the first tutorial part if you recall) and is used to give us the ability to use images types other than .bmp (like .png so you can have alpha transparency in your images, incredibly useful).
So now we have included everything we need so we can move onto setting up our private functions and attributes in the header file. The only things we’ll need privatising are the constructor, destructor and a static instance of TextureManager as we will be making this a singleton (refer to previous parts for the definition of a singleton). Your private section should look like this after adding them:
So now we have included everything we need so we can move onto setting up our private functions and attributes in the header file. The only things we’ll need privatising are the constructor, destructor and a static instance of TextureManager as we will be making this a singleton (refer to previous parts for the definition of a singleton). Your private section should look like this after adding them:
private:
static TextureManager* instance; TextureManager(){} ~TextureManager(){} }; |
Now we can go ahead and start on our public section, first off let’s get our singleton Instance getter out of the way:
public:
static TextureManager* Instance() { if (instance == 0) { instance = new TextureManager(); } return instance; } |
Also add the following line to the bottom of the TextureManager.h file below the class definition (after eeeverything).
typedef TextureManager _TextureManager;
|
We’ve covered singleton’s before so if you don’t know what’s going on here look back over previous tutorials where I explain singletons (the game loop tutorial, part 3). Here however I’m going to move onto setting up our public functions and attributes. Here’s what it’ll look like when you’ve set them up, and I’ll explain once you’ve looked it over.
bool load(std::string fileName, std::string id, SDL_Renderer* renderer);
void draw(std::string id, int x, int y, int w, int h, double scale, double r, SDL_Renderer* renderer, SDL_RendererFlip flip = SDL_FLIP_NONE); void drawFrame(std::string id, int x, int y, int w, int h, double scale, int currentRow, int currentFrame, double r, SDL_Renderer* renderer, SDL_RendererFlip flip = SDL_FLIP_NONE); void clearFromTextureMap(std::string id); std::map<std::string, SDL_Texture*> textureMap; |
So we have 4 functions here and a variable at the bottom so let’s get to explaining, I’ll start with the last of the aforementioned content.
The variable named “textureMap” is of type map which in C++ is a type of array which doesn’t store its data in numbered slots (i.e. 0, 1, 2 and 3) like a vector, but instead uses a unique identifier of a type chosen by the programmer. Have a look inside these symbols “< >” and you’ll see two variable types; a string variable and an SDL_Texture pointer (pointer denoted by the *). We haven’t seen SDL_Texture before but it is part of the SDL library and stores an image which we can draw to the screen using our SDL_Renderer in the Game class. The first variable, the string variable, is the unique identifier variable type definition (in other words, we’ll be giving string IDs to our textures to call them with).
Now, the first function “load” is the function used to load a texture into memory and store it in the “textureMap” map array. It takes the fileName (or file path, originating in the folder where all your source code files are e.g. Main.cpp and Game.h), a string variable named ID with which to call the texture to use (for example, when drawing it) and a pointer variable to a renderer with which to get the image data.
The next two are almost the same so I will first explain the function “draw”. It takes in a string variable named id which pertains to the texture you wish to draw (as created by the function load and stored in textureMap). The next 4 variables it takes in its parameters is the x and y position you wish to draw the image at and the width and height you want to stretch the image to (we can use the texture to get its width and height to keep the original size without hard coding it). We also have a variable for scale, which we will mostly set manually to be 1 so it draws the image in original scale. The next variable “r” is the rotation factor in degrees which the image will be drawn with. We then pass in the SDL_Renderer to draw with and the option of flipping the image horizontally or vertically, this value as you can see is set to 0 in the parameters itself; this means that we can leave this variable out at the end, not even acknowledging it and it will take the default value of 0.
In the next function, “drawFrame” we take the parameters in the “draw” function and add a couple more; “currentRow” and “currentFrame”. These two parameters pertain to positions in a sprite sheet, the method by which we’ll be accepting animated graphics in our framework we’re building here.
The final function is purely for clean-up and will be used when finished with certain textures to free up the memory to use for something else.
So now we have our TextureManager.h set up which should look something like this:
The variable named “textureMap” is of type map which in C++ is a type of array which doesn’t store its data in numbered slots (i.e. 0, 1, 2 and 3) like a vector, but instead uses a unique identifier of a type chosen by the programmer. Have a look inside these symbols “< >” and you’ll see two variable types; a string variable and an SDL_Texture pointer (pointer denoted by the *). We haven’t seen SDL_Texture before but it is part of the SDL library and stores an image which we can draw to the screen using our SDL_Renderer in the Game class. The first variable, the string variable, is the unique identifier variable type definition (in other words, we’ll be giving string IDs to our textures to call them with).
Now, the first function “load” is the function used to load a texture into memory and store it in the “textureMap” map array. It takes the fileName (or file path, originating in the folder where all your source code files are e.g. Main.cpp and Game.h), a string variable named ID with which to call the texture to use (for example, when drawing it) and a pointer variable to a renderer with which to get the image data.
The next two are almost the same so I will first explain the function “draw”. It takes in a string variable named id which pertains to the texture you wish to draw (as created by the function load and stored in textureMap). The next 4 variables it takes in its parameters is the x and y position you wish to draw the image at and the width and height you want to stretch the image to (we can use the texture to get its width and height to keep the original size without hard coding it). We also have a variable for scale, which we will mostly set manually to be 1 so it draws the image in original scale. The next variable “r” is the rotation factor in degrees which the image will be drawn with. We then pass in the SDL_Renderer to draw with and the option of flipping the image horizontally or vertically, this value as you can see is set to 0 in the parameters itself; this means that we can leave this variable out at the end, not even acknowledging it and it will take the default value of 0.
In the next function, “drawFrame” we take the parameters in the “draw” function and add a couple more; “currentRow” and “currentFrame”. These two parameters pertain to positions in a sprite sheet, the method by which we’ll be accepting animated graphics in our framework we’re building here.
The final function is purely for clean-up and will be used when finished with certain textures to free up the memory to use for something else.
So now we have our TextureManager.h set up which should look something like this:
#pragma once
#include <string> #include <map> #include "SDL.h" #include "SDL_image.h" class TextureManager { public: static TextureManager* Instance() { if (instance == 0) { instance = new TextureManager(); } return instance; } bool load(std::string fileName, std::string id, SDL_Renderer* renderer); void draw(std::string id, int x, int y, int w, int h, double scale, double r, SDL_Renderer* renderer, SDL_RendererFlip flip = SDL_FLIP_NONE); void drawFrame(std::string id, int x, int y, int w, int h, double scale, int currentRow, int currentFrame, double r, SDL_Renderer* renderer, SDL_RendererFlip flip = SDL_FLIP_NONE); void clearFromTextureMap(std::string id); std::map<std::string, SDL_Texture*> textureMap; private: static TextureManager* instance; TextureManager(){} ~TextureManager(){} }; typedef TextureManager _TextureManager; |
We can now move onto our TextureManager.cpp file to define these functions and get some function going in our TextureManager. First things first we need to empty out the TextureManager.cpp file and add the following 2 lines:
#include "TextureManager.h"
#include <iostream> TextureManager* TextureManager::instance = 0; |
Once we’ve done that, our Texture Manager is now setup to be a singleton (once again, we’ve covered this previously, most notably in the game loop tutorial (part 3)). Also added “iostream” functionality, so we can print to the console.
We can now go ahead and start defining those functions. Let’s start with the load function and start creating the system which we can load our images into the texture map with. Add the following function to the TextureManager.cpp file below the 3 lines we have there already:
We can now go ahead and start defining those functions. Let’s start with the load function and start creating the system which we can load our images into the texture map with. Add the following function to the TextureManager.cpp file below the 3 lines we have there already:
bool TextureManager::load(std::string fileName, std::string id, SDL_Renderer* renderer)
{ SDL_Surface* tempSurf = IMG_Load(fileName.c_str()); if (tempSurf == 0) { std::cout << "could not load image named: '" << fileName.c_str() << "'!!!" << std::endl; return false; } SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, tempSurf); SDL_FreeSurface(tempSurf); if (texture != 0) { std::cout << "Texture loaded successfully... ('" << fileName.c_str() << "')" << std::endl; textureMap[id] = texture; return true; } std::cout << "Could not create texture from surface!!! ('" << fileName.c_str() << "')" << std::endl; return false; } |
Now to explain this function; we went through the parameters in the header file so let’s move onto to the statements inside the function. Line 1 creates an SDL_Surface pointer which is called “tempSurf” here quite aptly because it will indeed be temporarily used. We need to use the SDL_Surface to get the image data before we can then create the draw able texture from the surface later on. To store the image data we assign a value to the temporary variable created using the SDL_image function, “IMG_Load” which takes a different form of “string” variable so to speak, but we can convert our string variable we passed in the parameters, “fileName”, using the method “c_str()” included in the strings capabilities. Once we’ve done this we check to make sure that the function found a compatible image by using an if statement. The if statement checks if the surface is equal to nothing, and if so then it cannot find the image file (faulty path files or incompatible files can cause this error) and exits the function using a return statement of false which cancels everything out and doesn’t load any texture. We also have the line in there printing to the console that it cannot find the file, and prints the fileName also so we can see if we went wrong there.
This if statement is obviously ignored if we successfully load a file into the temporary surface file and we can move onto the next line which creates the texture itself, the draw able texture we will store in our Texture Manager instead of the temporary surface pointer. We do this using the SDL function SDL_CreateTextureFromSurface which takes the renderer and the surface as its parameters. We can then go ahead and free up the memory used by the temporary surface using the function SDL_FreeSurface, passing in the temporary surface variable.
Once we’ve done that we then perform a second “NULL check” with an if statement. We check to see if the texture is equal to anything (the image was found and read correctly and is compatible) and if se we print to the console to tell us that it was indeed successful and then we add the texture to our texture map by calling a place in the map using the syntax “textureMap[id]”; if existent already, this retrieves the texture at the point in the map with the unique identifier which we passed into the parameters and we can edit it then, if not it creates a point in the map with that unique identifier and gives us the opportunity to assign it a texture which we do with “= texture;”. We can then return true as the texture is loaded and everything is okay. If the texture is faulty and hasn’t loaded properly, this if statement isn’t run and the return statement of true is never reached so it progresses onto a console print out which tells us the image did not load correctly and then returns a false statement so we know something went wrong.
Now we’ve finished with that function we can now load images into our texture map, but we’ll get onto testing everything once we’re completely done so we can draw the test textures too when we come to it. For now let’s move onto our next function, “draw”; add this below the load function we just did:
This if statement is obviously ignored if we successfully load a file into the temporary surface file and we can move onto the next line which creates the texture itself, the draw able texture we will store in our Texture Manager instead of the temporary surface pointer. We do this using the SDL function SDL_CreateTextureFromSurface which takes the renderer and the surface as its parameters. We can then go ahead and free up the memory used by the temporary surface using the function SDL_FreeSurface, passing in the temporary surface variable.
Once we’ve done that we then perform a second “NULL check” with an if statement. We check to see if the texture is equal to anything (the image was found and read correctly and is compatible) and if se we print to the console to tell us that it was indeed successful and then we add the texture to our texture map by calling a place in the map using the syntax “textureMap[id]”; if existent already, this retrieves the texture at the point in the map with the unique identifier which we passed into the parameters and we can edit it then, if not it creates a point in the map with that unique identifier and gives us the opportunity to assign it a texture which we do with “= texture;”. We can then return true as the texture is loaded and everything is okay. If the texture is faulty and hasn’t loaded properly, this if statement isn’t run and the return statement of true is never reached so it progresses onto a console print out which tells us the image did not load correctly and then returns a false statement so we know something went wrong.
Now we’ve finished with that function we can now load images into our texture map, but we’ll get onto testing everything once we’re completely done so we can draw the test textures too when we come to it. For now let’s move onto our next function, “draw”; add this below the load function we just did:
void TextureManager::draw(std::string id, int x, int y, int w, int h, double scale, double r, SDL_Renderer* renderer,
SDL_RendererFlip flip) { SDL_Rect srcRect, destRect; srcRect.x = 0; srcRect.y = 0; srcRect.w = destRect.w = w; srcRect.h = destRect.h = h; destRect.x = x; destRect.y = y; destRect.w *= scale; destRect.h *= scale; SDL_RenderCopyEx(renderer, textureMap[id], &srcRect, &destRect, r, NULL, flip); } |
When we draw an image, we need two rectangles with which to draw the texture. The first is a source rectangle, this defines the area of the texture which we want to draw, this can be a small part of the image or the same size or larger, completely up to the developer. For now in our function we’re going to make it the same size as the original image, at least for the “draw” function (more on the “drawFrame” function later). We also need the destination rectangle, which is used to tell the program where in the game you want to draw the image. So the first thing we do is create those rectangles, or SDL_Rect objects.
Once created we need to initialise their attributes; their x, y, width and height values. You could make them different and do it manually, but in general it’s going to be fairly simple like above. First off we want to setup the source rectangle (or srcRect); we set the x and y of srcRect to 0, meaning rectangle used to select what part of the image want to draw starts in the top left of the image. We then set the width and height (w and h) to the width and height variables passed in as parameters. We also want the destination rectangle (or destRect) to have the same width so we can set the srcRect.w equal to the destRect.w which in turn we set equal to the w passed in as a parameter. We can do the same but for the height (h). So we have set up the srcRect completely and the width and height of the destRect. Now for the x and y of the destRect we simply want to set it equal to the width and height passed into the function via the parameters. So very simply we use destRect.x = x; and vice versa for the y value. Now we also have our scaling to do with, so we then multiply the width and height of the destRect by the scale value passed in so as not to affect the srcRect (so we scale the actual image rather than taking a chunk of the image twice the size of the image itself, which could give us either 4 of the image in a grid or just the image in the corner). Once we’ve set up the rectangles we can call the SDL function SDL_RenderCopyEx which takes in it’s parameters the renderer we passed in, the position in the texture map at the unique identifier passed into the parameters, a pointer to the srcRect and a pointer to the destRect, the rotation factor, the point around which we want to rotate (we set this to NULL because when done like so the default point is the center of the image, you can change this to a parameter if you wish) and finally the flip flag at the end which we can ignore if we don’t wish to (just as a side note, this optional flip variable is very good for directional sprites for a player. Instead of having more than one image for left and right, you can have one left facing sprite and then flip it horizontally using this functionality saving memory and time).
The next function is almost exactly the same, so add this code below the draw function:
Once created we need to initialise their attributes; their x, y, width and height values. You could make them different and do it manually, but in general it’s going to be fairly simple like above. First off we want to setup the source rectangle (or srcRect); we set the x and y of srcRect to 0, meaning rectangle used to select what part of the image want to draw starts in the top left of the image. We then set the width and height (w and h) to the width and height variables passed in as parameters. We also want the destination rectangle (or destRect) to have the same width so we can set the srcRect.w equal to the destRect.w which in turn we set equal to the w passed in as a parameter. We can do the same but for the height (h). So we have set up the srcRect completely and the width and height of the destRect. Now for the x and y of the destRect we simply want to set it equal to the width and height passed into the function via the parameters. So very simply we use destRect.x = x; and vice versa for the y value. Now we also have our scaling to do with, so we then multiply the width and height of the destRect by the scale value passed in so as not to affect the srcRect (so we scale the actual image rather than taking a chunk of the image twice the size of the image itself, which could give us either 4 of the image in a grid or just the image in the corner). Once we’ve set up the rectangles we can call the SDL function SDL_RenderCopyEx which takes in it’s parameters the renderer we passed in, the position in the texture map at the unique identifier passed into the parameters, a pointer to the srcRect and a pointer to the destRect, the rotation factor, the point around which we want to rotate (we set this to NULL because when done like so the default point is the center of the image, you can change this to a parameter if you wish) and finally the flip flag at the end which we can ignore if we don’t wish to (just as a side note, this optional flip variable is very good for directional sprites for a player. Instead of having more than one image for left and right, you can have one left facing sprite and then flip it horizontally using this functionality saving memory and time).
The next function is almost exactly the same, so add this code below the draw function:
void TextureManager::drawFrame(std::string id, int x, int y, int w, int h, double scale, int currentRow,
int currentFrame, double r, SDL_Renderer* renderer, SDL_RendererFlip flip) { SDL_Rect srcRect, destRect; srcRect.x = w * currentFrame; srcRect.y = h * currentRow; srcRect.w = destRect.w = w; srcRect.h = destRect.h = h; destRect.x = x; destRect.y = y; destRect.w *= scale; destRect.h *= scale; SDL_RenderCopyEx(renderer, textureMap[id], &srcRect, &destRect, r, NULL, flip); } |
You can see the similarities but let’s address the differences (besides the obvious like the function name and the extra parameters). The function is called drawFrame because we use this function to draw a frame of a sprite sheet (for animation purposes mainly); knowing this should go a long way to understanding much of what I’m about to explain about the difference of this function to the previous. The first of which is the x and y of the srcRect; previously we set these to 0, but because we’re taking a frame of the image file, like a slice, we need to tell the function where to start cutting said slice out. So the two new parameters, currentRow and currentFrame come into play here; we take the width parameter (this is the width of the frame, not the image file) and multiply it by the currentFrame to get the x position which we want to start drawing from. Then for the y value we take the height parameter (again for the frame, not the image file) and multiply that by the currentRow variable to get the starting point for the srcRect of the frame. Let’s consider an example to better understand this – if we want to take the first frame of a sprite sheet then currentFrame and currentRow would both likely be “0” and so it would start selecting the srcRect at the point x = 0, y= 0. The rest of the initialisation for the rectangles are the same, all we change for drawing the frame is the starting point of the srcRect. The actual function to draw it to the screen is also the same, the SDL_RenderCopyEx function that is.
Now with those two done we move onto our final function, the clearFromTextureMap function. So go ahead and at that function below the other 3:
Now with those two done we move onto our final function, the clearFromTextureMap function. So go ahead and at that function below the other 3:
void TextureManager::clearFromTextureMap(std::string id)
{ textureMap.erase(id); } |
This is a very simple function, you pass in the id of the texture you wish to clear from the map and then call the “erase” function of the map array, passing is that parameter and it clears it from the texture map freeing up memory.
So now that we’ve finished setting up our Texture Manager class (yaaaay), we can test it out! Go into the Game.cpp file and add “#include “TextureManager.h”” to the top of the file with the other includes and we can start to test out our Texture Manager.
To test we’ll need a place to store our images, so go into the folder housing all your C++ and header files (if you haven’t tampered with the default project folder structure of Visual Studio then it should be somewhere like “sdl-game-practice/sdl-game-practice”, you’ll see all your files alongside a folder named “debug”). In here, create a new folder and call it “assets”; the download this testing image -> here <- and place it in there.
Now, back in our Game.cpp file we can firstly load this image. So we want to do our initialisation for now after the running = true statement in Game’s init function but before the final return statement. We add the following line here to load an image:
So now that we’ve finished setting up our Texture Manager class (yaaaay), we can test it out! Go into the Game.cpp file and add “#include “TextureManager.h”” to the top of the file with the other includes and we can start to test out our Texture Manager.
To test we’ll need a place to store our images, so go into the folder housing all your C++ and header files (if you haven’t tampered with the default project folder structure of Visual Studio then it should be somewhere like “sdl-game-practice/sdl-game-practice”, you’ll see all your files alongside a folder named “debug”). In here, create a new folder and call it “assets”; the download this testing image -> here <- and place it in there.
Now, back in our Game.cpp file we can firstly load this image. So we want to do our initialisation for now after the running = true statement in Game’s init function but before the final return statement. We add the following line here to load an image:
_TextureManager::Instance()->load("assets/test.png", "test", _Game::Instance()->getRenderer());
|
We use the singleton typedef “_TextureManager” to get the instance and then call the load function. For the parameters, if you recall, we pass in the file name (or path) to the image including the extension (.png in this case). Then we pass in a string for the unique identifier of the image to call it with, in this case “test”. Finally we pass in the renderer belonging to our singleton game class (this is the one and only renderer we will ever use in the tutorial).
This should successfully load the image into the texture map if you have followed the tutorial correctly, so now we can try and draw the image. Over in the render function of the Game.cpp file inbetween the two pre existing functions, we can call the draw function of our Texture Manager singleton instance:
This should successfully load the image into the texture map if you have followed the tutorial correctly, so now we can try and draw the image. Over in the render function of the Game.cpp file inbetween the two pre existing functions, we can call the draw function of our Texture Manager singleton instance:
_TextureManager::Instance()->draw("test", 100, 100, 128, 128, 1, 0, _Game::Instance()->getRenderer());
|
If you recall the parameters for the draw function in our Texture Manager you can see we first pass in the id of the texture we wish to draw. Then we give it an x and y value for where we want the image drawn on screen. Then we pass in the width and the height of the image, here I have put 128 for both because the test image is 128 pixels width and height. We then pass a value for the scale (you can mess around with this value if you wish, making it smaller with 0.5 or bigger with 2 or 3) which we’re going to leave as 1 for the original image size. The rotation factor we’re going to pass as “0” (again, feel free to mess around with this value), just keeping it simple for now. Finally we pass in the renderer from our Game class’ singleton instance. With that done we can now perform our first test. Run the program and you should get something that looks like this:
We can test the drawFrame function now too, using the same image we’re just going to call the 1 frame from this image and it will be the whole image. This is just to test if it works, we will later go over actual animation using spritesheets in another part of the tutorial; add this line below the draw function we called in the render function:
_TextureManager::Instance()->drawFrame("test", 300, 300, 128, 128, 1, 0, 0, 0,
_Game::Instance()->getRenderer()); |
When calling the drawFrame function, all we need to change is to pass in the currentRow and currentFrame variables (in that order). Seen as we just want the same image as before we’ll set these to 0 and 0 as you can see in the above code excerpt; if you see in the Texture Manager, these two variables go after scale and before rotation. Now with these two lines, let’s run it again and you should get something like this:
Now that we know the images load correctly and they draw correctly, let’s tidy up.
In the Game class’ clean function add the following line before the three pre existing SDL functions like so:
In the Game class’ clean function add the following line before the three pre existing SDL functions like so:
_TextureManager::Instance()->clearFromTextureMap("test");
// Pre existing SDL functions // SDL_DestroyWindow(window); SDL_DestroyRenderer(renderer); SDL_Quit(); |
That’ll remove the textures from memory when we exit this game. So, we’re all done and dusted with the texture manager now. In the next tutorial I’m going to show you how to make a finite state machine to handle multiple game states such as menus and levels.