In SDL games there’s a lot of image loading and blitting going on every frame, and a lot of frames going on every second (even if I’ve capped my games at 60fps that’s still 3600 frames a minute) so there needs to be some way to ensure that there isn’t endless loading and reloading of game attributes. In a game like Breakout (a clone of which I’ve made and will have on here at some point this bank holiday weekend) there is also a lot of elements used multiple times (bricks being the main one). Thus there is also a need to reduce memory usage so that the same image isn’t loaded into memory every time it is needed, which also complicates things when we come to the freeing of surfaces when they are deleted (calling SDL_FreeSurface() endless times in a program looks (a) ugly and (b) confusing).
These issues converged and lead to a bit of research on the matter. I believe it was a one off general google search for SDL_FreeSurface() that lead me to this post on StackOverflow and more importantly, the first answer. This introduced the idea of an ImageCache that would do all of the loading and destruction of images for me. Obviously viewing this as a massive boon, I shamelessly took the code from that answer and set about creating my own version. As of today, endless refactoring has lead me to this class interface, which is what I’m using in my current Bomber Run project:
class ImageCache{
private:
SDL_Surface* screen = nullptr;
const int screenWidth, screenHeight;
std::unordered_map<int, TTF_Font*> fonts;
std::unordered_map<std::string, SDL_Surface*> images;
SDL_Surface* loadIMG(const std::string filename) const;
SDL_Surface* loadText(TTF_Font* font, const char* text, const SDL_Color color) const;
TTF_Font* loadFont(const int size) const;
void blitSurface(SDL_Surface* image, SDL_Rect* position) const;
public:
ImageCache(int screenWidth, int screenHeight);
~ImageCache();
void clearScreen(const SDL_Color color = {255, 255, 255});
void drawRectangle(const SDL_Rect rect, const SDL_Color);
void updateScreen() const;
void flush();
void freeSurface(const std::string text);
void freeSurface(const std::string text, int amount);
TTF_Font* getFont(const int size);
SDL_Surface* getImage(const std::string file);
SDL_Surface* getText(TTF_Font* font, const std::string text, const SDL_Color color = {192,0,0});
SDL_Surface* getText(TTF_Font* font, const std::string text, const int amount, const SDL_Color color = {192,0,0});
SDL_Surface* getScreen() const { return screen; }
void render(SDL_Surface* theImage, const int x, const int y);
void render(const std::string imgFile, const Point2D& location);
const int centreX(SDL_Surface* surface) const;
const int centreY(SDL_Surface* surface) const;
};
The public interface is designed to be as cut-down as possible, and the functions are designed to do as much of the work as possible. For example, the getText() functions – there are two because I want the functions to take something like getText(font, “Lives “, 3) and put it together for the user using a stringstream, taking away the need for the user to do it themselves each time (remember programming 101: DRY – don’t repeat yourself).
The ImageCache constructor sets up the game window for the user
ImageCache::ImageCache(int width, int height) : screenWidth(width), screenHeight(height) {
try {
SDL_putenv("SDL_VIDEO_CENTERED=center"); //centres the window on the user's screen
if(SDL_Init(SDL_INIT_VIDEO) == -1) //initializes SDL
throw(SDL_GetError());
if(TTF_Init() == -1) //initializes SDL_ttf
throw(TTF_GetError());
SDL_WM_SetIcon(SDL_LoadBMP("images/icon.bmp"), NULL); //load and set the window icon
screen = SDL_SetVideoMode(screenWidth, screenHeight, 32, SDL_SWSURFACE); //set up the main screen surface
if(!screen)
throw(SDL_GetError());
SDL_WM_SetCaption("Bomber Run", NULL); //set the window title
clearScreen(); //paint the screen the background colour
updateScreen(); //calls SDL_Flip() to show the updated screen to the user
}
catch(const char* e) {
printf("Failed to create ImgCache: %s", e); //if any errors are thrown, catch them and output them stdout.txt in the bin folder
exit(101); //then exit the program
}
}
There is one line in there that needs to be changed for each project, the SDL_WM_SetCaption("Bomber Run", NULL) line, which on further inspection could become SDL_WM_SetCaption(gameTitle, NULL), with std::string gameTitle added as a parameter to the constructor.
The ImageCache() destructor calls one method to free all the surfaces and fonts – the flush() method:
void ImageCache::flush() {
for(auto i = images.begin(); i != images.end(); ++i) { //use the auto keyword from C++11
SDL_FreeSurface(i->second); //free the surface
i->second = nullptr; //set the pointer to nullptr, also from C++11
}
images.clear();
for(auto i = fonts.begin(); i != fonts.end(); ++i) {
TTF_CloseFont(i->second);
i->second = nullptr;
}
fonts.clear();
}
The loadImage()/loadFont()/loadText() functions are pretty basic – the ideas are mostly borrowed from Lazy Foo, but rewritten to fit in with my code. The getImage()/getText()/getFont() methods are all incredibly similar, and borrowed almost straight from the StackOverflow question above.
//load the font, if null throw an error, else return the font
TTF_Font* ImageCache::loadFont(const int size) const {
TTF_Font* font = TTF_OpenFont("fonts/saucerbb.ttf", size);
if(!font)
throw(TTF_GetError());
else
return font;
}
//look for the font in the map
//if not there, load it up and insert it into the map
TTF_Font* ImageCache::getFont(const int size) {
auto i = fonts.find(size);
if(i == fonts.end()) {
try {
TTF_Font* font = loadFont(size);
i = fonts.insert(i, std::make_pair(size, font));
}
catch(const char* e) {
printf("%s", e);
exit(70);
}
}
return i->second;
}
The fonts are held by their size, which does mean that I can only have one font throughout the game, but this constraint leads to a more refined and consistent game design in my eyes. The maps are unordered_maps because there is a slight speed increase from the use of a hash table as a sorting method (which I’ll be honest is probably non-existent with the number of game elements I’m using – if I was using thousands of elements for whatever reason, this would be a preferable design choice) and the order of the map isn’t important in any way.
The render functions are shown below, relatively basic but very easy to use:
void ImageCache::blitSurface(SDL_Surface* image, SDL_Rect* position) const {
try {
if(SDL_BlitSurface(image, NULL, screen, position) == -1)
throw(SDL_GetError());
}
catch(const char* e) {
printf("%s", e);
exit(74);
}
}
//Draw theImage on theScreen at (x,y)
void ImageCache::render(SDL_Surface* theImage, const int x, const int y) {
SDL_Rect position;
position.x = x;
position.y = y;
blitSurface(theImage, &position);
}
SDL_BlitSurface is hidden in it’s own function to reduce the number of try/catch loops (as there are 2 render functions). The use of two render functions is to make calling them easier in the game code. Each game object that is drawn to the screen has a Point2D position member variable along with a Point2D getPosition() member function that can be used to position them on the screen. However for the title screen and any text, they don’t have a Point2D position, they’re just drawn on the screen at x and y positions. These could be declared inside curly brackets {}, but this would get confusing and easy to make an error with, so I wrote a render() function with an x and y input too.
Finally, the centreX() and centreY() functions, the two simplest functions in the whole class. These are mainly used to position text on the screen on the menu screens with the minimum of fuss – pass in the surface to be centred and the function will return the integer point that will centre it on the screen.
//centre a surface horizontally
const int ImageCache::centreX(SDL_Surface* surface) const {
return int((screenWidth - surface->w) / 2);
}
//centre a surface vertically
const int ImageCache::centreY(SDL_Surface* surface) const {
return int((screenHeight - surface->h) / 2);
}
Lovely. This has to be one of my proudest classes – it has no coupling to any game objects at all, so is completely independent between games and helps me get images on the screen in no time at all on a new project. It is used extensively in all my projects and is part of my Blank Game project, a basic outline of classes that I have set up that give me the basic functionality of a game and allow me to build the basics of a game with the least hassle. The Blank Game includes Game, ImageCache, StateManager, FSM, CollisionEngine, Title, Level, GameOver and HighScore.