Attached Sprites

An efficient method for sprite animation

Diana Gruber

Diana is senior programmer at Ted Gruber Software, publishers of the Fastgraph programmers' graphics library, and author of the book Action Arcade Adventure Set (Coriolis Group Books, 1994). Diana can be contacted at Fastgraph@aol.com.


Game programmers are always looking for efficient ways to accomplish sprite animation. As the science of game programming evolves, certain techniques, such as the use of "attached" sprites, have become standard in many games.

Consider the case of a dogfight like that in the Quickfire demo in Figure 1. A player-controlled airplane confronts one or more enemy airplanes and shoots bullets at them; when an airplane is hit, it explodes and dies. To achieve a pleasing visual effect, the airplane does not vanish immediately, but rather catches fire and gradually becomes engulfed in flames before dissolving into a puff of smoke.

Data structures are used to keep track of airplanes as they move across the scrolling background. Each data structure holds an airplane's current x- and y-position, its speed, a pointer to the function that controls its action (called the "action function"), and a pointer to a bitmap which describes the current image of the airplane.

An interesting thing happens when an airplane explodes. To achieve the explosion effect, a single sprite must become two sprites--one, the airplane, which remains unchanged; and the other, the explosion, which starts as a small ball of fire in the nose of the airplane, then grows over the course of several frames to a large, smoky fireball that eventually covers the entire airplane. Finally, the airplane disappears as the fireball covers it, and all that is left is the fireball, which dissipates and eventually disappears as well.

The motion of the fireball depends on the motion of the airplane, which has random elements and therefore cannot easily be predicted. It is important that the fireball sprite knows where the airplane sprite is. If two airplanes are exploding at the same time, it is also necessary to match each explosion with the proper airplane. Therefore, you need a mechanism to pass information from the airplane to the explosion.

Similarly, the airplane needs to get information from the explosion. In particular, the airplane needs to know how big the fireball is, so it will know when it is time to disappear.

The easiest way to pass information between the airplane and the explosion is to use an attached sprite. The data structures of both sprites attach themselves to each other by simply using a structure member to point to each other. The action of one sprite is then conveniently influenced by the status of the other sprite.

All the objects--airplanes, explosions, and bullets--are stored in a linked list. Because of the nature of the game, nodes are constantly being added to and removed from the list. This happens when bullets go off the edge of the screen, enemies are killed, and so on. An object and its attached sprite may exist anywhere in the list, as in Figure 2.

Notice that only the enemy that is currently exploding has an attached sprite and that it is possible for the player to have an attached sprite. When the player is hit, a small fireball appears, even though the player does not die from the strike. When there is no explosion, the attached sprite is set to NULL.

The OBJstruct structure holds the information about each sprite object, including airplanes, bullets, and explosions. In this structure (see Example 1), the first two members are the pointers to adjacent nodes in the linked list. The next two members, x and y, specify the current position of the sprite. These values change in each frame according to the speed and direction of the sprite, which are described in the next five members. The frame member describes something about the animation: whether the plane is upright or turning, for example. The next four elements specify the tile extents and are used to determine when an object has moved off the edge of the screen.

The image member is a pointer to the object's bitmap data, which is the actual physical representation of the sprite. This is stored in the SPRITE structure, as in Example 2. This structure holds all the information necessary to display the sprite, including its width and height, and the offset values. The offsets are used to adjust the position of the sprite and are especially useful with explosions, which need to be centered around their midpoint rather than displayed from a corner.

The action member of the object structure is a pointer to a function, such as the do_explosion() function in Listing One . This function is an action function and is executed once each frame. It determines the current state of the object, such as going, falling, or dying.

The final member of the object structure, OBJp attached_sprite, is a pointer to another object structure; in other words, the pointer to the attached sprite. The pointer is always bidirectional, meaning the object points back to whichever object points to it.

The code that controls the creation of the explosion is shown in the function start_explosion(); see Listing One. As you can see, this function spawns an object and adds it to the linked list in the traditional way. It also forms the attachment between the airplane sprite (objp) and the explosion sprite (node); see Example 3 and Figure 3.

The motion of the explosion is controlled by the function do_explosion(). This function first examines the current state of the explosion. If the state of the animation has reached the third frame, the explosion is big enough to cover the airplane. At that point, it is time to kill the airplane by setting its action function to kill_enemy(), as in Example 4.

After the enemy airplane has been killed, the attached sprite is set to NULL, indicating there is no longer an airplane attached to the explosion. The explosion may now move independently for the next few frames, until it also is killed.

During those three frames when both the airplane and the explosion are visible, the x- and y-coordinates of the explosion are determined by the x- and y-coordinates of the airplane, as in Example 5. These coordinates include a 16-pixel horizontal adjustment and a four-pixel vertical adjustment to center the explosion over the nose of the airplane.

Attached sprites have many applications. When a character needs to hold an object, such as a gun, attached sprites can greatly simplify the code. This also saves room, which is always at a premium when designing games. If you have a sprite with 30 positions (running, jumping, falling, standing, and so forth) and you add a gun to each of those positions, you will need to generate 30 more sprites. If the sprites have an average width of 30 pixels and height of 40 pixels, this will use 36 Kbytes of sprite space. If you can reuse the nonshooting sprites by simply adding an attached gun arm to each one, the savings in RAM and disk space will be significant. (For more information about sprite animation, see Chapters 12 and 13 in my book, Action Arcade Adventure Set.)

Figure 1 Sample mid-air battle between airplanes (from the Quickfire demo).

Example 1: The OBJstruct structure.

typedef struct OBJstruct
{
  OBJp  next;
  OBJp  prev;
  int x;
  int y;
  int xspeed;
  int max_xspeed;
  int yspeed;
  int direction;
  int frame;
  int tile_xmin;
  int tile_xmax;
  int tile_ymin;
  int tile_ymax;
  SPRITE *image;
  ACTIONp action;
  OBJp attached_sprite;
};

Example 2: The SPRITE structure.

typedef struct _sprite
{
   char *bitmap;
   int width;
   int height;
   int xoffset;
   int yoffset;
}  SPRITE;

Figure 2 Objects in the list may point to each other or to nothing.

Figure 3 The airplane and the explosion point to each other.

Example 3: The portion of the start_explosion() function that attaches two sprites to each other.

/* set up the links between the explosion and the enemy plane */
node->attached_sprite = objp;
objp->attached_sprite = node;

Example 4: Setting a sprite's action function.

if (objp->attached_sprite != (OBJp)NULL)
   objp->attached_sprite->action = &kill_enemy;

Example 5: Determining x- and y-coordinates.

/* The position of the explosion depends on the position of the airplane */
if (objp->attached_sprite != (OBJp)NULL)
{
   objp->x = objp->attached_sprite->x+16;
   objp->y = objp->attached_sprite->y-4;
}

Listing One


/********************  sprite declarations *************************/
int nsprites;
typedef struct _sprite
{
   char *bitmap;
   int width;
   int height;
   int xoffset;
   int yoffset;

}  SPRITE;
SPRITE *sprite[40];

/* forward declarations */
struct OBJstruct;
typedef struct OBJstruct OBJ, near *OBJp;

/* pointer to object action function */
typedef void near ACTION (OBJp objp);
typedef ACTION *ACTIONp; 

/* data structure for objects */
typedef struct OBJstruct 
{
  OBJp  next; 
  OBJp  prev; 
  int x;
  int y;
  int xspeed;
  int max_xspeed;
  int yspeed;
  int direction;
  int frame;
  int tile_xmin;
  int tile_xmax;
  int tile_ymin;
  int tile_ymax;

  SPRITE *image;

  ACTIONp action; 
  OBJp attached_sprite;
};
SPRITE *explosion[11];

/**********************************************************************/
void near start_explosion(OBJp objp)
{
   OBJp node;

   /* allocate space for the object */
   node = (OBJp)malloc(sizeof(OBJ));
   if (node == (OBJp)NULL) return;

   /* assign values to the structure members */

   /* after the plane has been killed, the explosion moves at a slower
      speed because smoke drifts slower than metal */
   node->xspeed = objp->xspeed/2;
   node->yspeed = objp->yspeed/2;

   /* tile extents */
   node->tile_xmin = 2;
   node->tile_xmax = 21;
   node->tile_ymin = 0;
   node->tile_ymax = 14;

   /* the sprite will be the first frame explosion bitmap */
   node->image = explosion[0];
   node->x = objp->x+16;
   node->y = objp->y-4;
   node->frame = -1;

   /* insert at the top of the linked list */
   node->prev = top_node;
   node->prev->next = node;
   top_node = node;
   node->next = (OBJp)NULL;

   /* set up the links between the explosion and the enemy plane */
   node->attached_sprite = objp;
   objp->attached_sprite = node;

   /* assign the action function */
   node->action = do_explosion;
}
/**********************************************************************/
void near do_explosion(OBJp objp)
{
   /* If the explosion has reached the frame 3 state, at which point
      the bitmap is bigger than the airplane, it is time to kill the
      airplane. */

   if (objp->frame > 3)
   {

      /* if the attached sprite is NULL that means the airplane was
         already killed */
      if (objp->attached_sprite != (OBJp)NULL)
         objp->attached_sprite->action = &kill_enemy;

      objp->x += objp->xspeed;
      objp->y += objp->yspeed;
   }
   else
   {
      /* The position of the explosion depends on the position of
         the airplane */
      if (objp->attached_sprite != (OBJp)NULL)
      {
         objp->x = objp->attached_sprite->x+16;
         objp->y = objp->attached_sprite->y-4;
      }

      /* it is possible for the explosion to be at less than frame 3
         but there is no attached sprite.  That happens when the enemy
         plane has drifted off the edge of the screen. */
      else
      {
         objp->x += objp->xspeed;
         objp->y += objp->yspeed;
      }
   }

   /* Increment the explosion frame */
      objp->frame++;

   /* define which sprite will be displayed this frame */
   objp->image = explosion[objp->frame];

   /* We have 10 frames for the explosion */
   if (objp->frame > 10)
   {
      objp->image = explosion[10];
      objp->action = kill_explosion;
   }
}


Copyright © 1995, Dr. Dobb's Journal