If Chapter 7 provided the foundation for developing bitmap-based games, then Chapter 8 provided the frame, walls, plumbing, and wiring. (House analogies are frequently used to describe software development, so it may be used to describe game programming as well.) Therefore, what you need from this chapter is the sheetrock, finishing, paint, stucco, roof tiles, appliances, and all the cosmetic accessories that complete a new house—yes, including the kitchen sink. Understanding animated sprites is absolutely crucial in your quest to master the subject of 2D game programming. So what is an animated sprite? You already learned a great deal about sprites in the last chapter, and you have at your disposal a good tool set for loading and blitting sprites (which are just based on common bitmaps). An animated sprite, then, is an array of sprites, drawn using new properties, such as timing, direction, and velocity. Here is what this chapter covers:
Grabbing frames out of a sprite sheet
Working with many sprites
The sprites you have seen thus far were handled somewhat haphazardly, in that no real structure was available for keeping track of these sprites. They have simply been loaded using load_bitmap
and then drawn using draw_sprite
, with little else in the way of control or handling. To really be able to work with animated sprites in a highly complex game (such as a high-speed scrolling shooter like R-Type or Mars Matrix), you need a framework for drawing, erasing, and moving these sprites, and for detecting collisions. For all of its abstraction, Allegro leaves this entirely up to you—and for good reason. No single person can foresee the needs of another game programmer because every game has a unique set of requirements (more or less). Limiting another programmer (who may be far more talented than you) to using your concept of a sprite handler only encourages that person to ignore your handler and write his own. That is exactly why Allegro has no sprite handler; rather, it simply has a great set of low-level sprite routines, the likes of which you have already seen.
What should you do next, then? The real challenge is not designing a handler for working with animated sprites; rather, it is designing a game that will need these animated sprites, and then writing the code to fulfill the needs of the game. In this case, the game I am targeting for the sprite handler is Tank War, which you have improved several times already—and this one will be no exception. In Chapter 8, you modified Tank War extensively to convert it from a vector- and bitmap-based game into a sprite-based game, losing some gameplay along the way. (The battlefield obstacles were removed.) At the end of this chapter, you'll add the sprite handler and collision detection—finally!
To get started, you need a simple example followed by an explanation of how it works. I have written a quick little program that loads six images (of an animated cat) and draws them on the screen. The cat runs across the screen from left to right, using the sprite frames shown in Figure 9.1.
The AnimSprite program loads these six image files, each containing a single frame of the animated cat, and draws them in sequence, one frame after another, as the sprite is moved across the screen (see Figure 9.2).
#include <stdlib.h> #include <stdio.h> #include <allegro.h> #define WHITE makecol(255,255,255) #define BLACK makecol(0,0,0) BITMAP *kitty[7]; char s[20]; int curframe=0, framedelay=5, framecount=0; int x=100, y=200, n; int main(void) { //initialize the program allegro_init(); install_keyboard(); install_timer(); set_color_depth(16); set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0); textout_ex(screen, font, "AnimSprite Program (ESC to quit)", 0, 0, WHITE, 0); //load the animated sprite for (n=0; n<6; n++) { sprintf(s,"cat%d.bmp",n+1); kitty[n] = load_bitmap(s, NULL); } //main loop while(!keypressed()) { //erase the sprite rectfill(screen, x, y, x+kitty[0]->w, y+kitty[0]->h, BLACK); //update the position x += 5; if (x > SCREEN_W - kitty[0]->w) x = 0; //update the frame if (framecount++ > framedelay) { framecount = 0; curframe++; if (curframe > 5) curframe = 0; } acquire_screen(); //draw the sprite draw_sprite(screen, kitty[curframe], x, y); //display logistics textprintf_ex(screen, font, 0, 20, WHITE, 0, "Sprite X,Y: %3d,%3d", x, y); textprintf_ex(screen, font, 0, 40, WHITE, 0, "Frame,Count,Delay: %2d,%2d,%2d", curframe, framecount, framedelay); release_screen(); rest(10); } allegro_exit(); return 0; } END_OF_MAIN()
Now for that explanation, as promised. The difference between AnimSprite and DrawSprite (from the previous chapter) is multifaceted. The key variables, curframe
, framecount
, and framedelay
, make realistic animation possible. You don't want to simply change the frame every time through the loop, or the animation will be too fast. The frame delay is a static value that really needs to be adjusted depending on the speed of your computer (at least until I cover timers in Chapter 11, “Programming the Perfect Game Loop”). The frame counter, then, works with the frame delay to increment the current frame of the sprite. The actual movement of the sprite is a simple horizontal motion using the x
variable.
//update the frame if (framecount++ > framedelay) { framecount = 0; curframe++; if (curframe > 5) curframe = 0; }
A really well thought-out sprite handler will have variables for both the position (x, y) and velocity (x speed, y speed), along with a velocity delay to allow some sprites to move quite slowly compared to others. If there is no velocity delay, each sprite will move at least one pixel during each iteration of the game loop (unless velocity is zero, which means that sprite is motionless).
//update the position x += 5; if (x > SCREEN_W - kitty[0]->w) x = 0;
Now that you have a basic—if a bit rushed—concept of sprite animation, I'd like to walk you through the creation of a sprite handler and a sample program with which to test it. Now you'll take the animation code from the last few pages and encapsulate it into a struct. The actual bitmap images for the sprite are stored separately from the sprite struct because it is more flexible that way.
In addition to those few animation variables seen in AnimSprite, a full-blown animated sprite handler needs to track several more variables. Here is the struct:
typedef struct SPRITE { int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE;
We'll take all of the code in this chapter, including the struct and functions, and turn it into a sprite class in the next chapter. The great thing about a class is that your functions don't need as many parameters, because the variables are stored internally in the class.
The variables inside a struct are called struct elements, so I will refer to them as such (see Figure 9.3).
The first two elements (x
, y
) track the sprite's position. The next two (width
, height
) are set to the size of the sprite image (stored outside the struct). The velocity elements (xspeed
, yspeed
) determine how many pixels the sprite will move in conjunction with the velocity delay (xdelay
, xcount
and ydelay
, ycount
). The velocity delay allows some sprites to move much slower than other sprites on the screen—even more slowly than one pixel per frame. This gives you a far greater degree of control over how a sprite behaves. The animation elements (curframe
, maxframe
, animdir
) help the sprite animation, and the animation delay elements (framecount
, framedelay
) help slow down the animation rate. The animdir
element is of particular interest because it allows you to reverse the direction that the sprite frames are drawn (from 0 to maxframe
or from maxframe
to 0, with looping in either direction). The main reason why the BITMAP
array containing the sprite images is not stored inside the struct is because that is wasteful—there might be many sprites sharing the same animation images.
Now that we have a sprite struct, the actual handler is contained in a function that I will call updatesprite
:
void updatesprite(SPRITE *spr) { //update x position if (++spr->xcount > spr->xdelay) { spr->xcount = 0; spr->x += spr->xspeed; } //update y position if (++spr->ycount > spr->ydelay) { spr->ycount = 0; spr->y += spr->yspeed; } //update frame based on animdir if (++spr->framecount > spr->framedelay) { spr->framecount = 0; if (spr->animdir == -1) { if (--spr->curframe < 0) spr->curframe = spr->maxframe; } else if (spr->animdir == 1) { if (++spr->curframe > spr->maxframe) spr->curframe = 0; } } }
As you can see, updatesprite
accepts a pointer to a SPRITE
variable. A pointer is necessary because elements of the struct are updated inside this function. This function would be called at every iteration through the game loop because the sprite elements should be closely tied to the game loop and timing of the game. The delay elements in particular should rely upon regular updates using a timed game loop. The animation section checks animdir
to increment or decrement the framecount
element.
However, updatesprite
was not designed to affect sprite behavior, only to manage the logistics of sprite movement. After updatesprite
has been called, you want to deal with that sprite's behavior within the game. For instance, if you are writing a space-based shooter featuring a spaceship and objects (such as asteroids) that the ship must shoot, then you might assign a simple warping behavior to the asteroids so that when they exit one side of the screen, they will appear at the opposite side. Or, in a more realistic game featuring a larger scrolling background, the asteroids might warp or bounce at the edges of the universe rather than just the screen. In that case, you would call updatesprite
followed by another function that affects the behavior of all asteroids in the game and rely on custom or random values for each asteroid's struct elements to cause it to behave slightly differently than the other asteroids, but basically follow the same behavioral rules. Too many programmers ignore the concept of behavior and simply hard-code behaviors into a game.
I love the idea of constructing many behavior functions and then using them in a game at random times to keep the player guessing what will happen next. For instance, a simple behavior that I often use in example programs is to have a sprite bounce off the edges of the screen. This could be abstracted into a bounce behavior if you go that one extra step in thinking and design it as a reusable function.
One thing that must be obvious when you are working with a real sprite handler is that it seems to have a lot of overhead, in that the struct elements must all be set at startup. There's no getting around that unless you want total chaos instead of a working game! You have to give all your sprites their starting values to make the game function as planned. Stuffing those variables into a struct helps to keep the game manageable when the source code starts to grow out of control (which frequently happens when you have a truly great game idea and you follow through with building it).
I have written a program called SpriteHandler that demonstrates how to put all this together into a workable program that you can study. This program uses a ball sprite with 16 frames of animation, each stored in a file (ball1.bmp, ball2.bmp, and so on to ball16.bmp). One thing that you must do is learn how to store an animation sequence inside a single bitmap image and grab the frames out of it at runtime so that so many bitmap files will not be necessary. I'll show you how to do that shortly. Figure 9.4 shows the SpriteHandler program running. Each time the ball hits the edge it changes direction and speed.
#include <stdlib.h> #include <stdio.h> #include <allegro.h> #define BLACK makecol(0,0,0) #define WHITE makecol(255,255,255) //define the sprite structure typedef struct SPRITE { int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE; //sprite variables BITMAP *ballimg[16]; SPRITE theball; SPRITE *ball = &theball; //support variables char s[20]; int n; void erasesprite(BITMAP *dest, SPRITE *spr) { //erase the sprite using BLACK color fill rectfill(dest, spr->x, spr->y, spr->x + spr->width, spr->y + spr->height, BLACK); } void updatesprite(SPRITE *spr) { //update x position if (++spr->xcount > spr->xdelay) { spr->xcount = 0; spr->x += spr->xspeed; } //update y position if (++spr->ycount > spr->ydelay) { spr->ycount = 0; spr->y += spr->yspeed; } //update frame based on animdir if (++spr->framecount > spr->framedelay) { spr->framecount = 0; if (spr->animdir == -1) { if (--spr->curframe < 0) spr->curframe = spr->maxframe; } else if (spr->animdir == 1) { if (++spr->curframe > spr->maxframe) spr->curframe = 0; } } } void bouncesprite(SPRITE *spr) { //simple screen bouncing behavior if (spr->x < 0) { spr->x = 0; spr->xspeed = rand() % 2 + 4; spr->animdir *= -1; } else if (spr->x > SCREEN_W - spr->width) { spr->x = SCREEN_W - spr->width; spr->xspeed = rand() % 2 - 6; spr->animdir *= -1; } if (spr->y < 40) { spr->y = 40; spr->yspeed = rand() % 2 + 4; spr->animdir *= -1; } else if (spr->y > SCREEN_H - spr->height) { spr->y = SCREEN_H - spr->height; spr->yspeed = rand() % 2 - 6; spr->animdir *= -1; } } int main(void) { //initialize allegro_init(); set_color_depth(16); set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0); install_keyboard(); install_timer(); srand(time(NULL)); textout_ex(screen, font, "SpriteHandler Program (ESC to quit)", 0, 0, WHITE, 0); //load sprite images for (n=0; n<16; n++) { sprintf(s,"ball%d.bmp",n+1); ballimg[n] = load_bitmap(s, NULL); } //initialize the sprite with lots of randomness ball->x = rand() % (SCREEN_W - ballimg[0]->w); ball->y = rand() % (SCREEN_H - ballimg[0]->h); ball->width = ballimg[0]->w; ball->height = ballimg[0]->h; ball->xdelay = rand() % 2 + 1; ball->ydelay = rand() % 2 + 1; ball->xcount = 0; ball->ycount = 0; ball->xspeed = rand() % 2 + 4; ball->yspeed = rand() % 2 + 4; ball->curframe = 0; ball->maxframe = 15; ball->framecount = 0; ball->framedelay = rand() % 3 + 1; ball->animdir = 1; //game loop while (!key[KEY_ESC]) { erasesprite(screen, ball); //perform standard position/frame update updatesprite(ball); //now do something with the sprite--a basic screen bouncer bouncesprite(ball); //lock the screen acquire_screen(); //draw the ball sprite draw_sprite(screen, ballimg[ball->curframe], ball->x, ball->y); //display some logistics textprintf_ex(screen, font, 0, 20, WHITE, 0, "x,y,xspeed,yspeed: %2d,%2d,%2d,%2d", ball->x, ball->y, ball->xspeed, ball->yspeed); textprintf_ex(screen, font, 0, 30, WHITE, 0, "xcount,ycount,framecount,animdir: %2d,%2d,%2d,%2d", ball->xcount, ball->ycount, ball->framecount, ball->animdir); //unlock the screen release_screen(); rest(10); } for (n=0; n<15; n++) destroy_bitmap(ballimg[n]); allegro_exit(); return 0; } END_OF_MAIN()
In case you haven't yet noticed, the idea behind the sprite handler that you're building in this chapter is not to encapsulate Allegro's already excellent sprite functions (which were covered in the previous chapter). The temptation of nearly every C++ programmer would be to drool in anticipation over encapsulating Allegro into a series of classes. I can understand classing up an operating system service, which is vague and obscure, to make it easier to use. In my opinion, a class should be used to simplify very complex code, not to make simple code more complex just to satisfy an obsessive-compulsive need to do so.
On the contrary, you want to use the existing functionality of Allegro, not replace it with something else. By “something else” I mean not necessarily better, just different. The wrapping of one thing and turning it into another thing should arise out of use, not compulsion. Add new functions (or in the case of C++, new classes, properties, and methods) as you need them, not from some grandiose scheme of designing a library before using it. For this reason, we'll write a C++ sprite class in the next chapter when the sprite functionality starts to become too complex for a simple struct.
Thus, you have a basic sprite handler and now you need a function to grab an animation sequence out of a tiled image. So you can get an idea of what I'm talking about, Figure 9.5 shows a 32-frame tiled animation sequence in a file called sphere.bmp.
Figure 9.5. This bitmap image contains 32 frames of an animated sphere used as a sprite. Courtesy of Edgar Ibarra.
The frames would be easy to capture if they were lined up in a single row, so how would you grab them out of this file with eight columns and four rows? It's easy if you have the sprite tile algorithm. I'm sure someone described this in some mathematics or computer graphics book at one time or another in the past; I derived it on my own years ago. I suggest you print this simple algorithm in a giant font and paste it on the wall above your computer—or better yet, have a T-shirt made with it pasted across the front.
int x = startx + (frame % columns) * width; int y = starty + (frame / columns) * height;
Using this algorithm, you can grab an animation sequence that is stored in a bitmap file, even if it contains more than one animation. (For instance, some simpler games might store all the images in a single bitmap file and grab each sprite at runtime.) Now that you have the basic algorithm, here's a full function for grabbing a single frame out of an image by passing the width, height, column, and frame number:
BITMAP *grabframe(BITMAP *source, int width, int height, int startx, int starty, int columns, int frame) { BITMAP *temp = create_bitmap(width,height); int x = startx + (frame % columns) * width; int y = starty + (frame / columns) * height; blit(source,temp,x,y,0,0,width,height); return temp; }
Note that grabframe
doesn't destroy the temp
bitmap after blitting the frame image to it. That is because the smaller temp
bitmap is the return value for the function. It is up to the caller (usually main
) to destroy the bitmap after it is no longer needed—or just before the game ends.
The grabframe
function really should have some error detection code built in, such as a check for whether the bitmap is NULL
after blitting it. As a matter of fact, all the code in this book is intentionally simplistic—with no error detection code—to make it easier to study. In an actual game, you would absolutely want to add checks in your code.
The SpriteGrabber program demonstrates how to use grabframe
by modifying the SpriteHandler program and using a more impressive animated sprite that was rendered (courtesy of Edgar Ibarra). See Figure 9.6 for a glimpse of the program.
Figure 9.6. The SpriteGrabber program demonstrates how to grab sprite images (or animation frames) from a sprite sheet.
I'm going to list the entire source code for SpriteGrabber and set in boldface the lines that have changed (or been added) so you can note the differences. I believe it would be too confusing to list only the changes to the program. There is a significant learning experience to be had by observing the changes or improvements to a program from one revision to the next.
#include <stdlib.h> #include <stdio.h> #include "allegro.h" #define BLACK makecol(0,0,0) #define WHITE makecol(255,255,255) //define the sprite structure typedef struct SPRITE { int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE; //sprite variables BITMAP *ballimg[32]; SPRITE theball; SPRITE *ball = &theball; int n; void erasesprite(BITMAP *dest, SPRITE *spr) { //erase the sprite using BLACK color fill rectfill(dest, spr->x, spr->y, spr->x + spr->width, spr->y + spr->height, BLACK); } void updatesprite(SPRITE *spr) { //update x position if (++spr->xcount > spr->xdelay) { spr->xcount = 0; spr->x += spr->xspeed; } //update y position if (++spr->ycount > spr->ydelay) { spr->ycount = 0; spr->y += spr->yspeed; } //update frame based on animdir if (++spr->framecount > spr->framedelay) { spr->framecount = 0; if (spr->animdir == -1) { if (--spr->curframe < 0) spr->curframe = spr->maxframe; } else if (spr->animdir == 1) { if (++spr->curframe > spr->maxframe) spr->curframe = 0; } } } void bouncesprite(SPRITE *spr) { //simple screen bouncing behavior if (spr->x < 0) { spr->x = 0; spr->xspeed = rand() % 2 + 4; spr->animdir *= -1; } else if (spr->x > SCREEN_W - spr->width) { spr->x = SCREEN_W - spr->width; spr->xspeed = rand() % 2 - 6; spr->animdir *= -1; } if (spr->y < 40) { spr->y = 40; spr->yspeed = rand() % 2 + 4; spr->animdir *= -1; } else if (spr->y > SCREEN_H - spr->height) { spr->y = SCREEN_H - spr->height; spr->yspeed = rand() % 2 - 6; spr->animdir *= -1; } } BITMAP *grabframe(BITMAP *source, int width, int height, int startx, int starty, int columns, int frame) { BITMAP *temp = create_bitmap(width,height); int x = startx + (frame % columns) * width; int y = starty + (frame / columns) * height; blit(source,temp,x,y,0,0,width,height); return temp; } int main(void) { BITMAP *temp; //initialize allegro_init(); set_color_depth(16); set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0); install_keyboard(); install_timer(); srand(time(NULL)); textout_ex(screen, font, "SpriteGrabber Program (ESC to quit)", 0, 0, WHITE, 0); //load 32-frame tiled sprite image temp = load_bitmap("sphere.bmp", NULL); for (n=0; n<32; n++) { ballimg[n] = grabframe(temp,64,64,0,0,8,n); } destroy_bitmap(temp); //initialize the sprite with lots of randomness ball->x = rand() % (SCREEN_W - ballimg[0]->w); ball->y = rand() % (SCREEN_H - ballimg[0]->h); ball->width = ballimg[0]->w; ball->height = ballimg[0]->h; ball->xdelay = rand() % 2 + 1; ball->ydelay = rand() % 2 + 1; ball->xcount = 0; ball->ycount = 0; ball->xspeed = rand() % 2 + 4; ball->yspeed = rand() % 2 + 4; ball->curframe = 0; ball->maxframe = 31; ball->framecount = 0; ball->framedelay = 1; ball->animdir = 1; //game loop while (!key[KEY_ESC]) { erasesprite(screen, ball); //perform standard position/frame update updatesprite(ball); //now do something with the sprite--a basic screen bouncer bouncesprite(ball); //lock the screen acquire_screen(); //draw the ball sprite draw_sprite(screen, ballimg[ball->curframe], ball->x, ball->y); //display some logistics textprintf_ex(screen, font, 0, 20, WHITE, 0, "x,y,xspeed,yspeed: %2d,%2d,%2d,%2d", ball->x, ball->y, ball->xspeed, ball->yspeed); textprintf_ex(screen, font, 0, 30, WHITE, 0, "xcount,ycount,framecount,animdir: %2d,%2d,%2d,%2d", ball->xcount, ball->ycount, ball->framecount, ball->animdir); //unlock the screen release_screen(); rest(10); } for (n=0; n<31; n++) destroy_bitmap(ballimg[n]); allegro_exit(); return 0; } END_OF_MAIN()
You might think of a single sprite as a single-dimensional point in space (thinking in the terms of geometry). An animated sprite containing multiple images for a single sprite is a two-dimensional entity. The next step, creating multiple copies of the sprite, might be compared to the third dimension. So far you have only dealt with and explored the concepts around a single sprite being drawn on the screen either with a static image or with an animation sequence. But how many games feature only a single sprite? It is really a test of the sprite handler to see how well it performs when it must contend with many sprites at the same time.
Because performance will be a huge issue with multiple sprites, I will use a double-buffer in the upcoming program for a nice, clean screen without flicker. I will add another level of complexity to make this even more interesting—dealing with a bitmapped background image instead of a blank background. The rectfill
will no longer suffice to erase the sprites during each refresh; instead, the background will have to be restored under the sprites as they move around.
Instead of a single sprite struct there is an array of sprite structs, and the code throughout the program has been modified to use the array. To initialize all of these sprites, you need to use a loop and make sure each pointer is pointing to each of the sprite structs.
//initialize the sprite
for (n=0; n<MAX; n++)
{
sprites[n] = &thesprites[n];
sprites[n]->x = rand() % (SCREEN_W - spriteimg[0]->w);
sprites[n]->y = rand() % (SCREEN_H - spriteimg[0]->h);
sprites[n]->width = spriteimg[0]->w;
sprites[n]->height = spriteimg[0]->h;
sprites[n]->xdelay = rand() % 3 + 1;
sprites[n]->ydelay = rand() % 3 + 1;
sprites[n]->xcount = 0;
sprites[n]->ycount = 0;
sprites[n]->xspeed = rand() % 8 - 5;
sprites[n]->yspeed = rand() % 8 - 5;
sprites[n]->curframe = rand() % 64;
sprites[n]->maxframe = 63;
sprites[n]->framecount = 0;
sprites[n]->framedelay = rand() % 5 + 1;
sprites[n]->animdir = rand() % 3 - 1;
}
This time I'm using a much larger animation sequence containing 64 frames, as shown in Figure 9.7. The source frames are laid out in an 8 × 8 grid of tiles.
To load these frames into the sprite handler, a loop is used to grab each frame individually.
//load 64-frame tiled sprite image temp = load_bitmap("asteroid.bmp", NULL); for (n=0; n<64; n++) { spriteimg[n] = grabframe(temp,64,64,0,0,8,n); } destroy_bitmap(temp);
The MultipleSprites program animates 100 sprites on the screen, each of which has 64 frames of animation! Had this program tried to store the actual images with every single sprite instead of sharing the sprite images, it would have taken a huge amount of system memory to run—so now you see the wisdom in storing the images separately from the structs. Figure 9.8 shows the MultipleSprites program running at 1024 × 768. This program differs from SpriteGrabber because it uses a screen warp rather than a screen bounce behavior.
This program uses a second buffer to improve performance. Could you imagine the speed hit after erasing and drawing 100 sprites directly on the screen? Even locking and unlocking the screen wouldn't help much with so many writes taking place on the screen. That is why this program uses double buffering—so all blitting is done on the second buffer, which is then quickly blitted to the screen with a single function call.
//update the screen acquire_screen(); blit(buffer,screen,0,0,0,0,buffer->w,buffer->h); release_screen();
The game loop in MultipleSprites might look inefficient at first glance because there are four identical for
loops for each operation—erasing, updating, warping, and drawing each of the sprites.
//erase the sprites for (n=0; n<MAX; n++) erasesprite(buffer, sprites[n]); //perform standard position/frame update for (n=0; n<MAX; n++) updatesprite(sprites[n]); //apply screen warping behavior for (n=0; n<MAX; n++) warpsprite(sprites[n]); //draw the sprites for (n=0; n<MAX; n++) draw_sprite(buffer, spriteimg[sprites[n]->curframe], sprites[n]->x, sprites[n]->y);
It might seem more logical to use a single for
loop with these functions inside that loop instead, right? Unfortunately, that is not the best way to handle sprites. First, all of the sprites must be erased before anything else happens. Second, all of the sprites must be moved before any are drawn or erased. Finally, all of the sprites must be drawn at the same time, or else artifacts will be left on the screen. Had I simply blasted the entire background onto the buffer to erase the sprites, this would have been a moot point. The program might even run faster than erasing 100 sprites individually. However, this is a learning experience. It's not always practical to clear the entire background, and this is just a demonstration—you won't likely have 100 sprites on the screen at once unless you are building a very complex scrolling arcade shooter or strategy game.
Following is the complete listing for the MultipleSprites program. If you are typing in the code directly from the book, you will want to grab the aster-oids.bmp and ngc604.bmp files from the CD-ROM.
#include <stdlib.h> #include <stdio.h> #include <allegro.h> #define BLACK makecol(0,0,0) #define WHITE makecol(255,255,255) #define NUMSPRITES100 #define WIDTH 640 #define HEIGHT 480 #define MODE GFX_AUTODETECT_WINDOWED //define the sprite structure typedef struct SPRITE { int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE; //variables BITMAP *spriteimg[64]; SPRITE thesprites[MAX]; SPRITE *sprites[MAX]; BITMAP *back; void erasesprite(BITMAP *dest, SPRITE *spr) { //erase the sprite blit(back, dest, spr->x, spr->y, spr->x, spr->y, spr->width, spr->height); } void updatesprite(SPRITE *spr) { //update x position if (++spr->xcount > spr->xdelay) { spr->xcount = 0; spr->x += spr->xspeed; } //update y position if (++spr->ycount > spr->ydelay) { spr->ycount = 0; spr->y += spr->yspeed; } //update frame based on animdir if (++spr->framecount > spr->framedelay) { spr->framecount = 0; if (spr->animdir == -1) { if (--spr->curframe < 0) spr->curframe = spr->maxframe; } else if (spr->animdir == 1) { if (++spr->curframe > spr->maxframe) spr->curframe = 0; } } } void warpsprite(SPRITE *spr) { //simple screen warping behavior if (spr->x < 0) { spr->x = SCREEN_W - spr->width; } else if (spr->x > SCREEN_W - spr->width) { spr->x = 0; } if (spr->y < 40) { spr->y = SCREEN_H - spr->height; } else if (spr->y > SCREEN_H - spr->height) { spr->y = 40; } } BITMAP *grabframe(BITMAP *source, int width, int height, int startx, int starty, int columns, int frame) { BITMAP *temp = create_bitmap(width,height); int x = startx + (frame % columns) * width; int y = starty + (frame / columns) * height; blit(source,temp,x,y,0,0,width,height); return temp; } int main(void) { BITMAP *temp, *buffer; int n; //initialize allegro_init(); set_color_depth(16); set_gfx_mode(MODE, WIDTH, HEIGHT, 0, 0); install_keyboard(); install_timer(); srand(time(NULL)); //create second buffer buffer = create_bitmap(SCREEN_W, SCREEN_H); //load & draw the background back = load_bitmap("ngc604.bmp", NULL); stretch_blit(back, buffer, 0, 0, back->w, back->h, 0, 0, SCREEN_W, SCREEN_H); //resize background to fit the variable-size screen destroy_bitmap(back); back = create_bitmap(SCREEN_W,SCREEN_H); blit(buffer,back,0,0,0,0,buffer->w,buffer->h); textout_ex(buffer, font, "MultipleSprites Program (ESC to quit)", 0, 0, WHITE, 0); //load 64-frame tiled sprite image temp = load_bitmap("asteroid.bmp", NULL); for (n=0; n<64; n++) { spriteimg[n] = grabframe(temp,64,64,0,0,8,n); } destroy_bitmap(temp); //initialize the sprite for (n=0; n<NUMSPRITES; n++) { sprites[n] = &thesprites[n]; sprites[n]->x = rand() % (SCREEN_W - spriteimg[0]->w); sprites[n]->y = rand() % (SCREEN_H - spriteimg[0]->h); sprites[n]->width = spriteimg[0]->w; sprites[n]->height = spriteimg[0]->h; sprites[n]->xdelay = rand() % 3 + 1; sprites[n]->ydelay = rand() % 3 + 1; sprites[n]->xcount = 0; sprites[n]->ycount = 0; sprites[n]->xspeed = rand() % 8 - 5; sprites[n]->yspeed = rand() % 8 - 5; sprites[n]->curframe = rand() % 64; sprites[n]->maxframe = 63; sprites[n]->framecount = 0; sprites[n]->framedelay = rand() % 5 + 1; sprites[n]->animdir = rand() % 3 - 1; } //game loop while (!key[KEY_ESC]) { //erase the sprites for (n=0; n<NUMSPRITES; n++) erasesprite(buffer, sprites[n]); //perform standard position/frame update for (n=0; n<NUMSPRITES; n++) updatesprite(sprites[n]); //apply screen warping behavior for (n=0; n<NUMSPRITES; n++) warpsprite(sprites[n]); //draw the sprites for (n=0; n<NUMSPRITES; n++) draw_sprite(buffer, spriteimg[sprites[n]->curframe], sprites[n]->x, sprites[n]->y); //update the screen acquire_screen(); blit(buffer,screen,0,0,0,0,buffer->w,buffer->h); release_screen(); rest(10); } for (n=0; n<63; n++) destroy_bitmap(spriteimg[n]); allegro_exit(); return 0; } END_OF_MAIN()
I think that wraps up the material for animated sprites. You have more than enough information to completely enhance Tank War at this point. But hang on for a few more pages so I can go over some more important topics related to sprites.
In the previous pages you have learned how to grab a frame of animation out of a sprite sheet and copy it into an image array, and then the array was used to draw the animation. This is a legitimate way to do sprite animation, but it involves a “middle man” in the form of that animation array. In the previous example, that middleman was an array called spriteimg[]
. If you are working on a large game with potentially hundreds of sprites, this middle step not only wastes memory, but it takes the game longer to load. I propose a better solution—drawing frames out of the sprite sheet directly to the screen (or to the back buffer).
Let's take a look at a new function called drawframe
that you might find preferable to the grabframe
function because it doesn't have to create a scratch bitmap and return it; it just draws the frame directly to the destination bitmap (see Figure 9.9).
Figure 9.9. The DrawFrame program demonstrates drawing frames from a sprite sheet directly to the back buffer without using an array.
The startx
and starty
parameters let you specify a location inside the sprite sheet where the animation is located. This is useful if you are storing several different sprites in a single sprite sheet, possibly with differently sized frames. For instance, you might have a spaceship sprite sheet that also includes projectiles and an animated explosion stored at several different locations in the image.
void drawframe(BITMAP *source, BITMAP *dest, int x, int y, int width, int height, int startx, int starty, int columns, int frame) { //calculate frame position int framex = startx + (frame % columns) * width; int framey = starty + (frame / columns) * height; //draw frame to destination bitmap masked_blit(source,dest,framex,framey,x,y,width,height); }
Let's see how this function simplifies the code for drawing an animated sprite. I'm just going to have this program draw a single sprite so you can examine the drawframe
function in action without a lot of overhead getting in the way. I've highlighted key lines of code in bold. All the rest of the code should be familiar to you by now because it was borrowed from earlier programs in this chapter.
#include <stdio.h> #include <allegro.h> #define MODE GFX_AUTODETECT_WINDOWED #define WIDTH 800 #define HEIGHT 600 #define WHITE makecol(255,255,255) //define the sprite structure typedef struct SPRITE { int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE; void updatesprite(SPRITE *spr) { //update x position if (++spr->xcount > spr->xdelay) { spr->xcount = 0; spr->x += spr->xspeed; } //update y position if (++spr->ycount > spr->ydelay) { spr->ycount = 0; spr->y += spr->yspeed; } //update frame based on animdir if (++spr->framecount > spr->framedelay) { spr->framecount = 0; if (spr->animdir == -1) { if (--spr->curframe < 0) spr->curframe = spr->maxframe; } else if (spr->animdir == 1) { if (++spr->curframe > spr->maxframe) spr->curframe = 0; } } } void bouncesprite(SPRITE *spr) { //simple screen bouncing behavior if (spr->x < 0) { spr->x = 0; spr->xspeed = rand() % 2 + 2; spr->animdir *= -1; } else if (spr->x > SCREEN_W - spr->width) { spr->x = SCREEN_W - spr->width; spr->xspeed = rand() % 2 - 4; spr->animdir *= -1; } if (spr->y < 0) { spr->y = 0; spr->yspeed = rand() % 2 + 2; spr->animdir *= -1; } else if (spr->y > SCREEN_H - spr->height) { spr->y = SCREEN_H - spr->height; spr->yspeed = rand() % 2 - 4; spr->animdir *= -1; } } void drawframe(BITMAP *source, BITMAP *dest, int x, int y, int width, int height, int startx, int starty, int columns, int frame) { //calculate frame position int framex = startx + (frame % columns) * width; int framey = starty + (frame / columns) * height; //draw frame to destination bitmap masked_blit(source,dest,framex,framey,x,y,width,height); } int main(void) { //images and sprites BITMAP *buffer; BITMAP *bg; SPRITE theball; SPRITE *ball = &theball; BITMAP *ballimage; //initialize allegro_init(); set_color_depth(16); set_gfx_mode(MODE, WIDTH, HEIGHT, 0, 0); install_keyboard(); install_timer(); srand(time(NULL)); //create the back buffer buffer = create_bitmap(WIDTH,HEIGHT); //load background bg = load_bitmap("bluespace.bmp", NULL); if (!bg) { allegro_message("Error loading background image %s", allegro_error); return 1; } //load 32-frame tiled sprite image ballimage = load_bitmap("sphere.bmp", NULL); if (!ballimage) { allegro_message("Error loading ball image %s", allegro_error); return 1; } //randomize the sprite ball->x = SCREEN_W / 2; ball->y = SCREEN_H / 2; ball->width = 64; ball->height = 64; ball->xdelay = rand() % 2 + 1; ball->ydelay = rand() % 2 + 1; ball->xcount = 0; ball->ycount = 0; ball->xspeed = rand() % 2 + 2; ball->yspeed = rand() % 2 + 2; ball->curframe = 0; ball->maxframe = 31; ball->framecount = 0; ball->framedelay = 1; ball->animdir = 1; //game loop while (!key[KEY_ESC]) { //fill screen with background image blit(bg, buffer, 0, 0, 0, 0, WIDTH, HEIGHT); //update the sprite updatesprite(ball); bouncesprite(ball); drawframe(ballimage, buffer, ball->x, ball->y, 64, 64, 0, 0, 8, ball->curframe); //display some info textout_ex(buffer, font, "DrawFrame Program (ESC to quit)", 0, 0, WHITE, 0); textprintf_ex(buffer, font, 0, 20, WHITE, 0, "Position: %2d,%2d", ball->x, ball->y); //refresh the screen acquire_screen(); blit(buffer, screen, 0, 0, 0, 0, WIDTH, HEIGHT); release_screen(); rest(10); } destroy_bitmap(ballimage); destroy_bitmap(bg); destroy_bitmap(buffer); allegro_exit(); return 0; } END_OF_MAIN()
The next enhancement to Tank War will incorporate the new features you learned in this chapter, such as the use of a sprite handler and collision detection. For this modification, you'll follow the same strategy used in previous chapters and only modify the latest version of the game, adding new features. This new version of Tank War is starting to restore collision testing after the primitive “pixel color” collision was removed from the original version (this happened when it was upgraded from vectors to bitmaps). Since I'm covering full-blown collision testing in the next chapter, I'll just give you a sneak peek at a simple collision function that will at least make it possible to hit the enemy tank with a bullet. This function is called inside
, and it just compares an (x,y) point to see if it is inside a boundary (left, top, right, bottom). Since I don't want to overload you with too many changes all at once, the drawframe
function will be postponed until a later version of the game, and we'll continue to use arrays for sprite animation for the time being.
You need to add the sprite STRUCT
to the tankwar.h header file. But the STRUCT
needs two more variables before it will accommodate Tank War because the tanks and bullets included variables that are not yet part of the sprite handler. The sprite STRUCT
must also contain an int called dir
and another called alive
. Open the tankwar.h file and add the struct to this file just below the color definitions. After declaring the struct, you should also add the sprite arrays. At the same time, you no longer need the tagTank
or tagBullet
structs, so delete them! Also, you need to fill in a replacement for the “score” variables for each tank, so declare this as a new standalone int array.
//define the sprite structure typedef struct SPRITE { //new elements int dir, alive; //current elements int x,y; int width,height; int xspeed,yspeed; int xdelay,ydelay; int xcount,ycount; int curframe,maxframe,animdir; int framecount,framedelay; }SPRITE; SPRITE mytanks[2]; SPRITE *tanks[2]; SPRITE mybullets[2]; SPRITE *bullets[2]; //replacement for the "score" variable in tank struct int scores[2];
Replacing the two structs with the new sprite struct will have repercussions throughout the entire game source code because the new code uses pointers rather than struct variables directly. Therefore, you will need to modify most of the functions to use the ->
symbol in place of the period (.
) to access elements of the struct when it is referenced with a pointer. The impact of converting the game to use sprite pointers won't be truly apparent until we add a scrolling background several chapters down the road.
///////////////////////////////////////////////////////////////////// // Game Programming All In One, Third Edition // Chapter 9 - Tank War Game (Enhancement 4) ///////////////////////////////////////////////////////////////////// #include "tankwar.h" int inside(int x,int y,int left,int top,int right,int bottom) { if (x > left && x < right && y > top && y < bottom) return 1; else return 0; } void drawtank(int num) { int dir = tanks[num]->dir; int x = tanks[num]->x-15; int y = tanks[num]->y-15; draw_sprite(screen, tank_bmp[num][dir], x, y); } void erasetank(int num) { int x = tanks[num]->x-17; int y = tanks[num]->y-17; rectfill(screen, x, y, x+33, y+33, BLACK); } void movetank(int num){ int dir = tanks[num]->dir; int speed = tanks[num]->xspeed; //update tank position based on direction switch(dir) { case 0: tanks[num]->y -= speed; break; case 1: tanks[num]->x += speed; tanks[num]->y -= speed; break; case 2: tanks[num]->x += speed; break; case 3: tanks[num]->x += speed; tanks[num]->y += speed; break; case 4: tanks[num]->y += speed; break; case 5: tanks[num]->x -= speed; tanks[num]->y += speed; break; case 6: tanks[num]->x -= speed; break; case 7: tanks[num]->x -= speed; tanks[num]->y -= speed; break; } //keep tank inside the screen //use xspeed as a generic "speed" variable if (tanks[num]->x > SCREEN_W-22) { tanks[num]->x = SCREEN_W-22; tanks[num]->xspeed = 0; } if (tanks[num]->x < 22) { tanks[num]->x = 22; tanks[num]->xspeed = 0; } if (tanks[num]->y > SCREEN_H-22) { tanks[num]->y = SCREEN_H-22; tanks[num]->xspeed = 0; } if (tanks[num]->y < 22) { tanks[num]->y = 22; tanks[num]->xspeed = 0; } } void explode(int num, int x, int y) { int n; //load explode image if (explode_bmp == NULL) { explode_bmp = load_bitmap("explode.bmp", NULL); } //draw the explosion bitmap several times for (n = 0; n < 5; n++) { rotate_sprite(screen, explode_bmp, x + rand()%10 - 20, y + rand()%10 - 20, itofix(rand()%255)); rest(30); } //clear the explosion circlefill(screen, x, y, 50, BLACK); } void updatebullet(int num) { int x, y, tx, ty; int othertank; x = bullets[num]->x; y = bullets[num]->y; if (num == 1) othertank = 0; else othertank = 1; //is the bullet active? if (!bullets[num]->alive) return; //erase bullet rectfill(screen, x, y, x+10, y+10, BLACK); //move bullet bullets[num]->x += bullets[num]->xspeed; bullets[num]->y += bullets[num]->yspeed; x = bullets[num]->x; y = bullets[num]->y; //stay within the screen if (x < 6 || x > SCREEN_W-6 || y < 20 || y > SCREEN_H-6) { bullets[num]->alive = 0; return; } //look for a direct hit using basic collision tx = tanks[!num]->x; ty = tanks[!num]->y; if (inside(x,y,tx,ty,tx+16,ty+16)) { //kill the bullet bullets[num]->alive = 0; //blow up the tank explode(num, x, y); score(num); } else //if no hit then draw the bullet { //draw bullet sprite draw_sprite(screen, bullet_bmp, x, y); //update the bullet positions (for debugging) textprintf(screen, font, SCREEN_W/2-50, 1, TAN, "B1 %-3dx%-3d B2 %-3dx%-3d", bullets[0]->x, bullets[0]->y, bullets[1]->x, bullets[1]->y); } } void fireweapon(int num) { int x = tanks[num]->x; int y = tanks[num]->y; //ready to fire again? if (!bullets[num]->alive) { bullets[num]->alive = 1; //fire bullet in direction tank is facing switch (tanks[num]->dir) { //north case 0: bullets[num]->x = x-2; bullets[num]->y = y-22; bullets[num]->xspeed = 0; bullets[num]->yspeed = -BULLETSPEED; break; //NE case 1: bullets[num]->x = x+18; bullets[num]->y = y-18; bullets[num]->xspeed = BULLETSPEED; bullets[num]->yspeed = -BULLETSPEED; break; //east case 2: bullets[num]->x = x+22; bullets[num]->y = y-2; bullets[num]->xspeed = BULLETSPEED; bullets[num]->yspeed = 0; break; //SE case 3: bullets[num]->x = x+18; bullets[num]->y = y+18; bullets[num]->xspeed = BULLETSPEED; bullets[num]->yspeed = BULLETSPEED; break; //south case 4: bullets[num]->x = x-2; bullets[num]->y = y+22; bullets[num]->xspeed = 0; bullets[num]->yspeed = BULLETSPEED; break; //SW case 5: bullets[num]->x = x-18; bullets[num]->y = y+18; bullets[num]->xspeed = -BULLETSPEED; bullets[num]->yspeed = BULLETSPEED; break; //west case 6: bullets[num]->x = x-22; bullets[num]->y = y-2; bullets[num]->xspeed = -BULLETSPEED; bullets[num]->yspeed = 0; break; //NW case 7: bullets[num]->x = x-18; bullets[num]->y = y-18; bullets[num]->xspeed = -BULLETSPEED; bullets[num]->yspeed = -BULLETSPEED; break; } } } void forward(int num) { //use xspeed as a generic "speed" variable tanks[num]->xspeed++; if (tanks[num]->xspeed > MAXSPEED) tanks[num]->xspeed = MAXSPEED; } void backward(int num) { tanks[num]->xspeed--; if (tanks[num]->xspeed < -MAXSPEED) tanks[num]->xspeed = -MAXSPEED; } void turnleft(int num) { tanks[num]->dir--; if (tanks[num]->dir < 0) tanks[num]->dir = 7; } void turnright(int num) { tanks[num]->dir++; if (tanks[num]->dir > 7) tanks[num]->dir = 0; } void getinput() { //hit ESC to quit if (key[KEY_ESC]) gameover = 1; //WASD - SPACE keys control tank 1 if (key[KEY_W]) forward(0); if (key[KEY_D]) turnright(0); if (key[KEY_A]) turnleft(0); if (key[KEY_S]) backward(0); if (key[KEY_SPACE]) fireweapon(0); //arrow - ENTER keys control tank 2 if (key[KEY_UP]) forward(1); if (key[KEY_RIGHT]) turnright(1); if (key[KEY_DOWN]) backward(1); if (key[KEY_LEFT]) turnleft(1); if (key[KEY_ENTER]) fireweapon(1); //short delay after keypress rest(20); } void score(int player) { //update score int points = ++scores[player]; //display score textprintf(screen, font, SCREEN_W-70*(player+1), 1, BURST, "P%d: %d", player+1, points); } void setuptanks() { int n; //configure player 1's tanks[0] = &mytanks[0]; tanks[0]->x = 30; tanks[0]->y = 40; tanks[0]->xspeed = 0; scores[0] = 0; tanks[0]->dir = 3; //load first tank bitmap tank_bmp[0][0] = load_bitmap("tank1.bmp", NULL); //rotate image to generate all 8 directions for (n=1; n<8; n++) { tank_bmp[0][n] = create_bitmap(32, 32); clear_bitmap(tank_bmp[0][n]); rotate_sprite(tank_bmp[0][n], tank_bmp[0][0], 0, 0, itofix(n*32)); } //configure player 2's tanks[1] = &mytanks[1]; tanks[1]->x = SCREEN_W-30; tanks[1]->y = SCREEN_H-30; tanks[1]->xspeed = 0; scores[1] = 0; tanks[1]->dir = 7; //load second tank bitmap tank_bmp[1][0] = load_bitmap("tank2.bmp", NULL); //rotate image to generate all 8 directions for (n=1; n<8; n++) { tank_bmp[1][n] = create_bitmap(32, 32); clear_bitmap(tank_bmp[1][n]); rotate_sprite(tank_bmp[1][n], tank_bmp[1][0], 0, 0, itofix(n*32)); } //load bullet image if (bullet_bmp == NULL) bullet_bmp = load_bitmap("bullet.bmp", NULL); //initialize bullets for (n=0; n<2; n++) { bullets[n] = &mybullets[n]; bullets[n]->x = 0; bullets[n]->y = 0; bullets[n]->width = bullet_bmp->w; bullets[n]->height = bullet_bmp->h; } } void setupscreen() { int ret; //set video mode set_color_depth(32); ret = set_gfx_mode(MODE, WIDTH, HEIGHT, 0, 0); if (ret != 0) { allegro_message(allegro_error); return; } //print title textprintf(screen, font, 1, 1, BURST, "Tank War - %dx%d", SCREEN_W, SCREEN_H); //draw screen border rect(screen, 0, 12, SCREEN_W-1, SCREEN_H-1, TAN); rect(screen, 1, 13, SCREEN_W-2, SCREEN_H-2, TAN); } int main(void) { //initialize the game allegro_init(); install_keyboard(); install_timer(); srand(time(NULL)); setupscreen(); setuptanks(); //game loop while(!gameover) { //erase the tanks erasetank(0); erasetank(1); //move the tanks movetank(0); movetank(1); //draw the tanks drawtank(0); drawtank(1); //update the bullets updatebullet(0); updatebullet(1); //check for keypresses if (keypressed()) getinput(); //slow the game down rest(20); } //end program allegro_exit(); return 0; } END_OF_MAIN()
This chapter was absolutely packed with advanced sprite code! You learned about animation, a subject that could take up an entire book of its own. Indeed, there is much to animation that I didn't get into in this chapter, but the most important points were covered here and as a result, you have some great code that will be used in the rest of the book (especially the grabframe
and drawframe
functions) and perhaps many of your own Allegro game projects. What comes next? We aren't done with sprites yet, not by a long shot! The next chapter delves into advanced sprite programming, where you'll learn about collision detection, among other awesome subjects.
You can find the answers to this chapter quiz in Appendix A, “Chapter Quiz Answers.”
13.58.5.57