Basic Animation Techniques for Mode 13h

Atrevida Game Programming Tutorial #10
Copyright 1997, Kevin Matz, All Rights Reserved.

Prerequisites:

  • Chapter 7: Introduction to VGA Graphics Programming Using Mode 13h

       
In this chapter, we'll learn about animation in VGA Mode 13h by using sprites. We'll learn how to create sprites, display them, read them off the screen, and manage them. We'll also discover how to produce animation, and how we can eliminate distracting flashing effects and jerkiness.

Sprites

A sprite is a rectangular set of pixels that make up an image or part of an image. (I don't know where the name "sprite" came from.) Sprites can be drawn to the screen, erased from the screen, and otherwise manipulated.

Think of a two-dimensional game like Space Invaders. The player's spaceship is a sprite. The enemy spaceships are also sprites. The bullets or lasers are probably also sprites. Sprites might be images of people or objects in other types of games.

Storing sprites in memory

If we want to use sprites in our games, we first have to decide how sprites should be stored in memory. Let's say we have the following image, where each asterisk represents a pixel of some color (let's use color 4, which is usually red), and the periods represent black pixels. The image, by the way, is supposed to resemble a car (and I have no idea why the art academy rejected me):

         ....**********.......
         ...**....*....*......
         ********************.
         *********************
         .*..*..........*..*..
         ..**............**...

We could use a two-dimensional array of unsigned chars (because the range of colors is 0..255) to store this image, like this:

unsigned char car_sprite[6][21] = {
  { 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 4, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0 },
  { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0 },
  { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 },
  { 0, 4, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 4, 0, 0 },
  { 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0 }
}

The only disadvantage to this format is that we need to keep track of the sprite's height and width (in pixels) somewhere else. We'd need to have separate variables, perhaps called car_sprite_height and car_sprite_width; in this case, they would be 6 and 21, respectively. These variables should be ints; if they were chars, the maximum sprite size would be 255 by 255 pixels, which might not be sufficient for use with a 320 by 200 pixel screen.

To avoid the bother of these separate variables, we'll use the sprite format that everybody else seems to use. It's sometimes called the linear sprite format (it's linear because Mode 13h is a straight-forward video mode with linear memory organization; other VGA video modes need more complex sprite formats, most notably planar formats). This format makes use of a one-dimensional array. We let the first two characters store the sprite's width, and the next two characters store the sprite's height.

But, remember that one of the joys of working on the PC is the fact that it uses a little-endian processor. And so the width and height must be stored in little-endian order. If we call our sprite car_sprite, car_sprite[0] must store the least-significant byte of the int-sized width; car_sprite[1] must store the most-significant byte of the width. And likewise, car_sprite[2] stores the least-significant byte of the int-sized height, and car_sprite[3] stores the height's most-significant byte. (For heights and widths that are 255 or less, the most-significant bytes are zero.)

After these first four bytes, the colors of the sprite are stored in the rest of the one-dimensional array. We start with the first column of the first row: its color goes into car_sprite[4]. Then we go to the next column in the same row, and store that color in car_sprite[5]. We continue until the end of that row, and then we go to the next row. We continue across the columns in that row, storing pixels in consecutive elements of the car_sprite array.

So, for the car sprite above, we can put it into the linear sprite format with:

/*                         width   height
                            /---\  /--\    */
unsigned char car_sprite = {21, 0, 6, 0,
    0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 4, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,
    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0,
    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    0, 4, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 4, 0, 0,
    0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0
}

For another example, let's try a sprite of a ball, with the following colors:

      0   11   11   11    0
     11   14    3    3    9
     11    3    3    3    9
     11    3    3    3    9
      0    9    9    9    0

And to encode it in the linear sprite format:

unsigned char ball_sprite = {5, 0, 5, 0,        /* width 5, height 5 */
      0,  11,  11,  11,   0,
     11,  14,   3,   3,   9,
     11,   3,   3,   3,   9,
     11,   3,   3,   3,   9,
      0,   9,   9,   9,   0
}

Drawing a sprite to the screen

So how would we display one of these sprites on the screen? We could loop through all of the rows of the sprite, and within each iteration, we would then loop through each column in that row and plot each pixel. And we will want to position the sprite anywhere on the screen, so we would like to supply an (x, y) coordinate pair that represents where on the screen the upper-left-hand corner of the sprite should be. Here's one possible algorithm:

Given the upper-left-hand corner coordinates (x, y), and a pointer to
the sprite, *sprite:

For y_count = 1 to the sprite's height
Begin
    For x_count = 1 to the sprite's width
    Begin
        Plot a pixel of color sprite[4 + (y_count * width) + x_count] at
            (x + x_count, y + y_count)
    End
End

And an implementation using this algorithm:

void PutSprite (int x, int y, unsigned char sprite[])
{
    int x_count, y_count;
    int width, height;

    /* Extract the width from the first two bytes of the array, and the
       height from the third and fourth bytes of the array: */
    width = sprite[0] + (sprite[1] << 8);
    height = sprite[2] + (sprite[3] << 8);

    /* Loop through the rows: */
    for (y_count = 0; y_count <= height - 1; y_count++)
        /* Loop through the columns: */
        for (x_count = 0; x_count <= width - 1; x_count++)
            PutPixel (x + x_count, y + y_count,
                sprite[4 + (y_count * width) + x_count]);
}

Then, to draw a sprite, you could say:

PutSprite (50, 100, car_sprite);

This implementation works, but it's not as fast as it could be. Probably the biggest problem is the multiplication within the most deeply nested for loop. The result of this multiplication stays the same throughout each iteration of the x_count loop, and only differs by width between each iteration of the y_count loop. Let's try take two:

void PutSprite (int x, int y, unsigned char sprite[])
{
    int x_count, y_count;
    int width, height;
    int y_count_times_width = 0;

    width = sprite[0] + (sprite[1] << 8);
    height = sprite[2] + (sprite[3] << 8);

    for (y_count = 0; y_count <= height - 1; y_count++) {
        for (x_count = 0; x_count <= width - 1; x_count++)
            PutPixel (x + x_count, y + y_count,
                sprite[4 + y_count_times_width + x_count]);

        y_count_times_width += width;
    }
}

That should improve the speed slightly. But we can do better: notice that we're adding 4 during each iteration of the x_count loop. And also notice that we're adding x_count, and each time, x_count is one greater than the last time. So the expression that is evaluating the offset into the sprite array only varies by one each time, so we can evaluate that expression once, and then repeatedly increment it. We are, after all, scanning sequentially through the sprite array from the first pixel (at the first position, offset 4) onwards. In the following implementation, I've used an int variable called position_within_sprite to act as a pointer to positions within sprite:

void PutSprite (int x, int y, unsigned char sprite[])
{
    int x_count, y_count;
    int width, height;
    int position_within_sprite = 4;

    width = sprite[0] + (sprite[1] << 8);
    height = sprite[2] + (sprite[3] << 8);

    for (y_count = 0; y_count <= height - 1; y_count++)
        for (x_count = 0; x_count <= width - 1; x_count++)
            PutPixel (x + x_count, y + y_count,
                sprite[position_within_sprite++]);
}

Next, let's use an actual pointer to scan through the sprite array. Then, we can replace the call to PutPixel() with the contents of the function PutPixel(). When we're finished plotting a row, we can add "SCREEN_WIDTH - width" to the pointer into video memory, to let it point to the first column of the new row. Here's the new version:

void PutSprite (int x, int y, unsigned char sprite[])
{
    int x_count, y_count, x_incr;
    int width, height;

    unsigned char *pointer_within_sprite = sprite + 4;
    unsigned char far *video_mem_pointer = ptr_to_video_segment +
        (y << 8) + (y << 6) + x;

    width = sprite[0] + (sprite[1] << 8);
    height = sprite[2] + (sprite[3] << 8);
    x_incr = SCREEN_WIDTH - width;

    for (y_count = 0; y_count <= height - 1; y_count++) {
        for (x_count = 0; x_count <= width - 1; x_count++)
            /* Plot pixel: */
            *(video_mem_pointer++) = *(pointer_within_sprite++);

         video_mem_pointer += x_incr;  /* Jump down to next row, first
                                          column */
    }
}

When we reach the assembler chapters, we'll see that there is a faster way to draw a linear-format sprite to the screen (using 80x86 instructions called REP MOVSB, REP MOVSW, etc.).

Many books refer to the process of putting a sprite on the screen as a bitblt operation, where bitblt stands for bit-block transfer. I believe it's pronounced "bit-blit", because software routines (and hardware devices on certain nice non-PC machines) that draw sprites to the screen are often called "blitters".

Drawing sprites with transparent pixels

If you were to draw a colored background to the screen, and then use one of the above PutSprite() routines to draw sprites on top of it, the background will be obliterated -- not only would the colored pixels be drawn on top of the background, which is what we want, but the black pixels (color 0) would also be drawn on top of the background, which is not desirable in most cases.

We can define some color between 0 and 255 to stand for a transparency indicator. In the past, I have always used color 255 as the "transparent color". But everyone else uses color 0, and so to be consistent with other sources, I'll use it too. Here's an advantage: comparing a color to zero can be done faster than comparing a color to some other arbitrary number.

How do we deal with transparent pixels in our PutSprite() function? Well, we need to compare each pixel to the transparent color: if the color in question is equal to the transparent color, we simply don't plot any pixel for that location. For all other colors, we plot the pixel as usual. It's a very simple addition to the last PutSprite() routine we developed:

void PutSprite (int x, int y, unsigned char sprite[])
{
    int x_count, y_count, x_incr;
    int width, height;

    unsigned char *pointer_within_sprite = sprite + 4;
    unsigned char far *video_mem_pointer = ptr_to_video_segment +
        (y << 8) + (y << 6) + x;

    width = sprite[0] + (sprite[1] << 8);
    height = sprite[2] + (sprite[3] << 8);
    x_incr = SCREEN_WIDTH - width;

    for (y_count = 0; y_count <= height - 1; y_count++) {
        for (x_count = 0; x_count <= width - 1; x_count++) {
            if (*pointer_within_sprite) /* If not a transparent pixel... */
                *(video_mem_pointer) = *(pointer_within_sprite);

            video_mem_pointer++;
            pointer_within_sprite++;
        }

        video_mem_pointer += x_incr;   /* Jump down to next row, first
                                          column */
    }
}

This does slow down the PutSprite() routine -- for each pixel, a comparison has to be performed. But it's the price we have to pay for transparent pixels. You could keep two separate sprite-drawing routines, one that handles transparencies and one that doesn't.

What if we still want black pixels in our sprites? Color number 16 is normally black, so for now, use 16 instead of 0 when you want black. When we deal with palette manipulations, we'll see that we can redefine the arrangement of colors, so you could then assign the color black to whatever color number you want.

Reading sprites from the screen

Often we will find it necessary to generate a sprite by reading a rectangular area of the screen. Perhaps we have drawn some pattern on the screen using graphics primitives, and we want to store it in a sprite. Or, as we'll see later, we might want to save a portion of the background, so that we can draw a sprite on it and then restore the background.

There is the issue of "where do we put our sprite data?", and we have two choices for our GetSprite() function: we can let the programmer using the function pass a pointer to an a block of memory (an array), which we hope is sufficiently large to hold the amount of data we'll put in it, or we can allocate memory ourselves by using malloc() (or new in C++). One problem associated with the use of memory allocation in our GetSprite() function is that we don't know how efficient malloc() or new is. Another problem concerns what we should do if we find there isn't any memory left -- should we pass an error code back, or should we exit the program (after all, that's what the programmer would probably do if given an out-of-memory error code).

As nice as dynamic allocation is, it's just not particularly appropriate here. We'll let the programmer using the function pass a pointer to a block of memory which is 4 + width * height bytes or greater in length. That block of memory can either be generated with an array declaration (eg. "unsigned char my_sprite[1000]") or with dynamic allocation (eg. "my_buffer = (unsigned char *) malloc(1000 * sizeof(unsigned char))").

How will GetSprite() work? The idea is just to scan through all the pixels in the rectangular area on the screen, and store all the pixels in an array that uses the linear sprite format. But we also need to handle transparencies. We would like to specify which color, if any, should be considered a transparent color; if we encounter pixels of that color, we'll write a zero to the sprite array. And if we come across pixels of color 0, as we don't want those pixels to become transparent (unless, of course, the transparent color is zero), we should convert the zero to another color number that represents black (such as 16, above). So the function prototype might read:

void GetSprite (int x1, int y1, int x2, int y2, unsigned char sprite[],
    unsigned char transparent_color, unsigned char convert_0_to_color);

If we wanted to read a sprite off the screen using the coordinates (25, 90)-(75, 120), storing it in the array my_sprite, considering color 1 (dark blue) to be the background color (to be converted to zeroes), and converting any "true black" pixels with color 0 to the alternate black color 16, we would call the function like this:

GetSprite (25, 90, 75, 120, my_sprite, 1, 16);

If the portion of the screen that we wanted to read the sprite from used color 0 as the background, and we wanted to let those black pixels represent transparent pixels in the sprite, we'd say:

GetSprite (25, 90, 75, 120, my_sprite, 0, 0);

So, here's my first try at a GetSprite() function:

void GetSprite (int x1, int y1, int x2, int y2, unsigned char sprite[],
    unsigned char transparent_color, unsigned char convert_0_to_color)
{
    int x_count, y_count;
    int width, height;
    int temp_color;

    width = x2 - x1 + 1;
    height = y2 - y1 + 1;

    sprite[0] = width & 0xFF;
    sprite[1] = width >> 8;
    sprite[2] = height & 0xFF;
    sprite[3] = height >> 8;

    /* Loop through the rows: */
    for (y_count = 0; y_count <= height - 1; y_count++)
        /* Loop through the columns: */
        for (x_count = 0; x_count <= width - 1; x_count++) {
            temp_color = ReadPixel(x1 + x_count, y1 + y_count);
            if (temp_color == transparent_color)  /* If transparent... */
                sprite[4 + (y_count * width) + x_count] = 0;
            else if (temp_color)                  /* If not 0... */
                sprite[4 + (y_count * width) + x_count] = temp_color;
            else                                  /* If 0... */
                sprite[4 + (y_count * width) + x_count] = convert_0_to_color;
        }
}

Then, using the optimizations that we used with the PutSprite() function:

void GetSprite (int x1, int y1, int x2, int y2, unsigned char sprite[],
    unsigned char transparent_color, unsigned char convert_0_to_color)
{
    int x_count, y_count, x_incr;
    int width, height;
    int temp_color;

    unsigned char *pointer_within_sprite = sprite + 4;
    unsigned char far *video_mem_pointer = ptr_to_video_segment +
        (y1 << 8) + (y1 << 6) + x1;

    width = x2 - x1 + 1;
    height = y2 - y1 + 1;
    x_incr = SCREEN_WIDTH - width;

    sprite[0] = width & 0xFF;
    sprite[1] = width >> 8;
    sprite[2] = height & 0xFF;
    sprite[3] = height >> 8;

    for (y_count = 0; y_count <= height - 1; y_count++) {
        for (x_count = 0; x_count <= width - 1; x_count++) {
            temp_color = *(video_mem_pointer++);
            if (temp_color == transparent_color)  /* If transparent... */
                *(pointer_within_sprite++) = 0;
            else if (temp_color)                  /* If not 0... */
                *(pointer_within_sprite++) = temp_color;
            else                                  /* If 0... */
                *(pointer_within_sprite++) = convert_0_to_color;
        }

        video_mem_pointer += x_incr;
    }
}

Simple animation

Animation on the computer works the same way it does with television and movies: there are a number of frames or animation cels which contain images, and each frame's image is slightly different than the one before. If the frames are displayed in order, and at a certain rate, your brain is supposedly tricked into believing that your eyes are witnessing constant motion. If the display rate is too slow, as is the case with low-budget cartoons, your brain sees the individual frames being displayed in order.

For our first animation example, we'll simply move one sprite around the screen. For now, we'll ignore the entire issue of frames and timing.

Let's animate our car sprite by moving it across the screen from left to right. To do this, we simply draw the sprite at some specified coordinates, pause for some short duration, erase that sprite, and then draw the sprite at a slightly different location (in this example, we'll just increment the x coordinate). Then we pause again, erase that sprite, and draw it again, somewhere else.

Putting the above plan into a pseudo-code algorithm form:

start_x and end_x denote the limits of horizontal motion; y is some
arbitrary y-coordinate

For x = start_x to end_x
Begin
    PutSprite (x, y, car_sprite);
    Wait for some duration
    Erase the sprite
End

And here's a sample code fragment to do the job:

int x;
int y = 95;
int sprite_width, sprite_height;

sprite_width = car_sprite[0] + (car_sprite[1] << 8);
sprite_height = car_sprite[2] + (car_sprite[3] << 8);

SetMode13h ();
for (x = 10; x <= 290; x++) {
    PutSprite (x, y, car_sprite);
    delay (15);    /* Remember to #include <dos.h> to use delay() */
    Bar (x, y, x + sprite_width, y + sprite_height, 0);
}

The speed of the car's movement depends on the speed of your computer. To speed up the car, change the "15" in the "delay (15);" line to something lower, or change it to something higher to slow the car down.

You'll notice that the animation is not particularly smooth -- the image flickers and pauses. A solution to this problem involves taking advantage of the video system's vertical retrace, as we will in the following section.

The vertical retrace

To understand what vertical retrace is, and how we can use it, we have to briefly consider how a computer monitor works.

On the (back of the) glass screen on the front of the monitor is a phosphor coating. When the phosphor is excited by a focused stream of electrons, it illuminates (phosphoresces) and glows for a short period of time. An electron gun at the back of the monitor provides the stream of electrons; using electric and/or magnetic fields, the electrons are accelerated forward and steered towards various locations on the front screen. The process occurs inside an evacuated container called a cathode ray tube, because the electron beam is usually called a cathode ray. (Consult a physics instructor or an electronics enthusiast for more (accurate) information.) With a color monitor, three electron guns provide electrons; they are associated with the colors red, green, and blue. A fine mesh in front of the phosphors permits electrons from each cathode ray to pass through only to certain locations on the screen.

The VGA has circuitry that controls the path of the cathode ray(s) in the CRT. The beam is first brought along the top row of the display (it also draws the borders, but we won't consider that here), and the cathode rays associated with each color -- red, green, and blue -- are set to the correct intensities to produce the colors for each pixel. When the first row of pixels has been traced out, the beams are temporarily shut off, and the beams are moved to the start of the second row of pixels. This period in which this occurs is called the horizontal retrace. The second row of pixels is drawn, and then another horizontal retrace period occurs, so that the beams begin at the start of the next row. This continues until the last pixel at the right-hand bottom corner of the screen is drawn. Then, the beams are shut off again, and they move to the top left-hand corner of the display, ready to redraw the next screen. This period of movement is called the vertical retrace. It is longer in duration than a horizontal retrace.

Depending on the video mode, screen refreshes occur 50, 60, or 70 times per second. The vertical retrace period, as you can imagine, is rather short, as the majority of the time spent in each screen update is used for refreshing the image; the vertical retrace is done at the end of each refresh, in preparation for the next screen refresh.

How do we take advantage of the vertical retrace? Well, consider what happens if we are drawing a sprite or graphics primitives to the video memory. The VGA is constantly scanning through video memory, reading pixels to send to the monitor. If we are changing the screen image by drawing sprites or graphics primitives, there is always a chance that the VGA will scan through that region of video memory right as we are writing to it. It is possible that only a portion of the graphics primitive or sprite gets drawn before the VGA's "pointer" sweeps through that area, and so only part of the primitive or sprite shows up on the display. The rest of the primitive or sprite would show up on the display by the next screen refresh, unless, of course, its drawing had still not been completed.

This explains why we experienced flickering when we animated the car in the example above. When the sprite was being drawn, the VGA's "screen refresh pointer" may or may not have passed through that area at the same time. If it did, only part of the car would have been visible. When we were drawing a bar on top of the car to erase it, we might have been "caught in the act", so that only part of the car would be erased.

To avoid flicker, we can try to save our updates to the video memory for vertical retrace periods. During a vertical retrace period, no refreshing of the screen is performed by the VGA, so we can draw sprites and primitives without worry. (Can we take advantage of the horizontal retrace periods too? We could, but they are so short in duration that it is not worth the attempt.)

The VGA has a status flag in a register that tells us whether or not the vertical retrace period is in effect. In the "Input Status #1 Register", at port 3DA hex (for color VGA systems), bit 3 determines the vertical retrace state: if it is set, a vertical retrace period is in effect; if it is cleared, the display is being refreshed. (See Ferraro: "Programmer's Guide to the EGA, VGA, and Super VGA Cards, Third Edition", pages 335 and 336). So, to determine the state, all we need to do is:

1. Read a byte from port 3DA hex.
2. AND the byte with the AND mask 08 hex.
3. If the result is zero, the display is being refreshed; if the result
   is non-zero (specifically, 08 hex), a vertical retrace period is in
   effect.

For our purposes right now, we will simply want to use a loop to check the vertical retrace status. As soon as the display refresh period ends, we can exit the loop and draw our sprites and primitives. As the vertical retrace period is short, we should try to update the video memory as quickly as possible; otherwise, the display refresh period will kick in and we'll still suffer from some flickering. We should save all our calculations and decision-making for when the vertical retrace period is finished.

There's one thing missing: if we use the loop described above, what happens if we enter the loop when the vertical retrace period is already in effect? The loop exits and we start drawing our sprites and primitives. But what if the vertical retrace period was just ending? Then we'll run into the refresh period, and we'll get flicker again. So it is common to take the "safe" approach: if it is determined at first that a vertical retrace period is already active, we wait for it to finish, and then we wait for the following refresh period to end, and then we draw to video memory. The algorithm would then be:

Read a byte, x, from port 3DA hex
Let x = x AND 08 hex

If x is not zero, then
Begin
    While x is not zero:
    Begin
        Read a byte, x, from port 3DA hex
        Let x = x AND 08 hex
    End
End

While x is zero:
Begin
    Read a byte, x, from port 3DA hex
    Let x = x AND 08 hex
End
(At this point, the vertical retrace period has just begun.)

Then it's easy to convert it to C; let's create a function called WaitForVerticalRetrace():

void WaitForVerticalRetrace ()
{
    int x;

    x = inportb(0x3DA) & 0x08;         /* include dos.h for inportb() */
    if (x)
        while (x) {
            x = inportb(0x3DA) & 0x08;
        }

    while (x == 0) {
        x = inportb(0x3DA) & 0x08;
    }
}

Now, let's use our car animation example again, but instead of using delay() to create a delay, let's make use of WaitForVerticalRetrace():

int x;
int y = 95;
int sprite_width, sprite_height;

sprite_width = car_sprite[0] + (car_sprite[1] << 8);
sprite_height = car_sprite[2] + (car_sprite[3] << 8);

SetMode13h ();
WaitForVerticalRetrace();

for (x = 10; x <= 290; x++) {
    PutSprite (x, y, car_sprite);
    WaitForVerticalRetrace ();
    Bar (x, y, x + sprite_width, y + sprite_height, 0);
}

The car's movement should now be much smoother. The car's speed is now synchronized with the refresh rate of the video mode, and the vertical retrace signals are a very steady timing source. We are updating the screen once for every vertical retrace that occurs.

This brings up another point: what happens on computers with different speeds than your own? We'll consider cases like the above sample code where we are synchronized exactly at the refresh rate. On faster computers, there is little to worry about; the car cannot go faster (that is, get updated more often) than the video mode's refresh rate allows. And the car should not go slower, because faster computers can be expected to perform tasks (such as drawing sprites) faster.

But on slower computers, it is possible that things will take so long that the sprites and primitives get drawn only on every second vertical retrace period. The animation will then proceed only at half the rate that we would like -- it's only going half-speed! And what if the computer is painfully slow, and we only get to update the screen every three, four, or more vertical retraces? Things can only get worse!

Ideally, we would like the animation to run at the same speed on as wide a range of computers as possible. If we were lazy, we would most likely give up and say, "you need a such-and-such computer with a minimum speed of so-and-so". Of course, a line has to be drawn somewhere -- we can no longer expect, say, my old 8 MHz XT to be able to run modern games. But we don't want to limit our market (if we are marketing a game) only to players who have the fastest computers available at the time.

Getting our games to run at the same speed on all reasonably capable machines really takes an absolutely scandalous amount of effort! It will be covered in a later chapter, because it involves programming a timing device (usually, the 8253 timer chip), writing interrupt handlers (which demands a little knowledge of assembly language), making use of fixed-point arithmetic (so that objects can move at different speeds, but performance doesn't suffer from floating-point arithmetic), and creating and managing some appropriate data structure to keep a list of sprites and primitives to draw. It's not particularly pleasant, and I don't know of any alternative ways to accomplish it.

The "dirty rectangles" technique

If you have a intricate background that is not just a solid color, drawing bars will not work for erasing sprites. Instead, we can save portions of the background using GetSprite(), draw our sprites, and then draw the saved background fragments on top of the sprites.

I suppose the name "dirty" part comes from the fact that we're avoiding the redrawing of the entire screen, which might be considered a quick-and-dirty technique.

I'll combine the example for the dirty rectangles technique with some more complicated motion: I'll move the ball sprite in a sine-wave pattern. Unfortunately, floating-point math and trigonometric functions are used, but hopefully they won't slow the animation down past the one-update-per-vertical-retrace rate.

int a;
int x, y, old_x, old_y, sprite_width, sprite_height;
unsigned char background_sprite[900];   /* Give ourselves lots of extra
    room; a background just under 30x30 pixels can be stored in this */

sprite_width = ball_sprite[0] + (ball_sprite[1] << 8);
sprite_height = ball_sprite[2] + (ball_sprite[3] << 8);

/* Put up some trash for a quick background image: */
Bar (0, 0, 319, 199, 8);
Circle (160, 100, 50, 2);
Line (20, 180, 300, 20, 4);
Line (130, 50, 310, 90, 7);
Box (30, 20, 105, 140, 3);
Bar (200, 120, 280, 190, 5);

GetSprite (10, 100, 10, 100, background_sprite, 0, 0);
old_y = 100;

for (x = 10; x <= 310; x++) {
    y = (int) (100 - 50 * sin((float) x / 30));
    WaitForVerticalRetrace ();
    PutSprite (old_x, old_y, background_sprite);
    GetSprite (x, y, x + sprite_width, y + sprite_height, background_sprite,
        0, 0);
    PutSprite (x, y, ball_sprite);

    old_x = x;
    old_y = y;
}

Other animation techniques

You can animate more than one object simply by adding code to draw and erase additional objects to your animation loops.

In a real game, very few objects would stay static as the car did in the previous example. An animated person would be expected to move his or her arms and legs when walking, for example. Simply alternate between two or more sprites in your animation loops.

Page flipping (or double buffering) is a popular and effective technique for animation. The idea is to have several "pages" of screen data in memory, but only one is displayed on the monitor at a time. You can modify one page "behind the scenes" while another page is being displayed, and then you can rapidly switch between the two pages, or copy one page to another. We'll use this technique in the chapters dealing with the undocumented "Mode X" video modes. Page flipping can be simulated to a degree with Mode 13h, but I don't feel it's worth getting into here. We'll use the real thing in the Mode X chapters.

In another chapter, we'll also see how to manipulate the VGA's palette. Shifting color assignments around can create some occasionally stunning animation effects that can be almost impossible to reproduce with other techniques.

Summary

In this chapter, we've dealt with the linear sprite format for storing Mode 13h sprites, and we've seen how to display sprites and read new sprites from the screen. We've seen examples of simple animation, we've learned how the vertical retrace works and how we can use it to our advantage, and we've learned about the "dirty rectangles" technique for preserving backgrounds.

References to material

Ferraro, Richard F. "Programmer's Guide to the EGA, VGA, and Super VGA Cards, Third Edition". Reading, MA, USA: Addison-Wesley Publishing Co., Inc., 1994. ISBN: 0-201-62490-7.

(See pages 335 and 336 for the vertical retrace status bit and its register.)

  

Copyright 1997, Kevin Matz, All Rights Reserved. Last revision date: Wed, Jun. 04, 1997.

[Go back]

A project