Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
17 views

TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 2 - CodeProject

The document discusses loading and managing textures and images in OpenGL. It describes organizing project files and setting up DevIL for loading images. It introduces a texture manager singleton class that loads and stores textures to avoid duplicating loads. Methods to get and release textures from the manager are also described.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
17 views

TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 2 - CodeProject

The document discusses loading and managing textures and images in OpenGL. It describes organizing project files and setting up DevIL for loading images. It introduces a texture manager singleton class that loads and stores textures to avoid duplicating loads. Methods to get and release textures from the manager are also described.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

TetroGL: An OpenGL Game Tutorial in

C++ for Win32 Platforms - Part 2


Cedric Moonen Rate me: 4.95/5 (36 votes)
29 Mar 2009 CPOL 28 min read 200K 5.6K 95 55

Learn how to load images, display them on the screen and manage them efficiently and how to
display animations.

Download source - 1.21 MB

Foreword
This series of articles focuses on a 2D game development with C++ and OpenGL for Windows
platform. We will not only focus on OpenGL but also talk about the designs that are commonly
used in game programming with a full object oriented approach. You should already be familiar
with the C++ language in order to get the maximum out of this series. There is a message board at
the bottom of the article that you can use if you have questions, remarks or suggestions.

The series is divided into three articles:


Part 1 : covers the window creation and the setting-up of OpenGL.
Part 2: covers resources handling and displaying simple animations.
Part 3: groups everything together and talk about the game logic.

Contents
Introduction
Organizing the Files
Loading Images
Resource Management
The CTexture Class
The CImage Class
Displaying Animations
Example
Conclusion
References
Acknowledgement
History

Introduction
The first article in the series focused on the main window creation and the set-up of OpenGL. This
article will be a bit more fun because we will be able to load and display graphic files and display
some animations. We will also see how we can efficiently manage those resources. The picture you
see at the top of the article is what we will reach at the end of the article. This is not yet a game
because it doesn't have any game logic: the only thing it does is the ability to move the character
on the screen and animating it correctly (collision detection is not implemented).

Organizing the Files


We first start by organizing our files in a better way. I usually create a src folder which contains all
my source files (*.cpp and *.h), a bin folder which contains the final executable and all the required
resources, an obj folder which is used for the intermediate files resulting from the compilation and a
dependencies folder which contains all external dependencies that are required for the compilation
of my project (we will see later that we use an external dependency). The main advantage is that we
now have a bin folder which contains what will be distributed. If you have many resources (images,
music, configuration files, ...), you can even divide this bin folder into specific sub-folders. Take a
look at the attached zip file to see the folder organization.

Let's now change the project settings in order to use this folder configuration. For the source files,
just copy them into the src folder and add them to your project. To configure the output folder and
the intermediate folder, change the Output Directory and the Intermediate Directory in
the General section as shown in the following picture.
$(SolutionDir) and $(ConfigurationName) are predefined macros. The first one translates to the
folder of the solution and the second one translates to the current active configuration (debug or
release): in the obj folder, two sub-folders will be created, one for each configuration. Don't forget
to apply those changes to both configurations (debug and release).

Loading Images
Unfortunately, OpenGL doesn't provide any support for loading graphic files. So, we have the
choice to either write the code to load the images ourselves (and do that for each of the formats
we are interested in), or to use an existing library that does the job for us. As you probably already
guessed, the second choice is probably the best: we will gain a considerable amount of time and
we will use a library that has already been tested and debugged and which is probably compatible
with much more file formats than we will be able to write.

There are several options for which library to use. Two that I am aware of are: DevIL and FreeImage.
DevIL is a bit more adapted to OpenGL so that is the reason why I've chosen this one, but
FreeImage is a perfectly valid choice as well.

The first thing we do is to copy the required DevIL files in the dependencies folder: we first create a
sub-folder called DevIL and we copy there the content of the archive that can be found on
the DevIL website. We have to modify the name of a file in order to use it correctly: in the
"include\IL" folder, you will find a file named config.h.win, rename it to config.h. Then copy
the DevIL.dll file into your bin folder because it is used by your executable.

We then have to configure the project settings in order to use DevIL. In C/C++ category -
> General -> Additional Include Directories, specify dependencies\DevIL\include\. This tells the
compiler where to find the header files required for DevIL. This way, we won't need to supply the
full path to the DevIL header file.

In Linker category-> General -> Additional Library Directories, specify dependencies\DevIL\lib. This
tells the linker where to find additional folders which may contain library to link with.

And in Linker category -> Input -> Additional Dependencies, specify DevIL.lib. This tells the linker
that the project must be linked with the DevIL library. Keep in mind that we were already linking
to OpenGL32.lib.
Resource Management
Now that everything is set-up correctly to use DevIL, we are ready to load some images and display
them. But first, let's think a bit of how we will manage those files a bit more efficiently. Suppose
that we need to display a tree that is contained in a file called tree.png, the brute force approach is
to simply load the file and store it in memory so that we can reuse it for each frame that needs to
be drawn. This seems nice as a first approach but there is a small problem: Suppose that we now
need to display this tree more than once, then we will load the texture several times in memory
which is clearly inefficient. We need a way to be able to reuse the same texture if it is needed at
different locations in our code. This is easily solved by delegating the loading to a specific class: the
texture manager. Let's first take a look at this class before going into the details of the file loading
itself:

C++ Shrink ▲

// The texture manager avoid a same texture to be loaded multiple


// times. It keeps a map containing all the already loaded textures.
class CTextureManager
{
public:
// Loads a texture specified by its filename. If the texture is not
// loaded already, the texture manager will load it, store it and
// return it. Otherwise it simply returns the existing one.
CTexture* GetTexture(const std::string& strTextName);
// Release the texture specified by its filename. Returns true if
// the texture was found, otherwise false.
bool ReleaseTexture(const std::string& strTextName);

// Returns the single instance of the texture manager.


// The manager is implemented as a singleton.
static CTextureManager* GetInstance();
protected:
// Both constructor and destructor are protected to make
// it impossible to create an instance directly.
CTextureManager();
~CTextureManager();

private:
typedef std::map<std::string,CTexture*> TTextureMap;
// The map of already loaded textures. There are indexed
// using their filename.
TTextureMap m_Textures;
};

The first thing to notice about this class is that it is implemented as a singleton pattern. If you never
heard about the singleton pattern before, take a look at the references, there's a link to an article
discussing it. Basically, it ensures that the class has only one instance and provides a way to access
it. In our case, the constructor is protected which forbids anybody to create an instance directly.
Instead, a static method (GetInstance) allows you to retrieve the unique instance of the class:

C++
CTextureManager* CTextureManager::GetInstance()
{
// Returns the unique class instance.
static CTextureManager Instance;
return &Instance;
}

I won't discuss this pattern in detail here but don't hesitate to take a look at the article or Google
for it (there are plenty of articles discussing it). In our case, we only want a single instance of this
class and having a global point to access it makes it easy to use:

C++
CTexture* pTexture = CTextureManager::GetInstance()->GetTexture("MyTexture.bmp");

The constructor of the class takes care of initializing the DevIL library properly:

C++
CTextureManager::CTextureManager() : m_Textures()
{
// Initialize DevIL
ilInit();

// Set the first loaded point to the


// upper-left corner.
ilOriginFunc(IL_ORIGIN_UPPER_LEFT);
ilEnable(IL_ORIGIN_SET);
}

Before calling any DevIL function, you first have to call ilInit in order to initialize the library. We
will also specify how the images will be loaded: the upper-left corner first. This is done so that we
won't have inverted textures. By default this option is disabled so we enable it by
calling ilEnable(IL_ORIGIN_SET).

Let's now look at the GetTexture method:

C++
CTexture* CTextureManager::GetTexture(const string& strTextName)
{
// Look in the map if the texture is already loaded.
TTextureMap::const_iterator iter = m_Textures.find(strTextName);
if (iter != m_Textures.end())
return iter->second;

// If it was not found, try to load it from file. If the load


// failed, delete the texture and throw an exception.
CTexture* pNewText = NULL;
try
{
pNewText = new CTexture(strTextName);
}
catch (CException& e)
{
delete pNewText;
throw e;
}

// Store the newly loaded texture and return it.


m_Textures[strTextName] = pNewText;
return pNewText;
}

The code is not too difficult to understand: We first try to retrieve the texture specified
by strTextName in the map of already loaded texture. If it was found, it is returned, otherwise we try
to load it from the file. As we will see later, the constructor of CTexture attempts to load the file
and throw an exception if it fails to do so. Then, in the texture manager, if an exception was caught,
we delete the texture (to avoid a memory leak) and we re-throw the exception. If the texture was
loaded successfully, it is stored in the map (using its name as a key) and it is returned.

A method to release existing texture is also provided:

C++
bool CTextureManager::ReleaseTexture(const std::string& strTextName)
{
// Retrieve the texture from the map
bool bFound = false;
TTextureMap::iterator iter = m_Textures.find(strTextName);
if (iter != m_Textures.end())
{
// If it was found, we delete it and remove the
// pointer from the map.
bFound = true;
if (iter->second)
delete iter->second;
m_Textures.erase(iter);
}
return bFound;
}

Here also, the code is rather self-explanatory: we simply try to retrieve the texture from the map
and on success, we delete it and remove the pointer from the map. If the texture was successfully
removed, the function returns true.

The CTexture Class


Let's now look at the CTexture class in more detail:

C++ Shrink ▲

class CTexture
{
friend class CTextureManager;

public:
// Specifies a color key to be used for the texture. The color
// specified as arguments will be transparent when the texture
// is rendered on the screen.
void SetColorKey(unsigned char Red, unsigned char Green, unsigned char Blue);

// Returns the width of the texture


unsigned int GetWidth() const { return m_TextData.nWidth; }
// Returns the height of the texture.
unsigned int GetHeight() const { return m_TextData.nHeight; }

// Adds/release a reference for the texture. When ReleaseReference


// is called and decreases the reference count to 0, the texture
// is released from the texture manager.
void AddReference();
void ReleaseReference();

// Bind this texture with openGL: this texture becomes


// the 'active' texture in openGL.
void Bind() const;

protected:
// Constructor which takes the filename as argument.
// It loads the file and throw an exception if the load
// failed.
CTexture(const std::string& strFileName);
~CTexture();

private:
// Loads the texture from the specified file. Throws an
// exception if the load failed.
void LoadFile(const std::string& strFileName);

// Structure that contains the information about the texture.


struct STextureData
{
// Width of the texture
unsigned int nWidth;
// Height of the texture
unsigned int nHeight;
// Byte array containing the texture data
unsigned char* pData;
};
STextureData m_TextData;

// The openGL id associated with this texture.


mutable GLuint m_glId;

// Reference count of the number of images that still hold a reference


// to this texture. When no images reference the texture anymore, it is
// released.
int m_iRefCount;
// The filename from which the texture was loaded from.
std::string m_strTextName;
};

For this class, we can also see that the constructor has been made protected. The reason is that
only the CTextureManager class should be able to create textures, that's the reason it has been
made a friend of this class. The core of the CTexture class is the STextureData structure, which
contains all the data loaded from the file: an array of bytes containing the file data and the width
and height of the texture. Let's see how the file is loaded, which is done in the LoadFile(const
std::string& strFileName) function:

C++ Shrink ▲

void CTexture::LoadFile(const std::string& strFileName)


{
// Generate a new image Id and bind it with the
// current image.
ILuint imgId;
ilGenImages(1,&imgId);
ilBindImage(imgId);

// Load the file data in the current image.


if (!ilLoadImage(strFileName.c_str()))
{
string strError = "Failed to load file: " + strFileName;
throw CException(strError);
}

// Store the data in our STextureData structure.


m_TextData.nWidth = ilGetInteger(IL_IMAGE_WIDTH);
m_TextData.nHeight = ilGetInteger(IL_IMAGE_HEIGHT);

unsigned int size = m_TextData.nWidth * m_TextData.nHeight * 4;


m_TextData.pData = new unsigned char[size];
ilCopyPixels(0, 0, 0, m_TextData.nWidth, m_TextData.nHeight,
1, IL_RGBA, IL_UNSIGNED_BYTE, m_TextData.pData);
// Finally, delete the DevIL image data.
ilDeleteImage(imgId);
}

As you can see, we are using DevIL to load the file. The first thing we do is create a new image id
in DevIL and bind it with the current image. This is needed if you want to do some manipulation on
a certain image using its id. In fact, we will only use it to delete the image later when we have
finished using it. Next, we try to load the file using ilLoadImage: The function takes care of the
different file formats and will return false if the load failed (you can also retrieve an error code by
calling ilGetError). If that's the case, we simply throw an exception. If you remember the first
article, those exceptions will be caught in the main function and display an error message before
exiting the program. We then retrieve the width and height of the image
(the ilGetInteger and ilCopyPixels functions always work on the current active image). We then
allocate room for the data in the m_TextData.pData field: each pixel is coded on 4 bytes (we will
see this later). We then call the ilCopyPixels function to copy the image data in our buffer. The
three first parameters are the X, Y and Z offset of where to start copying (the Z offset is used for
volumetric images), and the three next parameters are the number of pixels to copy in those
directions (here also, we don't use volumetric images so the Depth is 1). Then we specify the format
of the image: a RGBA format which means 1 byte for each color channel (Red, Green and Blue, or
RGB) and one byte for the alpha channel (A).The alpha channel is used to specify the transparency
of the pixel. A value of 0 means fully transparent and a value of 255 means fully opaque. We then
specify the type of each component: they should be coded as unsigned bytes (unsigned chars). The
last argument of the function is the pointer to the buffer where to copy the pixels. At the end, we
delete the DevIL image data because we won't need it anymore.

Remark: There's an easier way to load textures with DevIL if you want to use them in OpenGL.
The ILUT library allows you to load an image and associate it directly with an OpenGL texture by
calling ilutGLLoadImage which returns the OpenGL id of the texture. This is the easiest way to go
but you won't be able to manipulate the raw data directly as we will do to set the color key.

Once the data has been loaded from the file, we need to generate a new OpenGL texture and
supply the data. This is done the first time the texture is requested to be used, in
the CTexture::Bind() function:

C++ Shrink ▲

void CTexture::Bind() const


{
// If the texture has not been generated in OpenGL yet,
// generate it.
if(!m_glId)
{
// Generate one new texture Id.
glGenTextures(1,&m_glId);
// Make this texture the active one, so that each
// subsequent glTex* calls will affect it.
glBindTexture(GL_TEXTURE_2D,m_glId);

// Specify a linear filter for both the minification and


// magnification.
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Sets drawing mode to GL_MODULATE
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

// Finally, generate the texture data in OpenGL.


glTexImage2D(GL_TEXTURE_2D, 0, 4, m_TextData.nWidth, m_TextData.nHeight,
0,GL_RGBA,GL_UNSIGNED_BYTE,m_TextData.pData);
}
// Make the existing texture specified by its OpenGL id
// the active texture.
glBindTexture(GL_TEXTURE_2D,m_glId);
}

The important point to understand when working with textures is that OpenGL only works with one
texture at a time. So, in order to texture a polygon, you need to select the active texture (also called
'binding'). This is done by calling glBindTexture. Each OpenGL texture has its own Id, which is in
our case stored in the m_glId member of the CTexture class. An Id of 0 is reserved and will never
be generated by OpenGL, so we can use it to specify that our texture has not been generated in
OpenGL yet. So, the first time this function is called, m_glId will be 0. If we look inside
the if condition (so, if the texture is not generated), the first thing we do is ask OpenGL to
generate a free Id for us by calling glGenTextures.

The m_glId is mutable because we still want the bind function to be const and this member will be
modified only once, when the texture is generated. The glGenTextures function lets you generate
multiple Id (the first argument is the number of Id to be generated), but we only want a single Id,
which will be stored in m_glId. We then call glBindTexture: this binds the texture specified by its Id
to the active 2 dimensional active texture (you can also work with 1 dimensional textures). This is
needed so that each subsequent calls to texture manipulation routines will affect this specific
texture (this in fact makes our texture the active one in OpenGL).

We then specify the filtering for minification and magnification: in general, one point of the texture,
also called a texel, does not map directly to one pixel on the screen. Sometimes a texel covers less
than a pixel (so that you have more than one texel in one pixel) or sometimes it is the opposite (a
texel covers multiple pixels). When a pixel contains only a portion of a texel, it is called
magnification and when a pixel contains several texels, it is called minification. Those two functions
tells how should OpenGL interpret those situations: if you specify GL_LINEAR, OpenGL uses a linear
average of the 2x2 array of texels that lies nearest to the center of the pixel. If you
specify GL_NEAREST, OpenGL will use the texel with coordinates nearest the center of the pixel.
There are other options for the minification filter which consists of having multiple copies of the
texture for different sizes (those copies are called mip-maps) but we won't enter into too much
details here.

Next, glTexEnvf sets the drawing mode to GL_MODULATE so that the color of the textured polygons
will be a modulation of the texture color and the color on which the texture is pasted. This is
needed in order to make some parts of the image transparent using the alpha channel. Finally, we
generate the OpenGL texture by calling glTexImage2D: the first argument to the function is the type
of texture (1 or 2 dimensions), the second argument is the level of the texture in case we are using
multiple resolutions of the texture (mip-maps).

In our case, we don't use multiple resolution, so we specify 0. The third argument is the number of
components (R, G, B and A) that are used for modulating and blending. The two following
arguments are the width and the height of the texture. The 6th argument specifies the width of the
texture border, which is 0 in our case. The 7th and 8th arguments describe the format and data type
of the texture data: the texture format is RGBA and each component of a texel is an unsigned byte.
The last parameter is the pointer to the data.
Warning: OpenGL works with textures that have a size (width and height) which is a power of two
(so, a 128x128 texture is valid but a 128x120 is not). On some graphic cards, displaying textures that
do not follow this rule might fail and you will only see a white rectangle. A solution to the problem
is to have all your textures follow this rule (even if you have to leave some non-used space in the
image file). Another solution is to manage that when loading the image: you could always create a
buffer which has the correct dimensions and load the image in it (but you have to take care how
you do this, because the unused pixels should be left on each line).

If CTexture::Bind() is called when the texture is already available, the function only
calls glBindTexture, which makes this texture the active one. We will see later how this texture will
be used to be drawn on the screen.

A feature that is often used in games is what we call color keying. Some file formats do not support
a transparent channel (like a BMP file), so if you want to make some parts of the texture
transparent, the only option is to use a specific color that will be made transparent. OpenGL does
not support color keying but it can easily be added by using the alpha channel of the texture. That
is what the CTexture::SetColorKey function is doing:

C++ Shrink ▲

void CTexture::SetColorKey(unsigned char Red,


unsigned char Green,
unsigned char Blue)
{
// If the texture has already been specified to OpenGL,
// we delete it.
if (m_glId)
{
glDeleteTextures(1,&m_glId);
m_glId = 0;
}

// For all the pixels that correspond to the specified color,


// set the alpha channel to 0 (transparent) and reset the other
// ones to 255.
unsigned long Count = m_TextData.nWidth * m_TextData.nHeight * 4;
for (unsigned long i = 0; i<Count; i+=4)
{
if ( (m_TextData.pData[i]==Red) && (m_TextData.pData[i+1]==Green)
&& (m_TextData.pData[i+2]==Blue) )
m_TextData.pData[i+3] = 0;
else
m_TextData.pData[i+3] = 255;

}
}

The function is quite basic: we walk over our texture data and if we find a pixel of the specified
color, we set its alpha channel to 0, which means fully transparent. For all the other pixels, we reset
the channel to 255 (suppress a previous color key). But we first need to check if the texture was
already specified to OpenGL. If that is the case, we need to reload the texture in OpenGL. This is
done by simply setting m_glId to 0 (if you remember, the Bind function first checks if this variable
is 0). By calling glDeleteTextures, we delete the texture in OpenGL (the first argument is the
number of textures we want to delete and the second is their Id).
Finally, the texture is reference counted and its constructor is protected, so that you can't create
a CTexture object directly. The reference counted is done through
the AddReference and ReleaseReference functions:

C++
void CTexture::AddReference()
{
// Increase the reference count.
m_iRefCount++;
}

void CTexture::ReleaseReference()
{
// Decrease the reference count. If it reaches 0,
// the texture is released from the texture manager.
m_iRefCount--;
if (m_iRefCount == 0)
CTextureManager::GetInstance()->ReleaseTexture(m_strTextName);
}

As you can see, nothing really fancy here: whenever a CTexture object is
referenced, AddReference is called which increases the reference count. Once the texture is not
needed anymore, ReleaseReference is called which decrements the reference count. Once it
reaches 0, the texture will be released from the texture manager (which will delete it). Reference
counting is used because several CImage objects can reference the same texture. We need to know
how many of them are still using the texture instead of releasing it whenever one of the image
objects is destroyed.

The CImage Class


Let's now look at how this texture is used by the CImage class. As we saw earlier, the CTexture is not
manipulated directly by the user. The reason is that it is mainly a wrapper around a resource file
and such file can be made of several images: suppose that you want to display several kind of trees
in your game, it could be convenient to have them all stored in the same file. So, the texture class in
itself doesn't have any functionality to draw the image on the screen, but only to load a file. The
image class is the one responsible to draw the texture (or only a part of it) on the screen. Several
images can then reference the same texture but use a different portion of it.

C++ Shrink ▲

// Typedef of a CImage class that is wrapped inside a smart


// pointer.
typedef CSmartPtr<CImage> TImagePtr;

// An image is manipulated directly by the end user (instead of


// the texture). The main difference between an image and a texture
// is that the texture can contain multiple images (it is the
// complete file).
class CImage
{
public:
// Blit the image at the specified location
void BlitImage(int iXOffset=0, int iYOffset=0) const;
// Returns the texture that this image is using.
CTexture* GetTexture() const { return m_pTexture; }

// Helper functions to create an new image. A smart pointer


// holding the new image is returned. strFileName is the
// name of the file containing the texture and textCoord is
// the rectangle in this texture which contains the image.
static TImagePtr CreateImage(const std::string& strFileName);
static TImagePtr CreateImage(const std::string& strFileName,
const TRectanglei& textCoord);

~CImage();

protected:
// Protected constructors to avoid to be able to create a
// CImage instance directly.
CImage(const std::string& strFileName);
CImage(const std::string& strFileName, const TRectanglei& textCoord);

private:
// The texture from which this image is part of.
CTexture* m_pTexture;
// The rectangle that specifies the position of the image
// in the full texture.
TRectanglei m_rectTextCoord;
};

Before going into the details about how to instantiate this class, we will look at how it works. It has
two members: the texture from which the image comes from and a rectangle specifying the portion
of the texture which contains the image. I won't put the code of the CRectangle class because it is
very trivial: It contains four members which are the top, bottom, left and right coordinates of the
rectangle plus some support functions (like checking if it intersects with another rectangle, retrieve
the width and height of the rectangle, ...). It is a template class, so you can choose the type of the
rectangle coordinates (integer, float, double, ...). TRectanglei is a typedef for a rectangle with
integer coordinates. Let's see how the BlitImage function works, by drawing the texture at the
location specified by the arguments:

C++
void CImage::BlitImage(int iXOffset, int iYOffset) const
{
if (m_pTexture)
{
m_pTexture->Bind();

// Get the coordinates of the image in the texture, expressed


// as a value from 0 to 1.
float Top = ((float)m_rectTextCoord.m_Top)/m_pTexture->GetHeight();
float Bottom = ((float)m_rectTextCoord.m_Bottom)/m_pTexture->GetHeight();
float Left = ((float)m_rectTextCoord.m_Left)/m_pTexture->GetWidth();
float Right = ((float)m_rectTextCoord.m_Right)/m_pTexture->GetWidth();

// Draw the textured rectangle.


glBegin(GL_QUADS);
glTexCoord2f(Left,Top); glVertex3i(iXOffset,iYOffset,0);
glTexCoord2f(Left,Bottom); glVertex3i(iXOffset,iYOffset+
m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Bottom); glVertex3i(iXOffset+m_rectTextCoord.GetWidth(),
iYOffset+m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Top); glVertex3i(iXOffset+m_rectTextCoord.GetWidth(),
iYOffset,0);
glEnd();
}
}

We first bind the texture (make it the active one in OpenGL), then we calculate the coordinates of
the image within the texture. Those values are expressed between 0 and 1, with 0 being the top/left
side of the texture and 1 being the bottom/right side of the texture. We then draw a rectangle as
seen in the first tutorial, except that before specifying each point, we call glTexCoord2f which
specifies a texel (point in a texture) in the current binded OpenGL texture. By doing this, OpenGL
will be able to associate texels from the texture to pixels on the screen, and display our textured
rectangle using the active texture.

Let's now look at the constructors and destructor. There are two constructors (which are protected):
one which accepts only a texture name and one which accepts both a texture name and a
rectangle. The one with only the texture name will use the full texture as the image, and the other
one will use the image contained at the specified rectangle in the file.

C++ Shrink ▲

CImage::CImage(const string& strFileName)


: m_pTexture(NULL), m_rectTextCoord()
{
// This line will throw an exception if the texture is not found.
m_pTexture = CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture->AddReference();

// Set the texture coordinate to the full texture


m_rectTextCoord.m_Top = m_rectTextCoord.m_Left = 0;
m_rectTextCoord.m_Bottom = m_pTexture->GetHeight();
m_rectTextCoord.m_Right = m_pTexture->GetWidth();
}

CImage::CImage(const string& strFileName, const TRectanglei& textCoord)


: m_pTexture(NULL), m_rectTextCoord(textCoord)
{
// This line will throw an exception if the texture is not found.
m_pTexture = CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture->AddReference();
}

CImage::~CImage()
{
if (m_pTexture)
m_pTexture->ReleaseReference();
}

The constructors retrieve the texture through the texture manager. Remember that this call can
throw an exception if the texture doesn't exist. Then the reference count of the texture is increased.
In case no rectangle was specified, the full texture is used as an image. The destructor simply
releases the texture which decrements the reference count as seen earlier in the texture class.
As I already said, the constructors of the class are protected. The reason for that is to force the user
to use a smart pointer that wraps the CImage class. Ok, before panicking because of this strange
thing, let me first say that wrapping the CImage class into a smart pointer is not a necessity but it is
very useful to make sure that all of the resources are released when not used anymore. If you don't
allocate dynamically CImage objects (using new), this is already done for you (through the
destructor). But as soon as you are creating dynamic objects, you can always forget to delete them,
which lead to unreleased resources. Furthermore, if you start exchanging those objects between
different parts of your code, which part should be responsible to delete the object? All those
problems are solved by wrapping the object into a smart pointer class. I won't fully discuss how it is
implemented because there are already a lot of articles covering this subject (you can have a look
at the references, there is a link to a good article). In brief, a smart pointer takes care of the lifetime
of the object which it is maintaining: when the object is not needed anymore, it is destroyed. You
can 'share' this pointer and once it is not needed anymore, the pointed object will be deleted. You
can also easily access the wrapped object as if you were manipulating it directly: the smart pointer
overloads the -> and . operators to redirect them to the owned object. All of that sounds a bit
complicated, but the usage is really easy: Instead of using the pointer to the object directly, you
give it to a smart pointer which will take care of its lifetime for you (you don't have to delete the
pointer anymore). The access to the object is almost transparent because you can still access the
members as if you were using the pointer directly.

For this tutorial, I provided my own smart pointer class but it is preferable in general to use
the boost::shared_ptr class (see references). The reason why I provided mine is simply to avoid
having yet another dependency so that it is easier for you to compile the project (you don't have to
download the package from boost). You can have a look at how it is implemented but I won't give a
full explanation here.

Finally, the CImage class provides two static helper functions to be able to create instances of the
class. They simply create a new instance, pass it to a smart pointer and return the smart pointer:

C++
TImagePtr CImage::CreateImage(const string& strFileName)
{
TImagePtr imgPtr(new CImage(strFileName));
return imgPtr;
}

TImagePtr CImage::CreateImage(const string& strFileName, const TRectanglei& textCoord)


{
TImagePtr imgPtr(new CImage(strFileName,textCoord));
return imgPtr;
}

Displaying Animations
What would be a game without animations? Probably quite boring to play, so let's look at how we
can add some dynamism here by playing animations. The basic idea behind animations in 2D
games is rather simple: It is the same as a cartoon, which consists of breaking up the movement
into distinct images. The brute force approach would be to have a loop in which you sleep for a
while before displaying the next image. As you might already have guessed, this doesn't work at all.
You have several issues if you try to do that: first, nothing will be displayed at all because you never
swap the buffers (which was done in the CMainWindow::Draw() function). Second, if you do that,
the rest of your program is not processed at all, which also means that you would only be able to
display one animation at a time. Not very convenient... The correct approach consists of letting each
'animation' remember its state (e.g. which image it is currently displaying) and asking all of them to
draw their current image. When a new frame should be drawn, each animation is 'asked' to go to
the next image in the animation. This way, you keep a continuous flow in your program.

Let's now take a look at the CImageList class. This class is basically a wrapper class around
a std::list which contains images and provides some helper functions to play the images:

C++ Shrink ▲

// Wraps a list of images which is used to play animations.


class CImageList
{
public:
// Default constructor: construct an empty list.
CImageList();
// Copy constructor: copies the content of the
// list passed in argument.
CImageList(const CImageList& other);
// Default destructor.
~CImageList();

// Assignment operator: empty the current content


// and copies the content of the list passed in argument.
CImageList& operator= (const CImageList& other);

// Empty the content of the list


void Clear();
// Append a new image to the list
void AppendImage(TImagePtr pImage);
// Return the number of images in this list
unsigned GetImagesCount() const;

// Make the first image active


void GoToFirstImage();
// Make the next image active. If the last image
// was active, we go back to the first image. In
// that case, the function returns true.
bool GoToNextImage();
// Get the current image
TImagePtr GetCurrentImage() const;

private:
// Typedef for a std::list containing TImagePtr objects
typedef std::list<TImagePtr> TImageList;
// The list of images
TImageList m_lstImages;

// Iterator pointing to the current image


TImageList::iterator m_iterCurrentImg;
};
The implementation is pretty straightforward: it basically adds images to
a std::list<TImagePtr> on demand and keeps an iterator which points to the currently active
image. Let's for example take a look at the GoToNextImage() function:

C++
bool CImageList::GoToNextImage()
{
if (m_iterCurrentImg != m_lstImages.end() )
m_iterCurrentImg++;
else
return false;

if (m_iterCurrentImg != m_lstImages.end() )
{
m_iterCurrentImg = m_lstImages.begin();
return true;
}
return false;
}

We first check if the iterator is valid (doesn't point at the end of the list). The iterator is invalid when
the list is empty: in that case we simply return from the function, otherwise we increase the iterator.
We then check if the iterator reached the end of the list (which happens when it was previously
pointing to the last image). In that case we reset it to the first image and we return true. I won't
explain the other functions because they are rather trivial, but don't hesitate to take a look at the
code.

Let's now look at the CAnimatedSprite class which allows you to group several animations
together. Let's take an example: suppose that you are writing a game in which the player plays a
knight. This knight will of course have multiple different animations: walk, attack, standstill, ... In
general, you will need to provide such animations for each direction your knight can have in your
game. This class will then be used to represent your knight: you will be able to load several
animations and replay them later on demand:

C++ Shrink ▲

// This class represent an animated sprite: it is able to play


// different animations that were previously loaded.
class CAnimatedSprite
{
public:
// Default constructor and destructor.
CAnimatedSprite();
~CAnimatedSprite();

// Adds a new animation for the sprite. The strAnimName


// is a string that identifies the animation and should
// be unique for this sprite.
void AddAnimation(const std::string& strAnimName,
const CImageList& lstAnimation);
// Plays a previously loaded animation. The strAnimName
// is the name that was passed when calling AddAnimation.
void PlayAnimation(const std::string& strAnimName);

// Draw the current frame of the animation at the sprite


// current position.
void DrawSprite();
// Go to the next animation frame.
void NextFrame();

// Set the position of the sprite


void SetPosition(int XPos, int YPos)
{
m_iXPos = XPos;
m_iYPos = YPos;
}
// Move the sprite from its current position
void OffsetPosition(int XOffset, int YOffset)
{
m_iXPos += XOffset;
m_iYPos += YOffset;
}

private:
typedef std::map<std::string, CImageList> TAnimMap;
typedef TAnimMap::iterator TAnimMapIter;

// Map containing all the animations that can be


// played.
TAnimMap m_mapAnimations;
// Iterator to the current animation being played
TAnimMapIter m_iterCurrentAnim;

// Position of the sprite


int m_iXPos;
int m_iYPos;
};

The principle of the class is the following: it contains a map of all animations that can be played for
the sprite, with the key being a string identifying the animation and the value being
a CImageList object containing the animation. The AddAnimation and PlayAnimation simply add
or retrieve an animation from the map:

C++
void CAnimatedSprite::AddAnimation(const string &strAnimName,
const CImageList& lstAnimation)
{
m_mapAnimations[strAnimName] = lstAnimation;
}

void CAnimatedSprite::PlayAnimation(const string &strAnimName)


{
m_iterCurrentAnim = m_mapAnimations.find(strAnimName);
if (m_iterCurrentAnim == m_mapAnimations.end())
{
string strError = "Unable to play: " + strAnimName;
strError += ". Animation not found.";
throw CException(strError);
}
}
When trying to play an non existing animation, an exception is thrown.
The m_iterCurrentAnim variable is an iterator pointing to the current animation. It is used in
the DrawSprite and NextFrame method to access the current animation:

C++
void CAnimatedSprite::DrawSprite()
{
if (m_iterCurrentAnim == m_mapAnimations.end())
return;
m_iterCurrentAnim->second.GetCurrentImage()
->BlitImage(m_iXPos,m_iYPos);
}

void CAnimatedSprite::NextFrame()
{
if (m_iterCurrentAnim == m_mapAnimations.end())
return;

m_iterCurrentAnim->second.GoToNextImage();
}

In the DrawSprite method, we retrieve the current image of the current animation and simply blit it
at the specified position on the screen (remember how the CImage class was working). In
the NextFrame, we simply go to the next image in the current animation.

Example
After all those explanations, it is time for a concrete example to see how we will use all those
classes. The example will be quite simple and far from a complete game, but it shows the principles.
The purpose is to have an animated character (a knight) that can be controlled through the
direction keys. It moves in a simple scene: grass with some trees on it, in an isometric view. There is
no collision detection yet, which means that the knight can move through the trees. Another thing
that is not implemented is the order in which the sprites are drawn: the knight will always be drawn
on top of the scene, no matter where he is, which is wrong in some situations (if he is behind a tree,
the tree should be drawn on top of the knight). This is left as an exercise to the reader :).

All the code will be implemented in the CMainWindow class. Let's first add some member variables in
this class:

C++
// The image for the grass.
TImagePtr m_pGrassImg;

// Images for the trees


TImagePtr m_pTreesImg[16];

// The animated sprite of the knight


CAnimatedSprite* m_pKnightSprite;
// Which keys are currently pressed
bool m_KeysDown[4];
// The last direction of the knight
std::string m_strLastDir;

We first declare some TImagePtr which will hold several images that will be drawn (grass and trees).
We then declare the CAnimatedSprite which will be used to draw the knight. We finally have an
array of 4 booleans to store the current state of the direction keys and a string that contains the
current direction of the knight. Those variables are initialized in the constructor of the main window
class:

C++ Shrink ▲

// Load the grass image and set the color key.


m_pGrassImg = CImage::CreateImage("GrassIso.bmp");
m_pGrassImg->GetTexture()->SetColorKey(0,128,128);

// Load all the 'walk' animations for the knight.


m_pKnightSprite = new CAnimatedSprite;
CAnimFileLoader fileLoader1("KnightWalk.bmp", 8, 96, 96);
CTextureManager::GetInstance()->GetTexture("KnightWalk.bmp")
->SetColorKey(111, 79, 51);
m_pKnightSprite->AddAnimation("WalkE",
fileLoader1.GetAnimation(0,7));
m_pKnightSprite->AddAnimation("WalkSE",
fileLoader1.GetAnimation(8,15));
m_pKnightSprite->AddAnimation("WalkS",
fileLoader1.GetAnimation(16,23));
m_pKnightSprite->AddAnimation("WalkSW",
fileLoader1.GetAnimation(24,31));
m_pKnightSprite->AddAnimation("WalkW",
fileLoader1.GetAnimation(32,39));
m_pKnightSprite->AddAnimation("WalkNW",
fileLoader1.GetAnimation(40,47));
m_pKnightSprite->AddAnimation("WalkN",
fileLoader1.GetAnimation(48,55));
m_pKnightSprite->AddAnimation("WalkNE",
fileLoader1.GetAnimation(56,63));

// Load all the 'pause' animations for the knight.


CAnimFileLoader fileLoader2("KnightPause.bmp", 8, 96, 96);
CTextureManager::GetInstance()->GetTexture("KnightPause.bmp")
->SetColorKey(111, 79, 51);
m_pKnightSprite->AddAnimation("PauseE",
fileLoader2.GetAnimation(0,7));
m_pKnightSprite->AddAnimation("PauseSE",
fileLoader2.GetAnimation(8,15));
m_pKnightSprite->AddAnimation("PauseS",
fileLoader2.GetAnimation(16,23));
m_pKnightSprite->AddAnimation("PauseSW",
fileLoader2.GetAnimation(24,31));
m_pKnightSprite->AddAnimation("PauseW",
fileLoader2.GetAnimation(32,39));
m_pKnightSprite->AddAnimation("PauseNW",
fileLoader2.GetAnimation(40,47));
m_pKnightSprite->AddAnimation("PauseN",
fileLoader2.GetAnimation(48,55));
m_pKnightSprite->AddAnimation("PauseNE",
fileLoader2.GetAnimation(56,63));
m_pKnightSprite->PlayAnimation("PauseE");
for (int i=0; i<4; i++)
m_KeysDown[i] = false;
// Set the initial direction to the east.
m_strLastDir = "E";
m_pKnightSprite->SetPosition(350,250);

This looks like a lot of code but we need to load quite a bunch of animations for our knight: 2
animations (walk and pause) for each direction (8 different directions). We are using a new class
here: the CAnimFileLoader class. It is a simple helper class to easily load an image list from a file. It
takes the file name, the number of images per row, the width and the height of an image as
parameters in the constructor and you can retrieve an image list later by simply specifying the start
index and the stop index of images in the file (it returns a CImageList object). If you now look at
the code, we first load the grass image and specify its color key, then we load all the 'walk'
animations for our knight. Each animation name depends on the direction, e.g. for the 'walk' east
direction, the animation name is "WalkE". This will be used later to play a specific animation. We
then specify that the default animation is the "PauseE" animation.

Let's now look at how we handle the events when a key is pressed. This is done in
the ProcessEvent function:

C++ Shrink ▲

void CMainWindow::ProcessEvent(UINT Message, WPARAM wParam, LPARAM lParam)


{
switch (Message)
{
// Quit when we close the main window
case WM_CLOSE :
PostQuitMessage(0);
break;
case WM_SIZE:
OnSize(LOWORD(lParam),HIWORD(lParam));
break;
case WM_KEYDOWN :
switch (wParam)
{
case VK_UP:
m_KeysDown[0] = true;
break;
case VK_DOWN:
m_KeysDown[1] = true;
break;
case VK_LEFT:
m_KeysDown[2] = true;
break;
case VK_RIGHT:
m_KeysDown[3] = true;
break;
}
UpdateAnimation();
break;
case WM_KEYUP :
switch (wParam)
{
case VK_UP:
m_KeysDown[0] = false;
break;
case VK_DOWN:
m_KeysDown[1] = false;
break;
case VK_LEFT:
m_KeysDown[2] = false;
break;
case VK_RIGHT:
m_KeysDown[3] = false;
break;
}
UpdateAnimation();
break;
}
}

As you can see, we handle the WM_KEYDOWN and the WM_KEYUP messages, which correspond to a key
pressed and a key released respectively. When such message is sent, the WPARAM contains the code
of the key which is pressed or released. We simply then set or reset the flag in our array to specify
the state of the corresponding key (so, the first element in the array corresponds to the up key, the
second to the down key, ...). We then call the UpdateAnimation function:

C++ Shrink ▲

void CMainWindow::UpdateAnimation()
{
// First check if at least one key is pressed
bool keyPressed = false;
for (int i=0; i<4; i++)
{
if (m_KeysDown[i])
{
keyPressed = true;
break;
}
}

string strAnim;
if (!keyPressed)
strAnim = "Pause" + m_strLastDir;
if (keyPressed)
{
string vertDir;
string horizDir;
if (m_KeysDown[0])
vertDir = "N";
else if (m_KeysDown[1])
vertDir = "S";
if (m_KeysDown[2])
horizDir = "W";
else if (m_KeysDown[3])
horizDir = "E";
m_strLastDir = vertDir + horizDir;
strAnim = "Walk" + m_strLastDir;
}
m_pKnightSprite->PlayAnimation(strAnim);
}

We first check if at least one key is pressed. If that's not the case, we specify that the animation that
should be played is "Pause" + the name of the last knight direction. If at least one key is pressed,
we check which ones are pressed and we build the last direction string. Let's now look at
the Draw function:

C++ Shrink ▲

void CMainWindow::Draw()
{
// Clear the buffer
glClear(GL_COLOR_BUFFER_BIT);

// Draw the grass


int xPos=0, yPos=0;
for (int i=0; i<8; i++)
{
for (int j=0; j<6; j++)
{
xPos = i * 256/2 - 128;
if (i%2)
yPos = (j * 128) - 128/2;
else
yPos = (j * 128);

m_pGrassImg->BlitImage(xPos, yPos);
}
}

// Draw some trees


m_pTreesImg[0]->BlitImage(15,25);
m_pTreesImg[1]->BlitImage(695,55);
m_pTreesImg[2]->BlitImage(15,25);
m_pTreesImg[3]->BlitImage(300,400);
m_pTreesImg[4]->BlitImage(125,75);
m_pTreesImg[5]->BlitImage(350,250);
m_pTreesImg[6]->BlitImage(400,350);
m_pTreesImg[7]->BlitImage(350,105);
m_pTreesImg[8]->BlitImage(530,76);
m_pTreesImg[9]->BlitImage(125,450);
m_pTreesImg[10]->BlitImage(425,390);
m_pTreesImg[11]->BlitImage(25,125);
m_pTreesImg[12]->BlitImage(550,365);
m_pTreesImg[13]->BlitImage(680,250);
m_pTreesImg[14]->BlitImage(245,325);
m_pTreesImg[15]->BlitImage(300,245);

// Draw the knight


m_pKnightSprite->DrawSprite();
// Move to the next frame of the animation
m_pKnightSprite->NextFrame();
// Swap the buffers
SwapBuffers(m_hDeviceContext);
}
We first draw the grass: if you open the GrassIso.bmp file, you can see that this is a losange, and not
a rectangle. That shape is typically used for isometric games to give an impression of 3D. After the
grass is drawn, we draw some trees at some predefined positions on the screen. As you can see,
manipulating the object contained in the smart pointer is completely transparent (it is as if it were
manipulating the object directly). We finally draw the knight sprite and switch to the next frame in
the animation. Moving the knight sprite is done in the Update function:

C++
void CMainWindow::Update(DWORD dwCurrentTime)
{
int xOffset = 0;
int yOffset = 0;
if (m_KeysDown[0])
yOffset -= 5;
if (m_KeysDown[1])
yOffset += 5;
if (m_KeysDown[2])
xOffset -= 5;
if (m_KeysDown[3])
xOffset += 5;
m_pKnightSprite->OffsetPosition(xOffset, yOffset);
}

If one of the keys is pressed, we move the sprite by a certain offset. As the time is passed to the
function, we could also calculate the offset to apply to the sprite depending on the time elapsed.
So, you are now ready to test the example and move your knight on the screen. Of course, the
scene should probably be loaded from a file that is generated from a specific editor, but that falls
outside the scope of this article.

Conclusion
This terminates the second article of the series, in which we saw how to load graphic files and
render them on the screen and how to display animations. The next article is the last one of the
series. We will see there how to draw text on the screen, how to manage the different states of a
game and apply everything we saw on a concrete example.

References
[1] Singleton article: A good introduction to the singleton pattern
[2] Shared pointers: An extensive article about shared pointers
[3] Boost shared_ptr: The boost library about shared_ptr
[4] Reiner's tileset: Free resources from which the images of the example were taken from
[5] DevIL: DevIL library
[6] FreeImage: FreeImage library

Acknowledgement
Thanks to Jeremy Falcon and El Corazon for their advices and help. Thanks also to the CodeProject
editors for their great job.

History
15th August, 2008: Initial post
29th March, 2009: Updated source code

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

Written By

Cedric Moonen
Engineer
Belgium

I am a 29 years old guy and I live with my girlfriend in Hoegaarden, little city from Belgium well
known for its white beer .
I studied as an industrial engineer in electronics but I oriented myself more towards software
development when I started to work.
Currently I am working in a research centre in mechatronica. I mainly develop in C++ but I also do a
bit of Java.
When I have so spare time, I like to read (mainly fantasy) and play electric guitar.

You might also like