Introduction
This article steps you through the creation of a basic game framework using SDL and C++. The end result is quite simple, but it provides a solid foundation to build more functional visual applications and games.
Background
This code was used as the basis for a shoot'em'up created for an SDL game programming competition.
Using the code
We will start with the EngineManager
class. This class will be responsible for initialising SDL, maintaining the objects that will make up the game, and distributing events.
EngineManager.h
#ifndef _ENGINEMANAGER_H__
#define _ENGINEMANAGER_H__
#include <sdl.h>
#include <list>
#define ENGINEMANAGER EngineManager::Instance()
class BaseObject;
typedef std::list<baseobject*> BaseObjectList;
class EngineManager
{
public:
~EngineManager();
static EngineManager& Instance()
{
static EngineManager instance;
return instance;
}
bool Startup();
void Shutdown();
void Stop() {running = false;}
void AddBaseObject(BaseObject* object);
void RemoveBaseObject(BaseObject* object);
protected:
EngineManager();
void AddBaseObjects();
void RemoveBaseObjects();
bool running;
SDL_Surface* surface;
BaseObjectList baseObjects;
BaseObjectList addedBaseObjects;
BaseObjectList removedBaseObjects;
Uint32 lastFrame;
};
#endif
EngineManager.cpp
#include "EngineManager.h"
#include "ApplicationManager.h"
#include "BaseObject.h"
#include "Constants.h"
#include <boost/foreach.hpp>
EngineManager::EngineManager() :
running(true),
surface(NULL)
{
}
EngineManager::~EngineManager()
{
}
bool EngineManager::Startup()
{
if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
return false;
if((surface = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, BITS_PER_PIXEL,
SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)) == NULL)
return false;
APPLICATIONMANAGER.Startup();
lastFrame = SDL_GetTicks();
while (running)
{
SDL_Event sdlEvent;
while(SDL_PollEvent(&sdlEvent))
{
if (sdlEvent.type == SDL_QUIT)
running = false;
}
AddBaseObjects();
RemoveBaseObjects();
Uint32 thisFrame = SDL_GetTicks();
float dt = (thisFrame - lastFrame) / 1000.0f;
lastFrame = thisFrame;
BOOST_FOREACH (BaseObject* object, baseObjects)
object->EnterFrame(dt);
SDL_Rect clearRect;
clearRect.x = 0;
clearRect.y = 0;
clearRect.w = SCREEN_WIDTH;
clearRect.h = SCREEN_HEIGHT;
SDL_FillRect(surface, &clearRect, 0);
BOOST_FOREACH (BaseObject* object, baseObjects)
object->Draw(this->surface);
SDL_Flip(surface);
}
return true;
}
void EngineManager::Shutdown()
{
APPLICATIONMANAGER.Shutdown();
surface = NULL;
SDL_Quit();
}
void EngineManager::AddBaseObject(BaseObject* object)
{
addedBaseObjects.push_back(object);
}
void EngineManager::RemoveBaseObject(BaseObject* object)
{
removedBaseObjects.push_back(object);
}
void EngineManager::AddBaseObjects()
{
BOOST_FOREACH (BaseObject* object, addedBaseObjects)
baseObjects.push_back(object);
addedBaseObjects.clear();
}
void EngineManager::RemoveBaseObjects()
{
BOOST_FOREACH (BaseObject* object, removedBaseObjects)
baseObjects.remove(object);
removedBaseObjects.clear();
}
The first thing we need to do is call SDL_Init
. This loads the SDL library, and initialises any of the subsystems that we specify. In this case, we have specified that everything be initialised by supplying the SDL_INIT_EVERYTHING
flag. You could choose to initialise only the subsystems you need (audio, video, input etc.), but since we will be making use of most of these subsystems as the game progresses, initialising everything now saves some time.
if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
return false;
If SDL and its subsystems were loaded and initialised correctly, we then create a window. The SCREEN_WIDTH
, SCREEN_HEIGHT
, and BITS_PER_PIXEL
define the size of the window and the colour depth. These values are defined in the Constants.h file. The next parameter is a collection of options that further specify how the window is to work.
The SDL_HWSURFACE
option tells SDL to place the video surface in video memory (i.e., the memory on your video card). Most systems have dedicated video cards with plenty of memory, and certainly enough to hold our 2D game.
The SDL_DOUBLEBUF
option tells SDL to set up two video surfaces, and to swap between the two with a call to SDL_Flip()
. This stops the visual tearing that can be caused when the monitor is refreshing while the video memory is being written to. It is slower than a single buffered rendering scheme, but again, most systems are fast enough for this not to make any difference to the performance.
The SDL_ANYFORMAT
option tells SDL that if it can’t set up a window with the requested colour depth, that it is free to use the best colour depth available to it. We have requested a 32 bit colour depth, but some desktops may only be running at 16 bit. This means that our application won’t fail just because the desktop is not set to 32 bit colour depth.
if((surface = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, BITS_PER_PIXEL,
SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)) == NULL)
return false;
The ApplicationManager
is then started up. The ApplicationManager
holds the logic that defines how the application is run. It is kept separate from the EngineManager
to separate the code required to initialise and manage SDL from the code required to manage the application itself.
APPLICATIONMANAGER.Startup();
The current system time is taken and stored in the lastFrame
variable. This will be used later on to work out how long it has been since the last frame was rendered. The time between frames is used to allow the game objects to move in a predictable way regardless of the frame rate.
lastFrame = SDL_GetTicks();
The next block of code defines the render loop. The render loop is a loop that is executed once per frame.
The first thing we do in the render loop is deal with any SDL events that may have been triggered during the last frame. For now, the only event we are interested in is the SDL_Quit
event, which is triggered when the window is closed. In that event, we set the running variable to false
, which will drop us out of the render loop.
SDL_Event sdlEvent;
while(SDL_PollEvent(&sdlEvent))
{
if (sdlEvent.type == SDL_QUIT)
running = false;
}
Next, any new or removed BaseObject
s are synced up with the main baseObjects
collection. The BaseObject
class is the base class for all the objects that will be a part of the game. When a new BaseObject
class is created or destroyed, it adds or removes itself from the main collection maintained by the EngineManager
. But, they cannot modify the baseObjects
collection directly – any new or removed objects are placed into temporary collections called addedBaseObjects
and removedBaseObjects
, which ensures the baseObjects
collection is not modified while it is being looped over. It is never a good idea to modify a collection while you are looping over its items. The calls to the AddBaseObjects
and RemoveBaseObjects
functions allow the baseObjects
collection to be updated when we can be sure we are not looping over it.
AddBaseObjects();
RemoveBaseObjects();
The time since the last frame is calculated in seconds (or a fraction of a second), and the current system time is saved in the lastFrame
variable.
Uint32 thisFrame = SDL_GetTicks();
float dt = (thisFrame - lastFrame) / 1000.0f;
lastFrame = thisFrame;
Every BaseObject
then has its Update
function called. The Update
function is where the game objects can perform any internal updates they need to do, like moving, rotating, or shooting a weapon. The frame time calculated just before is supplied, so the game objects can update themselves by the same amount every second, regardless of the frame rate.
BOOST_FOREACH (BaseObject* object, baseObjects)
object->EnterFrame(dt);
The video buffer is then cleared by painting a black rectangle to the entire screen using the SDL_FillRect
function.
SDL_Rect clearRect;
clearRect.x = 0;
clearRect.y = 0;
clearRect.w = SCREEN_WIDTH;
clearRect.h = SCREEN_HEIGHT;
SDL_FillRect(surface, &clearRect, 0);
Now the game objects are asked to draw themselves to the video surface by calling their Draw
function. It’s here that any graphics that the game objects use to represent themselves are drawn to the back buffer.
BOOST_FOREACH (BaseObject* object, baseObjects)
object->Draw(this->surface);
Finally, the back buffer is flipped, displaying it on the screen.
SDL_Flip(surface);
The Shutdown
function cleans up any memory.
The ApplicationManager
has its shutdown function called, where it will clean up any objects it has created.
APPLICATIONMANAGER.Shutdown();
We then call SDL_Quit()
, which unloads the SDL library.
surface = NULL;
SDL_Quit();
The next four functions, AddBaseObject
, RemoveBaseObject
, AddBaseObjects
and RemoveBaseObjects
, are all used to either add or remove BaseObject
s to the temporary addedBaseObjects
and removedBaseObjects
collections, or to sync up the objects in these temporary collections to the main baseObjects
collection.
void EngineManager::AddBaseObject(BaseObject* object)
{
addedBaseObjects.push_back(object);
}
void EngineManager::RemoveBaseObject(BaseObject* object)
{
removedBaseObjects.push_back(object);
}
void EngineManager::AddBaseObjects()
{
BOOST_FOREACH (BaseObject* object, addedBaseObjects)
baseObjects.push_back(object);
addedBaseObjects.clear();
}
void EngineManager::RemoveBaseObjects()
{
BOOST_FOREACH (BaseObject* object, removedBaseObjects)
baseObjects.remove(object);
removedBaseObjects.clear();
}
As we mentioned earlier, the ApplicationManager
holds the code that defines how the application is run. In this very simple demo, we are creating a new instance of the Bounce
object in the Startup
function, and removing it in the Shutdown
function.
ApplicationManager.h
#ifndef _APPLICATIONMANAGER_H__
#define _APPLICATIONMANAGER_H__
#define APPLICATIONMANAGER ApplicationManager::Instance()
#include "Bounce.h"
class ApplicationManager
{
public:
~ApplicationManager();
static ApplicationManager& Instance()
{
static ApplicationManager instance;
return instance;
}
void Startup();
void Shutdown();
protected:
ApplicationManager();
Bounce* bounce;
};
#endif
ApplicationManager.cpp
#include "ApplicationManager.h"
ApplicationManager::ApplicationManager() :
bounce(NULL)
{
}
ApplicationManager::~ApplicationManager()
{
}
void ApplicationManager::Startup()
{
try
{
bounce = new Bounce("../media/image.bmp");
}
catch (std::string& ex)
{
}
}
void ApplicationManager::Shutdown()
{
delete bounce;
bounce = NULL;
}
The BaseObject
class is the base class for all game objects. It defines the EnterFrame
and Draw
functions that the EngineManager
class uses during the render loop. Apart from defining these functions, the BaseObject
also registers itself with the EngineManager
when it is created by calling the AddBaseObject
function, and removes itself by calling the RemoveBaseObject
when it is destroyed.
BaseObject.h
#ifndef _BASEOBJECT_H__
#define _BASEOBJECT_H__
#include <SDL.h>
class BaseObject
{
public:
BaseObject();
virtual ~BaseObject();
virtual void EnterFrame(float dt) {}
virtual void Draw(SDL_Surface* const mainSurface) {}
};
#endif
BaseObject.cpp
#include "BaseObject.h"
#include "EngineManager.h"
BaseObject::BaseObject()
{
ENGINEMANAGER.AddBaseObject(this);
}
BaseObject::~BaseObject()
{
ENGINEMANAGER.RemoveBaseObject(this);
}
The VisualGameObject
extends the BaseObject
class, and adds the ability to display an image to the screen.
VisualGameObject.h
#ifndef _VISUALGAMEOBJECT_H__
#define _VISUALGAMEOBJECT_H__
#include <string>
#include <SDL.h>
#include "BaseObject.h"
class VisualGameObject :
public BaseObject
{
public:
VisualGameObject(const std::string& filename);
virtual ~VisualGameObject();
virtual void Draw(SDL_Surface* const mainSurface);
protected:
SDL_Surface* surface;
float x;
float y;
};
#endif
VisualGameObject.cpp
#include "VisualGameObject.h"
VisualGameObject::VisualGameObject(const std::string& filename) :
BaseObject(),
surface(NULL),
x(0),
y(0)
{
SDL_Surface* temp = NULL;
if((temp = SDL_LoadBMP(filename.c_str())) == NULL)
throw std::string("Failed to load BMP file.");
surface = SDL_DisplayFormat(temp);
SDL_FreeSurface(temp);
}
VisualGameObject::~VisualGameObject()
{
if (surface)
{
SDL_FreeSurface(surface);
surface = NULL;
}
}
void VisualGameObject::Draw(SDL_Surface* const mainSurface)
{
SDL_Rect destRect;
destRect.x = int(x);
destRect.y = int(y);
SDL_BlitSurface(surface, NULL, mainSurface, &destRect);
}
An SDL surface is created from a loaded BMP file with the SDL_LoadBMP
function (SDL_LoadBMP
is technically a macro).
SDL_Surface* temp = NULL;
if((temp = SDL_LoadBMP(filename.c_str())) == NULL)
throw std::string("Failed to load BMP file.");
When the surface is loaded, it may not have the same colour depth as the screen. Trying to draw a surface that is not the same colour depth takes a lot of extra CPU cycles, so we use the SDL_DisplayFormat
function to create a copy of the surface we just loaded which matches the current screen depth. By doing this once, as opposed to every frame, we gain some extra performance.
surface = SDL_DisplayFormat(temp);
After the new surface has been created with the correct format, the surface that was created by loading the BMP file can be removed.
SDL_FreeSurface(temp);
The VisualGameObject
destructor cleans up the surface that was created by the constructor.
VisualGameObject::~VisualGameObject()
{
if (surface)
{
SDL_FreeSurface(surface);
surface = NULL;
}
}
Finally, the Draw
function is overridden, and provides the code necessary to draw the surface we loaded in the constructor of the screen, using the x and y coordinates as the top left position of the image.
void VisualGameObject::Draw(SDL_Surface* const mainSurface)
{
SDL_Rect destRect;
destRect.x = int(x);
destRect.y = int(y);
SDL_BlitSurface(surface, NULL, mainSurface, &destRect);
}
The Bounce
class is an example of how all of these other classes come together.
Bounce.h
#ifndef _BOUNCE_H__
#define _BOUNCE_H__
#include <SDL.h>
#include <string>
#include "VisualGameObject.h"
class Bounce :
public VisualGameObject
{
public:
Bounce(const std::string filename);
~Bounce();
void EnterFrame(float dt);
protected:
int xDirection;
int yDirection;
};
#endif
Bounce.cpp
#include "Bounce.h"
#include "Constants.h"
static const float SPEED = 50;
Bounce::Bounce(const std::string filename) :
VisualGameObject(filename),
xDirection(1),
yDirection(1)
{
}
Bounce::~Bounce()
{
}
void Bounce::EnterFrame(float dt)
{
this->x += SPEED * dt * xDirection;
this->y += SPEED * dt * yDirection;
if (this->x < 0)
{
this->x = 0;
xDirection = 1;
}
if (this->x > SCREEN_WIDTH - surface->w)
{
this->x = float(SCREEN_WIDTH - surface->w);
xDirection = -1;
}
if (this->y < 0)
{
this->y = 0;
yDirection = 1;
}
if (this->y > SCREEN_HEIGHT - surface->h)
{
this->y = float(SCREEN_HEIGHT - surface->h);
yDirection = -1;
}
}
The EnterFrame
function is overridden, and moves the image around by modifying the base VisualGameObject
x
and y
variables, bouncing off the sides of the screen when it hits the edge.
void Bounce::EnterFrame(float dt)
{
this->x += SPEED * dt * xDirection;
this->y += SPEED * dt * yDirection;
if (this->x < 0)
{
this->x = 0;
xDirection = 1;
}
if (this->x > SCREEN_WIDTH - surface->w)
{
this->x = float(SCREEN_WIDTH - surface->w);
xDirection = -1;
}
if (this->y < 0)
{
this->y = 0;
yDirection = 1;
}
if (this->y > SCREEN_HEIGHT - surface->h)
{
this->y = float(SCREEN_HEIGHT - surface->h);
yDirection = -1;
}
}
As you can see, by defining the underlying logic required to use SDL in the EngineManager
, BaseObject
, and VisualGameObject
classes, classes like Bounce
can focus on the code that defines their behaviour rather than worry about the lower level issues like drawing itself to the screen. We will use these base classes more as the game is developed.
For now, we have a simple introduction to SDL, as well as the beginnings of a framework that we can build on to.
History
- 21 August 2009 - Initial post.