Professional 2D artists use programs, such as Adobe Photoshop, to create 2D assets for a game. Unfortunately, we can't take the time to teach you how to use a program as sophisticated as Photoshop.
If you want to play around with creating your own assets, then you might try the Paint program that comes installed on any Windows based computer. If you really want to dig deep into 2D art creation without digging deeply into your bank account, then you can download GIMP (http://www.gimp.org), a free, full-features 2D image manipulation program.
In the previous chapter, we loaded and rendered a bitmap file. It turns out that bitmaps aren't the best format to work with sprites because they take more file space (and therefore, more memory) than PNGs, and bitmaps do not support transparency.
Before we had image formats that allowed transparency to be directly encoded as part of the image, we used a specific background color, and then expected our image library to remove that color when it handled the image. Magenta was often used as the background because it is a color rarely used in images.
Bitmaps are larger in file size than PNGs because they are not stored in a compressed format. Compression allows an image to be stored in less space, and this can be very important on devices, such as mobile phones.
PNGs are stored using a lossless compression algorithm. Lossless means that the image quality is not sacrificed to achieve the compression. Other formats, such as JPEG, can be stored in a compressed format, but use a lossy algorithm that degrades the image quality.
PNGs also support transparency using an alpha channel. In addition to storing the red, green, and blue component of each pixel (RGB), PNGs also store each pixel's transparency in the alpha channel (RGBA).
You will recall from the previous chapter that all textures are represented as rectangles in a game. However, real shapes aren't rectangular. Trees, cars, and robots all have much more complex shapes.
If we used bitmaps for all of our images, then the full rectangle of the texture would be rendered blocking out everything behind the sprite. In the following image, our robot is passing in front of a pipe, and part of the pipe is occluded by the blank space in the bitmap.
In a PNG image, we set the blank space to be transparent. In the following image, the pipe is no longer occluded by the transparent parts of the image of the robot:
In the previous chapter, we wrote a code to load a BMP file. Normally, we would have to write different code to load a PNG file. In fact, we would have to write a loader for each different type of image we wanted to work with.
Fortunately, someone has already done all of this work and made it available in a library known as SOIL: Simple OpenGL Image Library. You can download your copy from http://www.lonesock.net/soil.html.
There are several advantages to using the SOIL library:
The download comes as a zipped folder. Once you unzip the folder, you will see a folder named Simple OpenGL Image Library
. This folder contains a lot of files, but we only need soil.h
.
Now, it is time to add the SOIL library to our project:
lib
folder and find libSOIL.a
.libSOIL.a
to the folder that contains the RoboRacer2D source code.opengl32.lib
and glu32.lib
on separate lines in the dialog window and click OK.Library files for Windows usually end in .lib
, while those written for UNIX end in .a
. The standard SOIL distribution comes with the UNIX library; you need to use the Windows library. You can either find SOIL.lib
online, use the SOIL source code to create your own Windows library file, or download SOIL.lib from the book's website.
Next, we need to copy the SOIL header file into our project and include it in our code:
src
folder and find SOIL.h
.SOIL.h
to the folder that contains the RoboRacer2D source code.RoboRacer2D.cpp
.#include "SOIL.h"
to the list of includes.Now, we are ready to write a function that loads an image file. We will pass in the name of the file, and the function will return an integer representing a handle on the OpenGL texture.
The following lines of code uses SOIL to load an image:
GLuint texture = SOIL_load_OGL_texture ( imageName, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, 0 );
All of the work is done by the call to SOIL_load_OGL_texture
. The four parameters are the most generic settings:
We will use code, such as this one, to load images into our sprite
class.
In order to easily incorporate sprites into our game, we will create a class specifically for dealing with sprites.
Let's think about the features that we want:
0
if it isn't moving).In addition to these properties, we would also like to be able to manipulate the sprite in several ways. We may add methods to:
Open your game project, and add a new class called Sprite.cpp
with a header file called Sprite.h
.
Use the following code for Sprite.h
:
#pragma once: #include <glgl.h> class Sprite { public: struct Point { GLfloat x; GLfloat y; }; struct Size { GLfloat width; GLfloat height; }; struct Rect { GLfloat top; GLfloat bottom; GLfloat left; GLfloat right; }; protected: GLuint* m_textures; unsigned int m_textureIndex; unsigned int m_currentFrame; unsigned int m_numberOfFrames; GLfloat m_animationDelay; GLfloat m_animationElapsed; Point m_position; Size m_size; GLfloat m_velocity; bool m_isCollideable; bool m_flipHorizontal; bool m_flipVertical; bool m_isVisible; bool m_isActive; bool m_useTransparency; bool m_isSpriteSheet; public: Sprite(const unsigned int m_pNumberOfTextures); ~Sprite(); void Update(const float p_deltaTime); void Render(); const bool AddTexture(const char* p_fileName, const bool p_useTransparency = true); const GLuint GetCurrentFrame() { if (m_isSpriteSheet) { return m_textures[0]; } else { return m_textures[m_currentFrame]; } } void SetPosition(const GLfloat p_x, const GLfloat p_y) { m_position.x = p_x; m_position.y = p_y; } void SetPosition(const Point p_position) { m_position = p_position; } const Point GetPosition() { return m_position; } const Size GetSize() const { return m_size; } void SetFrameSize(const GLfloat p_width, const GLfloat p_height) { m_size.width = p_width; m_size.height = p_height; } void SetVelocity(const GLfloat p_velocity) { m_velocity = p_velocity; } void SetNumberOfFrames(const unsigned int p_frames) { m_numberOfFrames = p_frames; } const bool isCollideable() const { return m_isCollideable; } void IsCollideable(const bool p_value) { m_isCollideable = p_value; } void FlipHorizontal(const bool p_value) { m_flipHorizontal = p_value; } void FlipVertical(const bool p_value) { m_flipVertical = p_value; } void IsActive(const bool p_value) { m_isActive = p_value; } const bool IsActive() const { return m_isActive; } void IsVisible(const bool p_value) { m_isVisible = p_value; } const bool IsVisible() const { return m_isVisible; } void UseTransparency(const bool p_value) { m_useTransparency = p_value; } };
I know, it's a lot of code! This is a typical object-oriented class, consisting of protected properties and public methods. Let's take a look at the features of this class:
#pragma once
: This is a C++ directive telling Visual Studio to only include files once if they are included in several source files.gl.h
in this header file because we need access to the standard OpenGL variable types.m_textures
is a GLuint
array that will dynamically hold all of the OpenGL texture handles that make up this sprite.m_textureIndex
starts at zero, and is incremented each time a texture is added to the sprite.m_currentFrame
starts at zero, and is incremented each time we want to advance the frame of the animation.m_numberOfFrames
stores the total number of frames that make up our animation.m_animationDelay
is the number of seconds that we want to pass before the animation frame advances. This allows us to control the speed of the animation.m_animationElapsed
will hold the amount of time that has elapsed since the last animation frame was changed.m_position
holds the x
and y
positions of the sprite.m_size
holds the width
and height
of the sprite.m_velocity
holds the velocity of the sprite. Larger values will cause the sprite to move more quickly across the screen.m_isCollideable
is a flag that tells us whether or not this sprite collides with other objects on the screen. When set to false
, the sprite will pass through other objects on the screen.m_flipHorizontal
is a flag that tells the class whether or not the sprite image should be horizontally flipped when it is rendered. This technique can be used to save texture memory by reusing a single texture for both right and left movement.m_flipVertical
is a flag that tells the class whether or not the sprite image should be vertically flipped when it is rendered.m_isVisible
is a flag that indicates whether the sprite is currently visible in the game. If this is set to false, then the sprite will not be rendered.m_isActive
is a flag that indicates whether the sprite is currently active. If this is set to false, then the sprite animation frame and sprite position will not be updated.m_useTransparency
is a flag that tells the sprite class whether or not to use the alpha channel in the sprite. As alpha checking is costly, we set this to false for images that don't have any transparency (such as the game background).m_isSpriteSheet
is a flat that tells the sprite class if a single texture is used to hold all of the frames for this sprite. If set to true
, then each frame is loaded as a separate texture.Sprite
is a constructor that takes a single parameter, p_numberOfTextures
. We have to tell the class the number of textures that will be used when the sprite is created so that the correct amount of memory can be allocated for the textures dynamic array.~Sprite
is the class destructor.Update
will be used to update the current animation frame and the current position of the sprite.Render
will be used to actually display the sprite on the screen.AddTexture
is used once the sprite is created to add the required textures.GetCurrentFrame
is used when the sprite is rendered to determine which frame of the sprite to render.Next, let's start the class implementation. Open Sprite.cpp
and add the following code:
#include "stdafx.h" #include "Sprite.h" #include "SOIL.h" Sprite::Sprite(const unsigned int p_numberOfTextures) { m_textures = new GLuint[p_numberOfTextures]; m_textureIndex = 0; m_currentFrame = 0; m_numberOfFrames = 0; m_animationDelay = 0.25f; m_animationElapsed = 0.0f; m_position.x = 0.0f; m_position.y = 0.0f; m_size.height = 0.0f; m_size.width = 0.0f; m_velocity = 0.0f; m_isCollideable = true; m_flipHorizontal = false; m_flipVertical = false; m_isVisible = false; m_isActive = false; m_isSpriteSheet = false; } Sprite::~Sprite() { delete[] m_textures; }
Here are some details about the implementation code:
stdafx.h
and Sprite.h
, we include SOIL.h
because this is the actual code block that we will use to load texturesSprite
constructor:m_textures
array based on p_numberOfTextures
.false
. The result is that a newly created sprite will not be active or visible until we specifically set it to be active and visible.~Sprite
destructor deallocates the memory used for the m_textures
arrayWe will implement the Update
, Render
, and AddTexture
methods next.
You probably noticed that I prefix many of the variables in my code with either m_
or p_
. m_ is always used to prefix the name of class properties (or member variables), and p_
is used to prefix variables used as parameters in functions. If a variable does not have a prefix, it is usually a local variable.
We already discussed how 2D animations are created by drawing multiple frames of the image with each frame being slightly different. The key points that must be remembered are:
One technique to save your frames is to save each frame as its own image. As you will eventually have a lot of sprites and frames to work with, it is important to come up with a consistent naming convention for all of your images. For example, with our three frame robot animation that were illustrated previously, we might use the following filenames:
robot_left_00.png
robot_left_01.png
robot_left_02.png
robot_left_03.png
robot_right_00.png
robot_right_01.png
robot_right_02.png
robot_right_03.png
Every image in the game should use the same naming mechanism. This will save you endless headaches when coding the animation system.
Let's take a look the code to load a sprite that has each frame saved as an individual file:
robot_right = new Sprite(4); robot_right->SetFrameSize(100.0f, 125.0f); robot_right->SetNumberOfFrames(4); robot_right->SetPosition(0, screen_height - 130.0f); robot_right->AddTexture("resources/robot_right_00.png"); robot_right->AddTexture("resources/robot_right_01.png"); robot_right->AddTexture("resources/robot_right_02.png"); robot_right->AddTexture("resources/robot_right_03.png");
The important points to notice about the preceding code are:
An alternative method to store your sprites is to use a sprite sheet. A sprite sheet holds all of the sprites for a particular animation in a single file. The sprites are often organized into a strip.
As the dimensions of each frame are identical, we can calculate the position of each frame in a particular animation as an offset from the first frame in the sprite sheet.
You can download a cool little program called GlueIt at http://www.varcade.com/blog/glueit-sprite-sheet-maker-download/. This small program allows you to specify several individual images, and then it glues them into a sprite sheet for you.
The following code loads a sprite that has been stored as a sprite sheet:
robot_right_strip = new Sprite(1); robot_right_strip->SetFrameSize(125.0f, 100.0f); robot_right_strip->SetNumberOfFrames(4); robot_right_strip->SetPosition(0, screen_height - 130.0f); robot_right_strip->AddTexture("resources/robot_right_strip.png");
This code is very similar to the code that we used to create a sprite with individual textures previously. However, there are important differences:
The following code shows the full code that we will use to load the sprites into our game. Open the RoboRacer2D project and open RoboRacer.cpp
. First we need to include the Sprite header:
#include "Sprite.h"
Next, we need some global variables to hold our sprites. Add this code in the variable declarations section of the code (before any functions):
Sprite* robot_left; Sprite* robot_right; Sprite* robot_right_strip; Sprite* robot_left_strip; Sprite* background; Sprite* player;
We created pointers for each sprite that we will need in the game until this point:
In order to make it easy for you to work with both types of sprites, I defined two sprites for each robot direction. For example, robot_left
will define a sprite made up of individual textures, while robot_left_strip
will define a sprite made up of a single sprite sheet. Normally, you would not use both in a single game!
Now, add the LoadTextures
function:
const bool LoadTextures() { background = new Sprite(1); background->SetFrameSize(1877.0f, 600.0f); background->SetNumberOfFrames(1); background->AddTexture("resources/background.png", false); robot_right = new Sprite(4); robot_right->SetFrameSize(100.0f, 125.0f); robot_right->SetNumberOfFrames(4); robot_right->SetPosition(0, screen_height - 130.0f); robot_right->AddTexture("resources/robot_right_00.png"); robot_right->AddTexture("resources/robot_right_01.png"); robot_right->AddTexture("resources/robot_right_02.png"); robot_right->AddTexture("resources/robot_right_03.png"); robot_left = new Sprite(4); robot_left->SetFrameSize(100.0f, 125.0f); robot_left->SetNumberOfFrames(4); robot_left->SetPosition(0, screen_height - 130.0f); robot_left->AddTexture("resources/robot_left_00.png"); robot_left->AddTexture("resources/robot_left_01.png"); robot_left->AddTexture("resources/robot_left_02.png"); robot_left->AddTexture("resources/robot_left_03.png"); robot_right_strip = new Sprite(1); robot_right_strip->SetFrameSize(125.0f, 100.0f); robot_right_strip->SetNumberOfFrames(4); robot_right_strip->SetPosition(0, screen_height - 130.0f); robot_right_strip->AddTexture("resources/robot_right_strip.png"); robot_left_strip = new Sprite(1); robot_left_strip->SetFrameSize(125.0f, 100.0f); robot_left_strip->SetNumberOfFrames(4); robot_right_strip->SetPosition(0, screen_height - 130.0f); robot_left_strip->AddTexture("resources/robot_left_strip.png"); background->IsVisible(true); background->IsActive(true); background->SetVelocity(-50.0f); robot_right->IsActive(true); robot_right->IsVisible(true); robot_right->SetVelocity(50.0f); player = robot_right; player->IsActive(true); player->IsVisible(true); player->SetVelocity(50.0f); return true; }
This code is exactly the same as the code that I showed you earlier to load sprites. It is simply more comprehensive:
LoadTexures
loads all of the sprites needed in the game (including duplicate strip versions so that you can see the difference between using sprite sheets versus individual textures).SetPosition
is used to set the initial position for the robot sprites. Notice that we don't do this for the background sprite because its position starts at (0, 0)
, which is the default.SetVisible
and SetActive
are used to set the background
sprite and the robot_left_strip
sprite as active and visible. All of the other sprites will remain inactive and invisible.As the loading of textures only needs to occur once in the game, we will add the call to do this to the StartGame
function. Modify the StartGame
function in RoboRacer.cpp
:
void StartGame() { LoadTextures(); }
The final step in getting our textures loaded is to implement the AddTexture
method in our sprite class. Open Sprite.cpp
and add the following code:
const bool Sprite::AddTexture(const char* p_imageName, const bool p_useTransparency) { GLuint texture = SOIL_load_OGL_texture( p_imageName, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, 0 ); if (texture == 0) { return false; } m_textures[m_textureIndex] = texture; m_textureIndex++; if (m_textureIndex == 1 && m_numberOfFrames > 1) { m_isSpriteSheet= true; } else { m_isSpriteSheet = false; } m_useTransparency = p_useTransparency; return true; }
AddTexture
is used after a new sprite has been created. It adds the required textures to the m_textures
array. Here's how it works:
p_imageName
holds the name and path of the image to load.p_useTransparency
is used to tell the sprite class whether this image uses an alpha channel. As most of our sprites will use transparency, this is coded to default to true
. However, if we set p_useTransparency
to false
, then any transparency information will be ignored.SOIL_load_OGL_texture
does all of the work of loading the texture. The parameters for this call were described earlier in this chapter. Note that SOIL is smart enough to load image types based on the file extension.SOIL_load_OGL_texture
will return an OpenGL texture handle. If not, it will return 0
. Generally, we would test this value and use some kind of error handling, or quit if any texture did not load correctly.m_textures
array is allocated in the constructor, we can simply store texture in the m_textureIndex
slot.m_textureIndex
.m_isSpriteSheet
to true
.m_useTransparency
to the value that was passed in. This will be used later in the Render
method.3.149.240.185