Chapter 9. Sprite Animation

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:

Animated 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!

Drawing an Animated Sprite

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 animated cat sprite.

Figure 9.1. The animated cat sprite.

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).

The AnimSprite program shows how you can do basic sprite animation.

Figure 9.2. The AnimSprite program shows how you can do basic sprite animation.

#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;

This concept is something I'll explain shortly.

Creating a Sprite Handler

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;

Note

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 SPRITE struct and its elements help abstract sprite movement into reusable code.

Figure 9.3. The SPRITE struct and its elements help abstract sprite movement into reusable code.

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).

The SpriteHandler Program

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.

The SpriteHandler program demonstrates a full-featured animated sprite handler.

Figure 9.4. The SpriteHandler program demonstrates a full-featured animated sprite handler.

#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()

Grabbing Sprite Frames from an Image

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.

This bitmap image contains 32 frames of an animated sphere used as a sprite. Courtesy of Edgar Ibarra.

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.

Note

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

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.

The SpriteGrabber program demonstrates how to grab sprite images (or animation frames) from a sprite sheet.

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()

The Next Step: Multiple Animated Sprites

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.

The source image for the animated asteroid contains 64 frames.

Figure 9.7. The source image for the animated asteroid contains 64 frames.

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

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.

The MultipleSprites program animates 100 sprites on the screen.

Figure 9.8. The MultipleSprites program animates 100 sprites on the screen.

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.

Drawing Sprite Frames Directly

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).

The drawframe Function

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).

The DrawFrame program demonstrates drawing frames from a sprite sheet directly to the back buffer without using an array.

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);
}

Testing the drawframe Function

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()

Enhancing Tank War

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()

Summary

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.

Chapter Quiz

You can find the answers to this chapter quiz in Appendix A, “Chapter Quiz Answers.”

1.

Which function draws a standard sprite?

  1. draw_standard_sprite

  2. standard_sprite

  3. draw_sprite

  4. blit_sprite

2.

What is a frame in the context of sprite animation?

  1. A single image in the animation sequence

  2. The bounding rectangle of a sprite

  3. The source image for the animation sequence

  4. A buffer image used to store temporary copies of the sprite

3.

What is the purpose of a sprite handler?

  1. To provide a consistent way to animate and manipulate many sprites on the screen

  2. To prevent sprites from moving beyond the edges of the screen

  3. To provide a reusable sprite drawing function

  4. To keep track of the sprite position

4.

What is a struct element?

  1. A property of a struct

  2. A sprite behavior

  3. The underlying Allegro sprite handler

  4. A variable in a structure

5.

Which term describes a single frame of an animation sequence stored in an image file?

  1. Snapshot

  2. Tile

  3. Piece

  4. Take

6.

Which Allegro function is used frequently to erase a sprite?

  1. rectfill

  2. erase_sprite

  3. destroy_sprite

  4. blit

7.

Which term describes a reusable activity for a sprite that is important in a game?

  1. Collision

  2. Animation

  3. Bounding

  4. Behavior

8.

What is the name of the new function that draws an animation frame to the screen?

  1. drawanim

  2. drawframe

  3. animate

  4. nextimage

9.

Which term best describes an image filled with rows and columns of small sprite images?

  1. scrolling

  2. sprite bitmap

  3. sprite sheet

  4. sprite tiles

10.

How does a sprite struct improve the source code of a game?

  1. Reduces global variable use

  2. Eliminates code comments

  3. Helps align code blocks

  4. Consolidates code into functions

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.51.157