Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C++

Introduction to SDL

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
20 Aug 2009CPOL7 min read 26.2K   1K   20  
See how to use SDL to create a simple visual application.

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
C++
#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
C++
#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.

C++
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.

C++
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.

C++
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.

C++
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.

C++
SDL_Event sdlEvent;
while(SDL_PollEvent(&sdlEvent))
{
    if (sdlEvent.type == SDL_QUIT)
        running = false;
}

Next, any new or removed BaseObjects 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.

C++
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.

C++
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.

C++
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.

C++
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.

C++
BOOST_FOREACH (BaseObject* object, baseObjects)
    object->Draw(this->surface);

Finally, the back buffer is flipped, displaying it on the screen.

C++
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.

C++
APPLICATIONMANAGER.Shutdown();

We then call SDL_Quit(), which unloads the SDL library.

C++
surface = NULL;
SDL_Quit();

The next four functions, AddBaseObject, RemoveBaseObject, AddBaseObjects and RemoveBaseObjects, are all used to either add or remove BaseObjects to the temporary addedBaseObjects and removedBaseObjects collections, or to sync up the objects in these temporary collections to the main baseObjects collection.

C++
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
C++
#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
C++
#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
C++
#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
C++
#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
C++
#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
C++
#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).

C++
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.

C++
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.

C++
SDL_FreeSurface(temp);

The VisualGameObject destructor cleans up the surface that was created by the constructor.

C++
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.

C++
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
C++
#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
C++
#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.

C++
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.

Image 1

History

  • 21 August 2009 - Initial post.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Writer
Australia Australia
I enjoy exploring any kind of multimedia programming, and most of my work is showcased over at Hubfolio and The Tech Labs. At these sites you will find a selection of Flash, Silverlight, JavaScript, Haxe and C++ tutorials, articles and sample projects.

Comments and Discussions

 
-- There are no messages in this forum --