[SDL]GFX with SDL Lesson 3: Sprites by Marius Andra
Hello and welcome to this yet-another-tutorial. This lesson will be about the things that most 2D games use - sprites. In a way sprites are like objects. I'm sure that at some point in your life you have seen or have played Super Mario or some other similar 2D game. Think about all the enemies, blocks, coins, etc. that you saw in that game. All these are called sprites. The main character (Mario) is a sprite, the enemies are sprites, the coins are sprites, the tubes are sprites, even these things that you can stand on are sprites. Everything that impacts the game/player/monster in some way (eg: you can touch it) is basically a sprite. The background is simply a background - you can't touch it in any way. Sometimes some sprites are parts of the background (eg: a sun that is moving/rotating, but can't affect you in any way). Let's call them passive sprites. And let's call the sprites that can affect the player (and the player itself) active sprites. In our little sprite demo that we will create today, we won't seperate active and passive sprites. Altough the vikings could very well be active sprites and the sun a passive sprite. Active sprites need to collide with each other. And if a collision occurs, respond. That's all called collision detection and response. More on it in a future lesson. Besides being active and passive, sprites can also be animated or non-animated. And all sprites can have the possibility to change their x and y location on the screen. Sprite animation doesn't only mean that a sprite moves from point A to point B, but MOVES from point A to point B. Examples of sprite animation can be: a ball rolling (rolling animation while changing the ball position), somebody walking (animation of legs moving while moving the position), a blinking traffic light, a lighthouse, etc, etc.
The sprite system that we will build today will consist of 2 parts: the sprite base, that we will use to store all the frames of the animation, and the sprite itself. The sprite itself is the thingy that moves on the screen. The reason why we have 2 classes in our sprite system is really simple. Usually in a game you have many sprites that look the same. In our sprite classes we will load all the animation data (frames) into the sprite base and then each individual sprite will access it from there. If we would load in all the data (images) with each sprite, then that would boost up the memory requirements a lot.
(Our) sprites can also be partly transparent. The sprite system that we will be using will (currently) only support RGB transparency. That means that some part of the sprite (animation frame image), that is of a crentain color (like full green for example), will be transparent. RGB transparency in SDL is REALLY easy.
This little statement will make some RGB value equal transparency:
SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format, r, g, b));
Where surface is the surface you want to modify and r, g and b are the r, g, b of RGB. Sometimes you may want to remove the transparency after setting it. To do that simply replace SDL_SRCCOLORKEY with 0.
So, let's get coding the sprite classes.
Our 2 sprite classes will also have one helper structure called CSpriteFrame. CSpriteFrame is used to represent each frame of the animation. It contains one SDL_Structure for the image of the frame and one integer pause, that says how many milliseconds to pause after drawing the picture.
struct CSpriteFrame { SDL_Surface *image; int pause; };
So let's start with the sprite base class. It's called CSpriteBase. CSpriteBase has one function, init() to initalize the class - load in all the frames. CSpriteBase also has one pointer to the structure of type CSpriteFrame that we use to store the animation and some more variables that say whether the class is built (via init()), the number of frames in the animation and the width and height of the animation.
class CSpriteBase { public: int init(char *dir); CSpriteFrame *mAnim; int mBuilt, mNumframes, mW, mH; };
The init function is the core of the CSpriteBase class. init() takes one parameter - an array of characters to the directory to get the frame animation from. init() then looks for the file "info" from that directory and reads in some stuff. All the frame images should also be in that same directory that was passed to init.
Ok, now in init we first define 3 char arrays that we will later use to temporarily store some stuff. We will also have 4 (temporary) integers: pause, r, g and b and a FILE pointer fp.
int CSpriteBase::init(char *dir) { char buffer[255]; char filename[255]; char name[255]; int pause=0, r=0, g=0, b=0; FILE *fp;
Now we use the sprintf function. sprintf() is just like printf(), only it prints the output into a string, not the screen. We print the full location of the info file into the string filename and then try to open it.
sprintf(filename, "%s/info", dir); if((fp=fopen(filename, "r")) == NULL) { printf("ERROR opening file %s\n\n", filename); return -1; }
We'll then grab one line from the file into the string buffer. Since we know that this is the first line of the file and that it starts with "FILES: nr", where nr is the number of frames in the animation, we can then get the number using sscanf. sscanf is just like scanf, only it gets the input from a string. We'll store the number of frames in the class member mNumframes. And after all that we'll allocate space for an array or CSpriteFrames.
fgets(buffer, 255, fp); sscanf(buffer, "FILES: %d", &mNumframes); mAnim = new CSpriteFrame[mNumframes];
We'll set mBuilt to 1, so that the sprite classes can see that this class has been built. We will also set a temporary variable that we will use to keep track of the images to be loaded.
mBuilt = 1; int count = 0;
Now we will loop through the info file while the eof (end of file) hasn't been reached and count < mNumframes. We'll grab one line of input and then check whether it's of any use or not (whether it starts with a comment sign, is blank, or not)
while(!feof(fp) && count<mNumframes) { fgets(buffer, 255, fp); if(buffer[0] != '#' && buffer[0] != '\r' && buffer[0] != '\0' && buffer[0] != '\n' && strlen(buffer) != 0) {
Since we now know that the line will be useful, we'll extract the name of the frame image, the milliseconds to pause after displaying it and the r, g and b of the transparent color. We'll then make the string filename equal the full path to the frame file and load it in.
sscanf(buffer, "%s %d %d %d %d", name, &pause, &r, &g, &b); sprintf(filename, "%s/%s", dir, name);
Our image loading routine will be a little different than the one from the previous lesson. First we create a temporary SDL_Surface called temp and we load in the image with SDL_LoadBMP.
SDL_Surface *temp; if((temp = SDL_LoadBMP(filename)) == NULL) return -1;
Now we'll check if the transparent color's r component is greater than or equal to zero. If so, we'll make temp transparent. But if r would be -1 or so, then we wouldn't have made the surface transparent.
if(r >= 0) SDL_SetColorKey(temp, SDL_SRCCOLORKEY, SDL_MapRGB(temp->format, r, g, b));
Now we will make the current frame's image in the array of CSpriteFrames equal the just-loaded image, only in the same format as the screen surface. That makes blitting images onto the screen a little faster. We then free the memory that was held up by the temp surface.
mAnim[count].image = SDL_DisplayFormat(temp); SDL_FreeSurface(temp);
We then make the pause integer in the mAnim array equal the pause value we got from the file. After that we make the width and the height equal the width/height of the frame image if they hadn't already been set.
mAnim[count].pause = pause; if(!mW) mW = mAnim[count].image->w; if(!mH) mH = mAnim[count].image->h; count++; } }
And finally we close the file
fclose(fp); return 0; }
And that's it with CSpriteBase. The sprite class itself is called CSprite. The class header for CSprite will be a little bit bigger than the one of CSpriteBase. First we have some functions. init() is used to initalize the sprite object. It takes 2 parameters: a pointer to a CSpriteBase object and a *SDL_Surface. More on it soon. draw(), clearBG() and updateBG() are all used when drawing the sprite onto the screen. They too will be discussed really shortly.
class CSprite { public: int init(CSpriteBase *base, SDL_Surface *screen); void draw(); void clearBG(); void updateBG();
Now CSprite has some small inline functions as well. setFrame() modifies the integet mFrame, that's used to keep track of what frame to display next and getFrame returns the currently displayed frame.
void setFrame(int nr) { mFrame = nr; } int getFrame() { return mFrame; }
setspeed() and getspeed() set and return the speed of the sprite. The mSpeed variable that these functions set and get is actually multiplied with the pause variable of the current frame. So, the bigger the value is, the longer the pause after drawing a frame lasts. If mSpeed would be 2, then the animation would be 2x slower. If it would be 0.5, then the animation would be 2x faster.
void setSpeed(float nr) { mSpeed = nr; } float getSpeed() { return mSpeed; }
The functions toggleAnim(), startAnim() and stopAnim() toggle, start and stop the animation. rewind() sets the next-to-be-displayed frame variable back to zero.
void toggleAnim() { mAnimating = !mAnimating; } void startAnim() { mAnimating = 1; } void stopAnim() { mAnimating = 0; } void rewind() { mFrame = 0; }
xadd() and yadd() can be used to add some value to the x and y pos of the sprite on the screen. xadd(1) would move the sprite 1 unit right on the screen, while yadd(-3) would move it 3 units up. xset() and yset() manually set the x or y positions of the sprite and set() sets both of them at once.
void xadd(int nr) { mX+=nr; } void yadd(int nr) { mY+=nr; } void xset(int nr) { mX=nr; } void yset(int nr) { mY=nr; } void set(int xx, int yy) { mX=xx; mY=yy; }
Now we have some variables. mFrame is used to keep track of what frame of the animation to display. mX and mY are used to keep track where the sprite is on the screen. mOldX and mOldY are used when the sprite has moved. They are used when clearing the screen. mAnimating tells us whether this sprite is currently animating or not. mDrawn is used to tell whether this sprite has been drawn once on the screen or not. mLastupdate stores the time when the sprite animation was last updated (more on it soon). mSpriteBase is a pointer to the sprite base (all the frame images) for this sprite. mBackreplacement is used to redraw the background after drawing the frame (more soon). And last, but not least, mScreen is a pointer to our screen surface.
private: int mFrame; int mX, mY, mOldX, mOldY; int mAnimating; int mDrawn; float mSpeed; long mLastupdate; CSpriteBase *mSpriteBase; SDL_Surface *mBackreplacement; SDL_Surface *mScreen; };
Now a couple more functions to go through and the CSprire class is built!
The system that we will use when drawing the sprite is simple: we grab the background from the screen and then store it in the SDL_Surface *mBackreplacement. After that we draw the sprite frame onto the screen. And before we draw the next frame, we clear the screen from mBackreplacement. This loop goes on and on for as long as the program runs.
Like with CSpriteBase, init() is used to initalize the class. We pass it a CSpriteBase that has all the frame images and more data in it. The reason why we pass it as a pointer and not by value is really simple: when passing things by value, local copies (inside the CSprite class) are created of them, thus needing more memory. The second parameter to init() is the SDL_Surface screen. We pass it along since we need something to draw on. Now in init() we make the class member mSpriteBase equal the passed sprite base "base". And if the sprite base has been built, then we do some stuff. We first check if there are more than one frame in the animation. And if so, we set mAnimating to 1, making the sprite animate. We also make mBackreplacement equal the first frame's SDL_Surface. We use SDL_DisplayFormat since it's a nice and easy way to copy a surface. And at the end we make mScreen, the class member for the screen surface, equal the screen surface so that the sprite can later draw to the screen.
int CSprite::init(CSpriteBase *base, SDL_Surface *screen) { mSpriteBase = base; if(mSpriteBase->mBuilt) { if(mSpriteBase->mNumframes>1) mAnimating=1; mBackreplacement = SDL_DisplayFormat(mSpriteBase->mAnim[0].image); } mScreen = screen; }
Now let's start with clearBG(). If the sprite has been drawn once before, then we can clear the screen. To do this we blit the surface mBackreplacement onto the screen at [mOldX x mOldY]. We blit it to mOld[X/Y] so that we could clear the exact spot that we draw the sprite to.
void CSprite::clearBG() { if(mDrawn==1) { SDL_Rect dest; dest.x = mOldX; dest.y = mOldY; dest.w = mSpriteBase->mW; dest.h = mSpriteBase->mH; SDL_BlitSurface(mBackreplacement, NULL, mScreen, &dest); } }
Now with updateBG() we grab an area from the screen at [mX x mY] and store it inside mBackreplacement. See lesson 2 for an explanation on what we are doing exactly here. Then we make mOldX equal mX and mOldY equal mY so that the next call to clearBG() will know where to clear.
void CSprite::updateBG() { SDL_Rect srcrect; srcrect.w = mSpriteBase->mW; srcrect.h = mSpriteBase->mH; srcrect.x = mX; srcrect.y = mY; mOldX=mX;mOldY=mY; SDL_BlitSurface(mScreen, &srcrect, mBackreplacement, NULL); }
And now the final CSprite function: draw(). As you may have guessed it, draw() draws the sprite on the screen. The first thing that we do in draw() is check whether we need to increase the variable mFrame that keeps track of what frame to draw. We use SDL_GetTicks() as the time function. SDL_GetTicks() returns the number of milliseconds since the SDL library initalization - the start of your program. You should also note that the value returned from SDL_GetTicks() wraps if the program runs for more than ~49 days. We check if the time of the last update + the number of milliseconds to pause after this frame is less than the current time. And if so then we increase mFrame. If mFrame would be larger than the number of frames - 1, then we would make it equal zero for continious looping animation. And finally we make mLastupdate (the time of the last frame change) equal the number of milliseconds from the start of the program.
void CSprite::draw() { if(mAnimating == 1) { if(mLastupdate+mSpriteBase->mAnim[mFrame].pause*mSpeed< SDL_GetTicks()) { mFrame++; if(mFrame>mSpriteBase->mNumframes-1) mFrame=0; mLastupdate = SDL_GetTicks(); } }
If this would be the first time this function is called then we would make mDrawn equal 0 so that the next call to clearBG() would actually clear it. And then we just draw the sprite.
if(mDrawn==0) mDrawn=1; SDL_Rect dest; dest.x = mX; dest.y = mY; SDL_BlitSurface(mSpriteBase->mAnim[mFrame].image, NULL, mScreen, &dest); }
And that, my friend, is all about the sprite classes. Now let's get to the non-sprite-class part of the program.
After all the #includes come some global variables. First come screen and back. You can probably guess what they do. Then we have 2 CSpriteBases: vikingbase and sunbase. These will contain the viking animation images and the sun animation images. Next come 3 CSprites: vikings1, vikings2 and sun.
SDL_Surface *screen, *back; CSpriteBase vikingbase; CSpriteBase sunbase; CSprite vikings1; CSprite vikings2; CSprite sun;
This function should be easy to grasp. It's the same function as in the previous lesson, only we do the SDL_DisplayFormat() speedup. More about it if you scroll up a bit.
SDL_Surface * ImageLoad(char *file) { SDL_Surface *temp1, *temp2; temp1 = SDL_LoadBMP(file); temp2 = SDL_DisplayFormat(temp1); SDL_FreeSurface(temp1); return temp2; }
InitImages() simply loads in the background images.
int InitImages() { back = ImageLoad("data/bg.bmp"); return 0; }
DrawIMG draws one image onto the screen at a given x and y location.
void DrawIMG(SDL_Surface *img, int x, int y) { SDL_Rect dest; dest.x = x; dest.y = y; SDL_BlitSurface(img, NULL, screen, &dest); }
DrawBG() fills the screen with the background.
void DrawBG() { DrawIMG(back, 0, 0); }
Now in DrawScene we draw all the sprites. We first clear the background, then let the sprites update themself and then finally draw them. If you are wondering, why I have the clearBG()'s, updateBG()'s and draw()'s grouped, then group them by their name (sun.clearBG(); sun.updateBG(); sun.draw(); vikings1.clearBG() ...) and see for yourself.
void DrawScene() { sun.clearBG(); vikings1.clearBG(); vikings2.clearBG(); sun.updateBG(); vikings1.updateBG(); vikings2.updateBG(); sun.draw(); vikings1.draw(); vikings2.draw(); SDL_Flip(screen); }
Now let's get to main(). The first part of it should be quite familiar. If not, then simply read the previous lessons.
int main(int argc, char *argv[]) { Uint8* keys; if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) { printf("Unable to init SDL: %s\n", SDL_GetError()); exit(1); } atexit(SDL_Quit); screen=SDL_SetVideoMode(640,480,32, SDL_SWSURFACE|SDL_FULLSCREEN|SDL_HWPALETTE); if ( screen == NULL ) { printf("Unable to set 640x480 video: %s\n", SDL_GetError()); exit(1); }
Now we initalize our CSpriteBases. We initalize vikingbase with the directory "data/vikings" that will be used to read the frames from and sunbase with "data/sun".
vikingbase.init("data/vikings"); sunbase.init("data/sun");
Now we initalize the sun itself. We make it of type sunbase. (For fun make it of type vikingbase, or vice versa - make the vikings of type sunbase) Then we set it's position to 480x50 and speed to 1. We do similar things with the vikings.
sun.init(&sunbase,screen); sun.set(480,50); sun.setSpeed(1); vikings1.init(&vikingbase,screen); vikings1.set(150,300); vikings1.setSpeed(1); vikings2.init(&vikingbase,screen); vikings2.set(350,300); vikings2.setSpeed(1.5);
Now we hide the mouse cursor, init the images and draw the background.
SDL_ShowCursor(0); InitImages(); DrawBG();
Now in the main loop we check for events
int done=0; while(done == 0) { SDL_Event event; while ( SDL_PollEvent(&event) ) { if ( event.type == SDL_QUIT ) { done = 1; } if ( event.type == SDL_KEYDOWN ) { if ( event.key.keysym.sym == SDLK_ESCAPE ) { done = 1; } if ( event.key.keysym.sym ==SDLK_SPACE){sun.toggleAnim();} } } keys = SDL_GetKeyState(NULL);
And in case of some arrow presses, move the first vikings around.
if ( keys[SDLK_UP] ) { vikings1.yadd(-1); } if ( keys[SDLK_DOWN] ) { vikings1.yadd(1); } if ( keys[SDLK_LEFT] ) { vikings1.xadd(-1); } if ( keys[SDLK_RIGHT] ) { vikings1.xadd(1); }
Draw the scene
DrawScene(); }
And exit!
return 0; }
And all that's left now is to give you the source. And here it is. (Linux users click here and type in "tar -zxf lesson3.tar.gz; cd lesson3; make; ./lesson3;" after you download the package. Thanx to Jens Rantil!)
Oh, and the vikings and the sun were taken from www.animfactory.com.