TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 2 - CodeProject
TetroGL - An OpenGL Game Tutorial in C++ For Win32 Platforms - Part 2 - CodeProject
Learn how to load images, display them on the screen and manage them efficiently and how to
display animations.
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.
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).
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 ▲
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();
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).
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;
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.
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.
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);
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);
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 ▲
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 ▲
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 ▲
}
}
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.
C++ Shrink ▲
~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();
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()
{
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;
}
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 ▲
private:
// Typedef for a std::list containing TImagePtr objects
typedef std::list<TImagePtr> TImageList;
// The list of images
TImageList m_lstImages;
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 ▲
private:
typedef std::map<std::string, CImageList> TAnimMap;
typedef TAnimMap::iterator TAnimMapIter;
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;
}
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;
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 ▲
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 ▲
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);
m_pGrassImg->BlitImage(xPos, yPos);
}
}
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.