Black Art of 3D Game Programming, Chapter 4: Waking the Dead with Animation
From Dpfileswiki
We now have the power to open the gateway to Cyberspace using the VGA card to create a subspace distortion field. We can project images from Cyberspace to the PC and display them on the monitor. The next step in our evolution of Cybersorcery is to learn how to animate these objects—or in other words, how to bring them to life. Animation is the technique of moving objects around on the screen and changing their form as a function of time. In the 3D games we’ll create, most of these objects will be polygon based. However, as an introduction to animation, we will start with 2D objects and bitmaps to gain a firm grasp on the basic concepts. Moreover, there are many uses for 2D animation in a 3D game. Bitmapped animation is commonly mixed with 3D graphics to create special effects such as explosions, instrument panels, and more.
Introduction to Animation
Animation is a broad term and has many meanings. In the context of video games it generally means to move objects around on the screen in real-time. The objects can be 2D, 3D, or a mixture of both. However, the steps you take to accomplish animation are relatively the same regardless of dimensionality. Animation for computer games is performed a frame at a time. The position and state of the player and the other elements of the game are used to draw the view of the universe a number of times a second, similar to a movie. Most games have a frame rate of 15 to 30 FPS (frames per second). As the frames are displayed, the objects in the universe move and change. This is what we call animation.
In a 3D game the images we draw will represent the player’s universe and objects within it. We will render these images using polygons or flat facets to approximate the objects in the universe. But before we learn about 3D animation, we will start off with something that is a bit easier to understand such as 2D bitmapped graphics. The concepts we learn for 2D animation will all transfer over to 3D. Moreover, we’re going to need 2D bitmapped graphics in our 3D games anyway, so now is a good time to learn.
For our discussions of animation we will always refer to the computer display in screen coordinates. In other words, the origin of the screen is at (0,0) located at the upper-left corner of the screen, and the bottom-right corner of the screen is (319,199). Figure 4-1 shows a representation of this. Note that 320x200 is the resolution of mode 13h, which is what we’ll be working with for the most part.
The Animation Cycle
We have learned how to plot pixels and perform simple text bitmapped graphics, but how are objects moved and animated? Like anything else in life, objects are moved and animated by following a sequence of steps:
Method I: Animation Cycle for Backgrounds of Solid Color
- Erase the object.
- Move the object to another location.
- Draw the object.
- Repeat process.
Figure 4-2 shows a small ship being moved using this technique. Now there are some details we should discuss. For example, the first step of erasing the object can be accomplished in many ways. The object could be drawn in the background color and that would erase it, but it would erase any image in the background! Say we want to animate a little man walking across a street scene. The street scene would be drawn on the screen and should be static. When we went to animate the little man, the first step of the animation cycle would erase the object. During this erasure, the image of the street at the location of the man would be obliterated. To solve this problem, we must refine what we mean by erase.
If objects are going to be animated on top of a predrawn background, then whenever the objects are drawn, the imagery under them must be saved. During the erasure phase, instead of drawing the object with background color, the imagery that was previously under the object is restored, and in one operation the object is erased and the background is replaced. This is the way nondestructive animation works. The background under an object is scanned, and then the object is drawn at its new position. When it’s time to move the object, the background is replaced and the cycle starts over. Hence, the new sequence of steps to perform proper animation:
Method II: Animation Cycle for Backgrounds with Images
- Replace background previously scanned where object was drawn.
- Move the object.
- Scan background under object.
- Draw object.
- Repeat the process.
The only unanswered detail is, how can we replace the background on the first cycle when the background hasn’t been scanned? Well, it’s performed as part of the startup or initialization of the object’s animation. Before the object is animated and the loop is entered, the background is scanned. You can think of this as step 0. Figure 4-3 shows the new animation process in action.
In general, Method II is usually used in games that have some kind of predrawn background. However, in 3D games, there isn’t a background most of the time since the entire world is redrawn every frame. There is no compelling reason to scan the background, since it is continually changing. If the background changes every frame, steps 0, 1, and 3 can be deleted. All that must be done to move and animate the object is to draw it over the newly drawn background every frame. This is a very important factor because deleting those three steps speeds up the rendering quite a bit. We will see demonstrations of both of these techniques later in the chapter.
Another aspect of animation we need to consider here is flicker. Flicker is a term used in computer graphics to describe an annoying aspect of animation: seeing the objects being drawn and erased. If you animate the objects in the video buffer itself, it’s possible that the user will “see” the objects being erased and drawn since the VGA hardware is constantly accessing the video buffer and using it to draw the screen. The solution is to draw each frame of animation in an invisible work. There are two main ways to do this. The first is with double buffering, which we will use for the remainder of the book and discuss shortly. The second method is page flipping, which is very similar but a bit more hardware dependent (as we saw using mode Z). We won’t be using page flipping for the animation in this book; but as I promised, there will be a demo of it supporting mode Z at the end of the chapter, so you can see what it is.
Before we dive off the deep end and get into a mega high-tech discussion, let’s digress for a second and talk about some of the more aesthetic aspects of a video game.
Cutting to Another Screen
You’ve just written a cool game and it’s almost ready to put on the market, but it has one flaw: every time the scenery changes or a new display comes up, there is an abrupt change instead of a smooth transition. Just as movies have screen fades, wipes, and other special effects, a game should too. These screen transitions are used when there is a change of universe or environment, for example. They’re just a nice way to make an entrance or an exit. Screen transitions usually operate directly in the video buffer and manipulate the image that’s within it. For example, you can dissolve the image in the video buffer by plotting colored pixels randomly, fade the lights by decreasing the values in the color registers slowly, or do something else more creative.
With that in mind, let’s write a function that takes as a parameter the screen transition that we want it to perform. The function will then perform the screen transition and return. We’ll use the screen transition function in the demos for this chapter, allowing them to make graceful exits when complete. We’ll name the function Screen_Transition( ), and it will take a single parameter that commands it to do a specific effect. The effects supported are defined below:
// screen transition commands #define SCREEN_DARKNESS 0 // fade to black #define SCREEN_WHITENESS 1 // fade to white #define SCREEN_WARP 2 // warp the screen image #define SCREEN_SWIPE_X 3 // do a horizontal swipe #define SCREEN_SWIPE_Y 4 // do a vertical swipe #define SCREEN_DISSOLVE 5 // a pixel dissolve
The result of each of the effects should be obvious from the names given to them except for SCREEN_WARP. The code for this one has been left blank, and you get to put in your own transition! Listing 4-1 shows the code for the screen transition function.
Listing 4-1 A screen transition function
void Screen_Transition(int effect)
{
// this function can be called to perform a myriad of screen transitions
// to the video buffer, note I have left one for you to create!
int pal_reg; // used as loop counter
long index; // used as loop counter
RGB_color color; // temporary color
// test which screen effect is being selected
switch(effect)
{
case SCREEN_DARKNESS:
{
// fade to black
for (index=0; index<20; index++)
{
// loop thru all palette registers
for (pal_reg=1; pal_reg<255; pal_reg++)
{
// get the next color to fade
Read_Color_Reg(pal_reg,(RGB_color_ptr)&color);
// test if this color register is already black
if (color.red > 4) color.red-=3;
else
color.red = 0;
if (color.green > 4) color.green-=3;
else
color.green = 0;
if (color.blue > 4) color.blue-=3;
else
color.blue = 0;
// set the color to a diminished intensity
Write_Color_Reg(pal_reg,(RGB_color_ptr)&color);
} // end for pal_reg
// wait a bit
Time_Delay(1);
} // end for index
} break;
case SCREEN_WHITENESS:
{
// fade to white
for (index=0; index<20; index++)
{
// loop thru all palette registers
for (pal_reg=0; pal_reg<255; pal_reg++)
{
// get the color to fade
Read_Color_Reg(pal_reg,(RGB_color_ptr)&color);
color.red+=4;
if (color.red > 63)
color.red = 63;
color.green+=4;
if (color.green > 63)
color.green = 63;
color.blue+=4;
if (color.blue >63)
color.blue = 63;
// set the color to a brighter intensity
Write_Color_Reg(pal_reg,(RGB_color_ptr)&color);
} // end for pal_reg
// wait a bit
Time_Delay(1);
} // end for index
} break;
case SCREEN_WARP:
{
// this one you do!!!!
} break;
case SCREEN_SWIPE_X:
{
// do a screen swipe from right to left, left to right
for (index=0; index<160; index+=2)
{
// use this as a 1/70th of second time delay
Wait_For_Vertical_Retrace();
// draw two vertical lines at opposite ends of the screen
Line_V(0,199,319-index,0);
Line_V(0,199,index,0);
Line_V(0,199,319-(index+1),0);
Line_V(0,199,index+1,0);
} // end for index
} break;
case SCREEN_SWIPE_Y:
{
// do a screen swipe from top to bottom, bottom to top
for (index=0; index<100; index+=2)
{
// use this as a 1/70th of second time delay
Wait_For_Vertical_Retrace();
// draw two horizontal lines at opposite ends of the screen
Line_H(0,319,199-index,0);
Line_H(0,319,index,0);
Line_H(0,319,199-(index+1),0);
Line_H(0,319,index+1,0);
} // end for index
} break;
case SCREEN_DISSOLVE:
{
// dissolve the screen by plotting zillions of little black dots
for (index=0; index<=300000; index++)
Write_Pixel(rand()%320,rand()%200, 0);
} break;
default:break;
} // end switch
} // end Screen_Transition
You will find the function code and header information in the library modules BLACK4.C and BLACK4.H. Let’s take a quick look at how each of the transitions works.
SCREEN_DARKNESS—This screen transition works by decrementing the RGB values of all the color registers repeatedly until each color register has a value that is near black (0,0,0). This has the effect of dimming the lights.
SCREEN_WHITENESS—The effect is the opposite of the fade to black transition. Instead of decrementing the RGB values of each color register, this transition increments them until they reach a maximum of pure white (63,63,63). This transition has the effect of being blinded by light.
SCREEN_WARP—This transition is meant for you to implement. Try to implement a kind of vortex that sucks the screen image into a single point.
SCREEN_SWIPE_X—This simple effect is achieved by performing a slow clear screen using vertical black lines that move toward the center from the edges of the screen. The function uses the Line_V( ) call and looks like a black door is being closed, obscuring the image.
SCREEN_SWIPE_Y—This is identical to the previous transition except it is done in the vertical direction instead of the horizontal by use of Line_H( ).
SCREEN_DISSOLVE—This is a favorite of mine, probably because it’s so easy to implement and looks cool. The effect is achieved by plotting 300,000 black pixels. As the pixels are drawn, the screen image is slowly “eroded” and finally disappears.
Now that we have a cool way to exit our animation demos and games let’s see how to actually implement animation.
Double Buffering
As I said, we will be using a double buffer animation system. In essence this means that we will build the next frame of animation in an invisible buffer offscreen. Then to draw the next video frame, we will copy the data from the double buffer to the video buffer using a high-speed memcpy( ) function. Although this means that we need an extra buffer of 64,000 bytes to hold the offscreen buffer, it is well worth it since using a double buffer is absolutely necessary to alleviate flicker. And 64,000 bytes isn’t going to kill us! Figure 4-4 shows the relationship between the double buffer and the video buffer. Creating a double buffer is as easy as defining a pointer and allocating 64,000 bytes to it. For example, we can define a double buffer area like this:
unsigned char far *double_buffer=NULL;
Then we use a memory allocation function to allocate 64,000 bytes for the buffer and point the pointer to it. Actually, we want to allocate the memory from the FAR heap, so we have to use _fmalloc( ) (or farmalloc( ) for Borland users).
Once the double buffer is allocated, we can access it as if it were the video buffer. We simply pretend that it has the same layout as the video buffer (which is 320 pixels per row with 200 rows). For instance, if we wanted to convert the pixel plotting function we wrote previously to work with a double buffer, we need to change only one variable in the Write_Pixel( ) function. See if you can find the change in the function in Listing 4-2.
Listing 4-2 A double buffer version of the pixel plotting function
void Write_Pixel_DB(int x,int y,int color)
{
// plots the pixel in the desired color to the double buffer
// to accomplish the multiplications
// use the fact that 320*y = 256*y + 64*y = y<<8 + y<<6
double_buffer[((y<<8) + (y<<6)) + x] = (unsigned char )color;
} // end Write_Pixel_DB
Since there's only one line of code in the whole function, it's easy to see that the reference to video_buffer has been chagned to double_buffer. That's all you need to do. If you wish, you can change every function written thus far to work with a double buffer instead of the video buffer. We won’t really need to change every function, but the ones we do change will have _DB added to their names and can be found in the library modules for this chapter.
Performing double buffer animation is very easy. You create the double buffer, do all your rendering in it, and then when the time is right, copy the double buffer into the video buffer and repeat the process each frame. By doing this you can get rid of flicker. As a second bonus, accessing normal memory is much faster than video memory, so the screen can be built up much faster. The only downside to the double buffer is that it must be copied to the video buffer to be seen by the user. But this can be done very quickly using a memcpy() function or, better yet, some inline assembly that moves WORDs or LONGs instead of BYTEs as memcpy() does.
So what do we need to create a double buffer animation system? Not much really. We need to write some functions that will create a double buffer, clear it, fill it with a color, and copy it to the video buffer. That’s about it. However, before we do this, let’s add a little bit of functionality to the double buffer system. Let’s allow it to be different heights, up to a maximum of 200 lines. For example, say we wrote a game that used the upper 180 lines of the screen for the game and the lower 20 lines for instruments. We would probably want to double buffer only the active part of the screen (the upper 180 lines) and draw directly to the video buffer to implement the instruments. Thus, our double buffer system will have a height associated with it.
We already defined the double buffer itself, but there are a couple more variables we need to keep everything straight. Here they are:
// the default dimensions of the double buffer unsigned int double_buffer_height = SCREEN_HEIGHT; // size of double buffer in WORDS unsigned int double_buffer_size = SCREEN_WIDTH*SCREEN_HEIGHT/2;
You will notice that the double_buffer_size variable tracks the size of the double buffer in WORDs instead of BYTEs. This is because the function that copies the double buffer’s image into the video buffer will use a WORD-size memory copy for speed. We’re ready to start writing. Let’s begin with a function to create a double buffer, shown in Listing 4-3.
Listing 4-3 Function to create a double buffer
int Create_Double_Buffer(int num_lines)
{
// allocate enough memory to hold the double buffer
if ((double_buffer = (unsigned char far *)_fmalloc(SCREEN_WIDTH *
(num_lines + 1)))==NULL)
{
printf("\nCouldn't allocate double buffer.");
return(0);
} // end if couldn't allocate
// set the height of the buffer and compute its size
double_buffer_height = num_lines;
double_buffer_size = SCREEN_WIDTH * num_lines/2;
// fill the buffer with black
_fmemset(double_buffer, 0, SCREEN_WIDTH * num_lines);
// everything was ok
return(1);
} // end Create_Double_Buffer
The function is really a memory allocation shell coupled with a few variable initializations. The function first tries to allocate a double buffer of the requested line size, and if successful, will set the global size and height variables and return success. In the case that there isn’t enough memory available to allocate the double buffer, an error will be printed and the function will return failure. Once the double buffer has been allocated, the next step is to fill or clear it with a value. This can be accomplished by writing a value from 0 to 255, representing the desired fill color, into the double buffer’s memory. To do this we could write something like this:
for (index=0; index<double_buffer_size*2; index++) double_buffer[index] = fill_color;
This will work, but it’s a bit slow. A better method would be to use some inline assembly language to make the process as fast as possible since clearing the screen (as we will later see) is a critical operation in 3D graphics. With that in mind, take a look at Listing 4-4 for a faster inline version.
Listing 4-4 Clearing the double buffer with inline assembly
void Fill_Double_Buffer(int color)
{
// this function fills in the double buffer with the sent color a WORD at
// a time
_asm
{
mov cx,double_buffer_size ; this is the size of buffer in WORDS
mov al, BYTE PTR color ; move the color into al
mov ah,al ; move the color in ah
les di,double_buffer ; es:di points to the double buffer
rep stosw ; fill all the words
} // end asm
} // end Fill_Double_Buffer
The function is simple if you know assembly, but if you don’t, it may seem a bit mysterious. Rule one: Never be intimidated by assembly language. It’s very simple once you get a handle on it. Let’s take a moment to see how the Fill_Double_Buffer( ) function works. The Intel processor has a set of memory movement instructions, and most of these instructions (I think all of them) use DS:SI to point to the source data, ES:DI to point to the destination data area, and CX to hold the number of BYTEs, WORDs, or LONGs to move. Finally, if the operation is a fill, then AX holds the data to be used as the filler. In our case we want to move WORDs, so in CX we place the size of the double buffer in WORDs. Then in AX we place the color value in AH and AL. Finally, we point ES:DI at the double buffer area and then use the instruction rep stosw, which roughly translates to “repeat the store word operation.” When the function is complete, the double buffer will be filled with the desired color, or cleared to black if the fill color was 0.
The next function we need to write is one that will copy the double buffer to the video buffer. Again, we could accomplish this by using a simple for loop, or even with a memcpy() function such as:
_fmemcpy((void far *)video_buffer, (void far *)double_buffer,double_buffer_size/2);
This would work and would be faster than the for loop, but it still only copies BYTEs. We need to use inline again to copy WORDs as fast as possible. And this is absolutely necessary since the double buffer must be literally blasted to the video buffer! Listing 4-5 contains the code to copy the double buffer to the video buffer.
Listing 4-5 Function to copy the double buffer to the video buffer
void Display_Double_Buffer(unsigned char far *buffer,int y)
{
// this function copies the double buffer into the video buffer at the
// starting y location
_asm
{
push ds ; save DS on stack
mov cx,double_buffer_size ; this is the size of buffer in WORDS
les di,video_buffer ; es:di is destination of memory move
mov ax,320 ; multiply y by 320 i.e. screen width
mul y
add di,ax ; add result to es:di
lds si,buffer ; ds:si is source of memory move
rep movsw ; move all the words
pop ds ; restore the data segment
} // end asm
} // end Display_Double_Buffer
The function Display_Double_Buffer( ) has a bit more functionality than it needs as a minimum, but this extra functionality will allow us to do a lot with the function. First, you will notice that the function takes a pointer to a buffer as a parameter. This will in most cases be double_buffer; but in the event that we want to copy another buffer to the video buffer, we will have the ability to do so. The second parameter is a positioning parameter. In the case that the double buffer is smaller than 200 lines, it is possible to position it on the video buffer at a location other than line 0.
Figure 4-5 shows this pictorially. We can map the double buffer down at different positions in the video buffer as long as the bottom of the double buffer doesn’t extend farther than the video buffer. This will allow us to do some cool effects. For example, if we make the double buffer 190 lines and then map it to the video buffer with each frame at a different starting y position, where 0<=y<=9, then the screen will look like it’s shaking. This technique can be used to simulate an earthquake or being hit by photon torpedoes. Also, we may wish to put the instrument panel at the top of the screen and the double buffer at the bottom, as in Figure 4-6. Therefore, sending the starting y position as a parameter allows all this to be implemented with ease.
The inline assembly is roughly the same as what was used in the fill screen, except this time around the source pointer DS:SI is used, and the memory movement instruction is changed to movsw, which means “move words.” And of course, the starting offset in the video buffer is computed based on the sent y parameter and added to the destination buffer start address in ES:DI. Basically, if you can understand how to set up the memory movement instructions in assembly, that’s about 99 percent of all the assembly language you’ll ever need to write games!
The final function needed to complete the system is a double buffer memory deallocation function that “frees” the memory used by the double buffer back to the operating system. This can be done in a couple lines, as shown in Listing 4-6.
Listing 4-6 Function to free up the memory used by the double buffer
void Delete_Double_Buffer(void)
{
// this function frees up the memory allocated by the double buffer
// make sure to use FAR version
if (double_buffer)
_ffree(double_buffer);
} // end Delete_Double_Buffer
That completes the software we need to implement a double buffer. Once we have a collection of functions to draw into the double buffer, we can write a game loop that looks like this:
//...initialize game
// create a 200 line full screen double buffer
Create_Double_Buffer(200)
// scan under all objects in game (if needed)
While(!game_over)
{
// erase all objects in double buffer
// process game logic
// scan background under all objects (if needed)
// draw all objects in double buffer
// display the next frame starting at line 0
Display_Double_Buffer(video_buffer,0);
// synchronize to some time base
Time_Delay(1); // wait 1/18th of a second
} // end main loop
Of course, we have a long way to go to add the functionality of all those comments, but we're getting there! Let's move on to a simple bitmapped graphics.
Simple Bitmapped Graphics
We learned in Chapter 3 that a bitmap is a rectangular matrix of pixels that can be drawn on the video screen (or double buffer) by copying the pixels from the source bitmap to the destination position on the screen. Figure 4-7 shows this process of mapping a bitmap onto the video screen. We actually implemented a crude form of bitmapping with the text blitter that drew text on the screen based on the ROM character set. However, this bitmapping wasn’t really bitmapping since there wasn’t a one-to-one correlation between the data of the characters and the images they represented. If you recall, we had to translate the bit pattern of each row into a byte pattern. Now we are going to do real bitmapping. This will provide us with the basic techniques to implement animation objects referred to as sprites.
Each pixel in the collection of pixels that makes up a bitmap is represented by a single byte. This collection of pixels is usually stored in a one-dimensional array. Although it may seem more logical to use a two-dimensional array, this is a bit slower than a one-dimensional array due to memory accessing. Therefore we’ll be using one-dimensional arrays to represent all bitmaps in the book.
Alrighty then. Let’s say we wanted to define a bitmap that was 8x8 pixels. This would take a total of 8*8=64 bytes of memory, so we could use a definition such as:
unsigned char bit_creature[64];
And we would use the convention that each row of the bitmap would be represented by 16 elements in the array, so the object would have an abstract data structure like this:
- Row 0—Elements 0–7
- Row 1—Elements 8–15
- Row 2—Elements 16–23
- Row 3—Elements 24–31
- Row 4—Elements 32–39
- Row 5—Elements 40–47
- Row 6—Elements 48–53
- Row 7—Elements 54–63
Hence, to locate a single pixel in the bitmap at a location (x,y), where (x,y) can each range from 0 through 7, the array location would be calculated as follows:
offset = y*8 + x;
That seems reasonable since there are 8 bytes per row. In essence, we are emulating the compiler’s 2D arrays with a one-dimensional array. Figure 4-8 depicts a bitmap and the offset location of some pixels within the image for an 8x8 bitmap. To draw a bitmap on the video screen or double buffer, we can use a pair of simple for loops to copy each line of the image into the designation buffer. For example, if bit_creature[ ] had an image in it and we wanted to map it into the video buffer at (x,y), we could use the following code:
for (row=0; row<8; row++)
for (column=0; column<8; column++)
video_buffer[(y+row)*320+x+column] = bit_creature[row*8+column];
The above fragment will work perfectly, but there is one little problem. It’s just about the slowest way that a bitmapped image can be drawn. Also, it doesn’t take transparency into consideration. Remember that some portions of an image should be opaque and some shouldn’t. The opaque portions of a bitmap should be drawn while the transparent portions (usually represented by black in the bitmaps) should not be drawn so the background can be seen through them. To take this point into consideration, an if statement must be used in the loop to test whether the pixel is transparent before drawing.
for (row=0; row<8; row++)
for (column=0; column<8; column++)
{
// get next pixel of this row
pixel = bit_creature[row*8+column];
// test for transparency
if (pixel)
video_buffer[(y+row)*320+x+column] =pixel;
} // end for
There are times when transparency isn’t important, for example, if the bitmap being drawn is part of the background. But in general, a transparency option should be implemented in the loop. In any case, neither of the above functions is going to work. They both have too much math involved and, with some optimizations, you can speed them up about 10 to 50 times (depending on your processor). The main optimization is to get rid of the multiplications (yuk) and precompute the starting offset at the entrance to the loop; then use simple additions to move the pointers in the source bitmap and destination buffer. We will see this shortly when we implement an actual bitmap engine, but for now keep those factors in mind.
The two previous fragments draw in the video buffer, which as we already learned isn’t going to be done all that much. We will be using the double buffer most of the time, but making the fragments work in the double buffer is as easy as changing a single variable from video_buffer to double_buffer. Hence, most of the functions we’ll write from now on will take as a parameter a pointer to the destination buffer, which could be the video buffer or a double buffer. This level of indirection allows a little more flexibility in the routines. Otherwise, we would have to make two copies of all the routines, one for the video buffer and one for the double buffer (which is actually quite common in real designs). Even passing a single variable to a function is sometimes unwise in time-critical graphics functions!
A Simple Bitmap Engine
At this point, we’re ready to take a first stab at writing some bitmapping functions that will pave the way for the more advanced functions later in the chapter. The functions that we are about to write will serve as a good exercise to see just how simple bitmapped graphics really are. Let’s begin by designing a minimal data structure that will suffice to track a bitmap. We will need a buffer to hold the memory for the bitmap image, a pair of (x,y) coordinates to locate its position, and a final pair of variables that record the size of the bitmap in pixels. A data structure that meets these criteria is:
// this is the typedef for a bitmap
typedef struct bitmap_typ
{
int x,y; // position of bitmap
int width,height; // size of bitmap
unsigned char far *buffer; // buffer holding image
} bitmap, *bitmap_ptr;
The bitmap structure has all the elements we need. Pay close attention to the way the buffer is defined as an (unsigned char far *). This is to alleviate casting problems when an element of a bitmap is assigned to a signed integer. If the bitmap buffer was not unsigned, there would be a sign extension, and a pixel value of 200 would be interpreted as a negative number when assigned to an int. Of course, this won’t be an issue if the integer that the pixel value is being assigned to is unsigned, but many times it is, hence this definition gives us a little insurance.
Drawing a Bitmap
Now that we have the bitmap structure in place, let’s write a function that will map it down into a destination buffer. The destination buffer will be sent as a parameter, so we can use the same bitmap functions for both video buffer output and double buffer output. Figure 4-9 depicts the function we wish to perform. The source image is contained within the bitmap structure, and it is mapped down at the desired (x,y) coordinates of the destination buffer. Of course, the width of the destination buffer must always be 320 bytes, but the height can be any size. Before writing the function, let’s add one more feature to it. We have been talking about transparency from time to time and have learned that a test must be made for every pixel in the rendering loop to test for transparency. However, if a bitmap or object doesn’t have any transparent portions, this test can be thrown out, and a very fast memcpy() function can be used to transfer the bitmap data to the destination buffer. Therefore, we are going to add this ability to our bitmap drawing function to allow it to select whether transparency is on or off. This gives us more speed in the cases that we don’t need to test for transparency. Taking all of the previous requirements into consideration, our bitmap drawing function is shown in Listing 4-7.
Listing 4-7 Bitmap drawing function
void Bitmap_Put(bitmap_ptr image, unsigned char far *destination,int transparent)
{
// this function draws a bitmap on the destination buffer which can
// be a double buffer or the video buffer
int x,y, // looping variables
width,height; // size of bitmap
unsigned char far *bitmap_data; // pointer to bitmap buffer
unsigned char far *dest_buffer; // pointer to destination buffer
unsigned char pixel; // current pixel value being processed
// compute offset of bitmap in destination buffer. Note: all video or
// double buffers must be 320 bytes wide!
dest_buffer = destination + (image->y << 8) + (image->y << 6) + image->x;
// create aliases to variables so the structure doesn't need to be
// dereferenced continually
height = image->height;
width = image->width;
bitmap_data = image->buffer;
// test if transparency is on or off
if (transparent)
{
// use version that will draw a transparent bitmap (slightly slower)
// draw each line of the bitmap
for (y=0; y<height; y++)
{
// copy the next row into the destination buffer
for (x=0; x<width; x++)
{
// test for transparent pixel i.e. 0, if not transparent then draw
if ((pixel=bitmap_data[x]))
dest_buffer[x] = pixel;
} // end for x
// move to next line in double buffer and in bitmap buffer
dest_buffer += SCREEN_WIDTH;
bitmap_data += width;
} // end for y
} // end if transparent
else
{
// draw each line of the bitmap, note how each pixel doesn't need to be
// tested for transparency hence a memcpy can be used (very fast!)
for (y=0; y<height; y++)
{
// copy the next row into the destination buffer using memcpy for speed
_fmemcpy((void far *)dest_buffer,
(void far *)bitmap_data,width);
// move to next line in destination buffer and in bitmap buffer
dest_buffer += SCREEN_WIDTH;
bitmap_data += width;
} // end for y
} // end else non-transparent version
} // end Bitmap_Put
The function has three distinct phases of operation. During the initialization phase, the starting memory location in the destination buffer where the bitmap will be placed is computed. This starting address is stored in dest_buffer. Similarly, bitmap_data is aliased to the buffer region within the bitmap structure so that a pointer dereference isn’t needed every cycle to access the bitmap data. This is a typical technique used to cache variables that are within structures to gain speed. The next phase of the function tests the transparent flag to determine whether the bitmap should be drawn with or without transparency. Then a jump is made to either the transparent version of the drawing code or the nontransparent version. In either case, each pixel of the source bitmap is transferred to the destination buffer (which is usually the double buffer), and the address of the next line of the source bitmap and destination bitmap is recomputed for the next cycle.
Scanning a Bitmap
When writing bitmapping functions, it’s always a good idea to make the function set orthogonal. In other words, for every function there’s an antifunction (kinda like antimatter, but different). In the case of Bitmap_Put(), its inverse function would be Bitmap_Get(), which would extract or scan a region from a source buffer. This source buffer could be the video buffer, the double buffer, or whatever. Both the put and get functions are absolutely necessary if animation is to take place, since part of the animation cycle needs to scan the background under an image before it is drawn so that the background can be replaced. This is the motivation behind the Bitmap_Get() function. Another motivation is that we need to have some method to load in bitmaps themselves, otherwise how are we going to get the actual bitmaps for the objects to animate? Later we’ll learn to scan objects from PCX files loaded from disk, but the point is we need to be able to scan objects.
Writing a Bitmap_Get() function is about half as hard as the put function. This is because we don’t need to have two versions of the code to scan for transparent and nontransparent. In all cases, we want to extract a rectangular region that has dimensions width by height as defined in the structure. And there is no transparency test, so we can use the faster memcpy() type function to extract each line of the source bitmap. Listing 4-8 contains the code we need to do just that.
Listing 4-8 Function to extract a bitmap from a source image buffer
void Bitmap_Get(bitmap_ptr image, unsigned char far *source)
{
// this function will scan a bitmap from the source buffer
// could be a double buffer, video buffer or any other buffer with a
// logical row width of 320 bytes
unsigned int source_off, // offsets into destination and source buffers
bitmap_off;
int y, // looping variable
width,height; // size of bitmap
unsigned char far *bitmap_data; // pointer to bitmap buffer
// compute offset of bitmap in source buffer. Note: all video or double
// buffers must be 320 bytes wide!
source_off = (image->y << 8) + (image->y << 6) + image->x;
bitmap_off = 0;
// create aliases to variables so the structure doesn't need to be
// dereferenced continually
height = image->height;
width = image->width;
bitmap_data = image->buffer;
// draw each line of the bitmap, note how each pixel doesn't need to be
// tested for transparency hence a memcpy can be used (very fast!)
for (y=0; y<height; y++)
{
// copy the next row into the bitmap buffer using memcpy for speed
_fmemcpy((void far *)&bitmap_data[bitmap_off],
(void far *)&source[source_off],width);
// move to next line in source buffer and in bitmap buffer
source_off += SCREEN_WIDTH;
bitmap_off += width;
} // end for y
} // end Bitmap_Get
The function works in much the same way as Bitmap_Put( ) does. First the source memory offset and bitmap image buffer are aliased, then a for loop scans each line of the source image into the bitmap image buffer, and that’s about it.
Using Bitmap_Put() and Bitmap_Get(), we could write a simple animation program that moves an object around the screen and so forth; however, at this point the functions we have aren’t really designed for the purpose of animation. We need a more robust data structure and a couple more functions to really do animation right. This is where the concept of sprites comes in. We will learn about them shortly and the power that they offer for 2D animation. But before that, let’s discuss where on Earth we’re going to get all the artwork and imagery for our games.
Loading Images from Outside Sources
I’m sure you’ve seen or heard of many different paint and illustration programs for the PC. These programs are used to create artwork for many purposes. In our case, we need a source of artwork and bitmapped images that we can import into our games. Standards are hard to come by in the graphics world, and there are probably as many different graphics file formats as there are graphic paint programs; however, over time a few formats have become popular and are used the most. These are GIF, TIFF, TARGA, PCX, and BMP. The only one we need to concern ourselves with is PCX. The PCX standard was invented by ZSoft as part of the paint program, PCPaintbrush, which they made years ago. At the time they wrote the program, there wasn’t a good bitmapped file format, so they invented one.
The PCX format is very easy to decode and supports all kinds of resolutions and color depths; moreover, it supports a simple data compression method, so that the image files take as little space as possible. We will be working primarily with PCX files, but there is no reason you can’t write other file readers if you wish. PCX files are generated by PC-Paintbrush, but where else can they be found? The fact of the matter is that it doesn’t really matter what paint program you use because there are hundreds of programs that will convert from one format to another.
Most paint programs themselves allow you to save and load images in most of the popular formats. However, I suggest that you use a paint program that supports 256 colors and one that can function in mode 13h. It is very important that you can see what your artwork looks like in the native mode it will be used in. For example, if you draw objects for a game in 640x480 and then import them into your game that runs in 320x200, the images are not going to look right. So the moral of the story is: Use a paint program that supports mode 13h. I have worked with DPaint and Deluxe Animation (Electronic Arts) and PC-Paintbrush (ZSoft). They are all excellent paint programs and all support mode 13h. I suggest you use one of these as a start.
Reading PCX Files
Once you have selected a paint program or have artwork that is in the 320x200, PCX file format, the next step is to figure out how to read the data in the file. PCX files consist of three main parts:
- Header section
- Image data
- Color palette
The header section is a typical header with information describing the kind of image that is contained within the image section. In our case, we will just skip the header since we already know that the image is a 320x200, 256-color image; but to be complete, here is the C version of the header for a PCX file:
// the PCX file structure
typedef struct pcx_header_typ
{
char manufacturer; // the manufacturer of the file
char version; // the file format version
char encoding; // type of compression
char bits_per_pixel; // number of bits per pixel
int x,y; // starting location of image
int width,height; // size of image
int horz_res; // resolution in DPI (dots per inch)
int vert_res;
char ega_palette[48]; // the old EGA palette (usually ignored)
char reserved; // don't care
char num_color_planes; // number of color planes
int bytes_per_line; // number of bytes per line of the image
int palette_type; // 1 for color, 2 for grey scale palette
char padding[58]; // extra bytes
} pcx_header, *pcx_header_ptr;
If you count the number of bytes that make up the entire header file, you will come up with a grand total of 128 bytes. Hence, the header section is always the first 128 bytes of a PCX file.
The next section of the PCX file is the compressed image that represents the full 320x200-pixel image. In actuality the image data can be any size. It may be 50 bytes or it may be 50,000 bytes, but when it’s decompressed and translated into pixels, there will be exactly 64,000 pixels or bytes that are generated from the file. This creates a little problem. We don’t know how big the image section is, so how can we find the start of the color palette information? We will use the file seeking function supported by C to move the file pointer to the end of the PCX file and back up to the color palette section without knowing how big the image section is. Let’s talk about how the image is compressed.
PCX files use a simple compression method that is based in run length encoding (RLE). It is very common for bitmapped images to have long runs of the same color. In the best case an entire screen could be filled with a single color. Thus, we could encode this as the number of bytes that the color ran (64,000 in this case) and the color. This is the basis of run length encoding. It works by scanning the image and looking for runs of pixels that have the same value. When a run is found, the RLE algorithm computes how many pixels have the same color; then the color itself along with the length of the run are written to the compressed output file. That’s the basic idea behind textbook RLE. However, ZSoft made a couple changes to it. I’m not sure why, but my intuition tells me that there was some kind of color palette motivation. Anyway, here’s how RLE works for PCX files. The first byte of the image data is read. If it is in the range of 0 to 191, it is written to the decompression buffer or destination buffer without change. However, if the value read is between 192 and 255, it’s a pixel run and should be decompressed. To decompress the run, the value of 192 is subtracted from the previously read value. The result is the number of bytes or pixels that need to be replicated. The value of the pixel can be found by reading the next byte in the file.
For example, take a look at Figure 4-10 to see the decompression of a few values in the beginning of a PCX file. Referring to the figure, the first three values are between 0 and 191, so they aren’t changed, they are copied directly into the decompression buffer. But the fourth number (200) is between 192 and 255; therefore, 192 is subtracted from 200 resulting in 8, and then the next data value (5) is copied 8 times into the destination buffer. Hence, the 200 described the run length and the 5 was the data to replicate. The last value is 26 and is less than 192; thus, it is directly copied to the decompression buffer.
The question is, how do we know when the PCX image has been totally decompressed? The best way is to keep a count of the number of bytes written to the destination decompression buffer. When it equals 64,000 we know that an entire file has been decompressed. But remember, the number of bytes read may only be a few, which when decompressed make up the total of 64,000!
The last section of the PCX file contains the color palette information. This section contains 256 descriptors that each have 3 bytes (one for red, green, and blue), for a total of 256*3=768 bytes in the color palette. Therefore, to read the color palette information for the PCX image, we can move the random access file pointer to the end of the PCX file and then read 768 bytes in 3-byte chunks. Figure 4-11 shows the relationship between the header, image data, and palette information. You see that the first 3 bytes define color register zero, the next 3 bytes define color register one, and so on. However, you’ll also note that each RGB value needs to be scaled down by a factor of 4. This is because the PCX file format uses “Truecolor” or 24-bit color descriptors, and if you remember, the VGA only uses 6 bits per channel or a total of 18-bit color. Alas, we must divide each RGB set by a factor of 4 or shift to the right 2 bits to scale the values down.
We now know all the little secrets of PCX files, so let’s write some functions to load them into the computer. First, let’s add another data structure that will contain the header of a PCX file, the full 64,000 bytes of image data, and the color palette. Here’s a structure that will work:
// this holds the PCX header and the actual image
typedef struct pcx_picture_typ
{
pcx_header header; // the header of the PCX file
RGB_color palette[256]; // the palette data
unsigned char far *buffer; // a pointer to the 64,000 byte buffer
// holding the decompressed image
} pcx_picture, *pcx_picture_ptr;
The pcx_picture structure will be used to encapsulate an entire PCX image, so we can load more than one image at a time. Now we’re ready to write a function that will load a PCX file. But first let’s write a pair of functions that will allocate the memory for a pcx_picture and release it. This will make the application programming a little cleaner. Listing 4-9 contains the functions to allocate and deallocate the memory for a pcx_picture.
Listing 4-9 Memory allocation functions for PCX file structures
int PCX_Init(pcx_picture_ptr image)
{
// this function allocates the buffer that the image data will be loaded into
// when a PCX file is decompressed
if (!(image->buffer = (unsigned char far *)_fmalloc(SCREEN_WIDTH * SCREEN_HEIGHT + 1)))
{
printf("\nPCX SYSTEM - Couldn't allocate PCX image buffer");
return(0);
} // end if
// success
return(1);
} // end PCX_Init
void PCX_Delete(pcx_picture_ptr image)
{
// this function de-allocates the buffer region used for the pcx file load
_ffree(image->buffer);
} // end PCX_Delete
The functions don’t do much except take a pointer to a pcx_picture and allocate memory from the FAR heap or deallocate it, but they make the process easier than doing it inline in the game code all the time. Thus, we can load a PCX file, work with it, and then delete it. You now have everything you need to load and decompress the PCX image data, so I’ll leave this function to you. The next thing we are going… Just kidding! The final function we need is a loader function that will open the PCX file on disk, load the header, image, and palette, and then close the file. The function to do this is called PCX_Load() and it’s shown in Listing 4-10.
Listing 4-10 Loading a PCX file
int PCX_Load(char *filename, pcx_picture_ptr image,int load_palette)
{
// this function loads a PCX file into the image structure. The function
// has three main parts: 1. load the PCX header, 2. load the image data and
// decompress it and 3. load the palette data and update the VGA palette
// note: the palette will only be loaded if the load_palette flag is 1
FILE *fp; // the file pointer used to open the PCX file
int num_bytes, // number of bytes in current RLE run
index; // loop variable
long count; // the total number of bytes decompressed
unsigned char data; // the current pixel data
char far *temp_buffer; // working buffer
// open the file, test if it exists
if ((fp = fopen(filename,"rb"))==NULL)
{
printf("\nPCX SYSTEM - Couldn't find file: %s",filename);
return(0);
} // end if couldn't find file
// load the header
temp_buffer = (char far *)image;
for (index=0; index<128; index++)
{
temp_buffer[index] = (char)getc(fp);
} // end for index
// load the data and decompress into buffer, we need a total of 64,000 bytes
count=0;
// loop while 64,000 bytes haven't been decompressed
while(count<=SCREEN_WIDTH * SCREEN_HEIGHT)
{
// get the first piece of data
data = (unsigned char)getc(fp);
// is this a RLE run?
if (data>=192 && data<=255)
{
// compute number of bytes in run
num_bytes = data-192;
// get the actual data for the run
data = (unsigned char)getc(fp);
// replicate data in buffer num_bytes times
while(num_bytes-->0)
{
image->buffer[count++] = data;
} // end while
} // end if rle
else
{
// actual data, just copy it into buffer at next location
image->buffer[count++] = data;
} // end else not rle
} // end while
// load color palete
// move to end of file then back up 768 bytes i.e. to beginning of palette
fseek(fp,-768L,SEEK_END);
// load the PCX palette into the VGA color registers
for (index=0; index<256; index++)
{
// get the red component
image->palette[index].red = (unsigned char)(getc(fp) >> 2);
// get the green component
image->palette[index].green = (unsigned char)(getc(fp) >> 2);
// get the blue component
image->palette[index].blue = (unsigned char)(getc(fp) >> 2);
} // end for index
// time to close the file
fclose(fp);
// change the palette to newly loaded palette if commanded to do so
if (load_palette)
{
// for each palette register set to the new color values
�� for (index=0; index<256; index++)
{
Write_Color_Reg(index,(RGB_color_ptr)&image->palette[index]);
} // end for index
} // end if load palette data into VGA
// success
return(1);
} // end PCX_Load
The function takes as parameters the file name of the PCX file to load, a pointer to the previously initialized pcx_picture that the information should be loaded into, and finally, a flag that determines whether the palette information should be loaded into the palette registers of the VGA. Many times when you create imagery in a paint program, the paint program will have its own palette or maybe a palette that you created. In either case, you have to load that palette if the images are to be colored correctly. Otherwise, when you view or manipulate the images from the PCX file, they will have the wrong colors. This is the purpose of the load_palette flag. It tells the PCX_Load() function that it should reprogram all the color registers in the VGA to the values within the PCX file’s palette.
We are almost ready to see the PCX functions in action, but we need one more very simple function. And that’s to copy the image in the pcx_picture buffer to the video buffer so we can see it. A function to do this is as easy as a single memcpy() function, but we’ll use inline assembly for speed. Take a look at Listing 4-11.
Listing 4-11 Copying the image from a PCX file in the video buffer
void PCX_Show_Buffer(pcx_picture_ptr image)
{
// copy the pcx buffer into the video buffer
char far *data; // temp variable used for aliasing
// alias image buffer
data = image->buffer;
// use inline assembly for speed
_asm
{
push ds ; save the data segment
les di, video_buffer ; point es:di to video buffer
lds si, data ; point ds:si to data area
mov cx,320*200/2 ; move 32000 words
cld ; set direction to forward
rep movsw ; do the string operation
pop ds ; restore the data segment
} // end inline
} // end PCX_Show_Buffer
I admit that it could have been done with a memcpy() instead of inline assembly, but this is a bit faster, and I really want you to feel comfortable with the inline assembler to do tasks such as this. At this point, we are ready for a demo to see all the PCX functions in action. The name of the demo is PCXDEMO.EXE and can be found in this chapter’s directory along with the source file PCXDEMO.C. If you want to compile it yourself, you will need to add the library module BLACK4.C and its header file BLACK4.H. The demo loads a picture of a really scary monster and then displays it. To exit the program, press any key and one of the transitions will fade the image away. Listing 4-12 contains the code.
Listing 4-12 PCX file demo
// PCXDEMO.C - A pcx file demo that loads a pcx file and displays it
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
// G L O B A L S ////////////////////////////////////////////////////////////
pcx_picture image_pcx; // general PCX image used to load background and imagery
// M A I N ///////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
// set the graphics mode to mode 13h
Set_Graphics_Mode(GRAPHICS_MODE13);
// load the screen image
PCX_Init((pcx_picture_ptr)&image_pcx);
// load a PCX file (make sure it's there)
if (PCX_Load("andre.pcx", (pcx_picture_ptr)&image_pcx,1))
{
// copy the image to the display buffer
PCX_Show_Buffer((pcx_picture_ptr)&image_pcx);
// delete the PCX buffer
PCX_Delete((pcx_picture_ptr)&image_pcx);
// wait for a keyboard press
while(!kbhit()){}
// use a screen transition to exit
Screen_Transition(SCREEN_DARKNESS);
} // end if pcx file found
// reset graphics to text mode
Set_Graphics_Mode(TEXT_MODE);
} // end main
A Sprite Structure
We have covered animation, bitmaps, and PCX files. Now we’re ready to design and implement a fairly complex 2D bitmapped graphics engine. Although the focus of this book is 3D, a majority of displays and animation will still be done in 2D, so having the functionality is an integral part of the overall 3D package. The animation objects that we’re going to focus on are called sprites. I personally don’t know the origin of the word, but it started back in the days of the Atari 800 and Commodore 64. We will continue the tradition and refer to them as sprites also.
Roughly defined, a sprite is an object that can move around on the screen without disturbing the background under it. That’s easy enough to implement. However, we are going to be using sprites to represent objects and possibly creatures in our games; thus, we are going to add many features to them to make them more powerful. For one thing, each sprite will have multiple frames of animation or multiple images that can all be used to represent the sprite. For example, Figure 4- 12 shows an abstract representation of a walking alien sprite. The alien has a position, timing variables, counters, a buffer for the background under it, and finally, an array of images that can be used to animate it.
There is no single best way to implement a sprite engine. We just want to make sure that the sprites can do what we need them to do. Therefore, why don’t we start by designing the data structure we’ll be using for our sprites. If we add all the attributes we’ve spoken of plus a few more to round out the sprite definition, the results are something like this:
// this is a sprite structure
typedef struct sprite_typ
{
int x,y; // position of sprite
int width,height; // dimensions of sprite in pixels
int counter_1; // some counters for timing and animation
int counter_2;
int counter_3;
int threshold_1; // thresholds for the counters (if needed)
int threshold_2;
int threshold_3;
unsigned char far *frames[MAX_SPRITE_FRAMES]; // array of pointers to
// the images
int curr_frame; // current frame being displayed
int num_frames; // total number of frames
int state; // state of sprite, alive, dead...
unsigned char far *background; // image under the sprite
int x_clip,y_clip; // clipped position of sprite
int width_clip,height_clip; // clipped size of sprite used
int visible; // by sprite engine to flag
// if a sprite was invisible last
// time it was drawn hence the background
// need not be replaced
} sprite, *sprite_ptr;
The function of each field is rather important, so let’s take the time to understand the motivation for each of them. The variables x and y track the absolute position of the sprite on the display buffer itself (which could be a double buffer or the video memory); therefore, if the sprite’s position was (160,100), the sprite would be drawn in the middle of the screen (or double buffer). The next variables, width and height, are similar to the ones in the bitmap structure. A sprite also will be defined by a rectangular bitmap, hence width and height are needed to track its size. The variables named counter_1, counter_2, and counter_3 work in conjunction with threshold_1, threshold_2, and threshold_3 as timing variables. They can be used for anything we wish. As you program games, you will find that there are a lot of little details that need to be tracked, and that’s the function of these variables. Next is an array of pointers that contain pointers to the cells of animation that make up the sprite. For example, a particular sprite may have five frames of animation; therefore, the first five pointers in this array would be pointing to the bitmaps for these images.
The next set of variables track the “state” of the sprite. The curr_frame variable is used to hold the current frame of animation that the sprite is displaying. Again, this is just a place holder, so our functions don’t have to keep track of these items. The next variable, num_frames, contains the number of animation cells that are within the sprite. Some sprites may have one animation cell, some may have ten, but in any case there is a record of this information. The next variable is a bit hard to explain at this point, but let’s see if we can get a good handle on it. In a game, whether it is 2D, 3D, or something else, the objects in the game must have some kind of “state” associated with them. There must be a record of whether the object is hunting down the player, exploding, dead, teleporting, or whatever. That is what the state field is for. It is a completely definable record of the concept of “state.” For example, in one game it may take on only a couple values: one value to mark a sprite as “alive” and another to mark it as “dead.” It’s up to you and your design, but this field gives you the ability to record that kind of information.
The next field, background, holds the background image under the sprite and is very important. If you remember, one of the most important aspects of a sprite is that it can move around the screen without destroying the background. This is accomplished by saving the background under a sprite (at the right moment) into the background buffer. Finally, the last few fields of the sprite structure are for clipping, which we will get to later in the chapter. For now we know all we need to, so let’s get cracking!
We could begin by writing functions to draw, erase, and scan under sprites, but this time we are going to consider a very important detail. And that’s the images that represent the sprite! In the bitmap examples, we learned the mechanics of creating the bitmaps, drawing them, and deallocating them, but we never really had a source of the artwork from which to extract the bitmaps in the first place. Now we are in a much better position. We can draw a set of animation cells for our sprites and then load them as PCX files. We can then access the image buffer field of the pcx_picture structure and extract bitmaps for the sprites from there. So what we need to do is write a function that takes as parameters a sprite, a PCX file, the desired (x,y) position from which to extract the bitmap, and the frame number of the bitmap. Figure 4-13 shows this process graphically. The data from a PCX file is used to load up the frames[] array of the sprite structure.
To write the function, we need to make a couple decisions about the way bitmaps will be extracted from the buffer of the pcx_picture (which is simply a 320x200-byte buffer). Since size of a sprite is widthxheight in the sprite structure, the bitmap that is extracted must be the exact same size. Hence, one way to do the extraction would be to send an (x,y) coordinate for the desired location of where to extract the bitmap from the pcx_picture buffer. This would do the trick since the function would know how big of a bitmap to scan by referring to the width and height fields in the sent sprite structure.
This will work, but the drawback is that the caller must continually recompute the (x,y) position of all the desired bitmaps, which can be a pain. A much easier method would be to create a template that has cells in it. For example, if you knew that all the bitmaps in your artwork were going to be 16x16 pixels, you could create a template of 16x16 place holders and put your bitmaps into them in your original artwork file. Then the extraction function would locate a bitmap by referring to it by its cell location in the template instead of its screen coordinates. This is the method that we are going to use for our sprite engine. To make sure that you really understand this, take a look at Figure 4-14, which is a representation of some artwork that has been laid out into cells. The original artwork was drawn with DPaint, and then each bitmap was picked up and placed into one of the cell locations using the painting tools.
As you see from the figure, each bitmap region is bounded by a rectangle which actually has a thickness of one pixel. Therefore, to locate a bitmap that has cell location (cx,cy), we would multiply each component by the width and the height of the sprite plus one. For example, to convert from cell coordinates to image coordinates in the PCX buffer, we would do this:
screen_x = (sprite_width+1) * cell_x; screen_y = (sprite_height+1) * cell_y;
The “+1” is to take into account the borders around each cell. These borders serve no purpose other than to help the artist position the bitmaps in the template and can be taken out if we wish.
Once the coordinates of the actual bitmap are computed, extracting the bitmap data is as simple as a for loop that scans each line of the source bitmap into one of the animation frames of the sprite structure. Before we write a function to do this, let’s take care of a little housekeeping and write sprite initialization and deletion functions similar to what we did with the bitmaps. The sprite initialization function will take a set of parameters to fill up the fields of the sprite, and then it will allocate the background buffer. It won’t allocate any memory for the animation cells since during initialization, we don’t know how many cells are going to represent the sprite (most of the time). The function is shown in Listing 4-13.
Listing 4-13 Sprite initialization function
void Sprite_Init(sprite_ptr sprite,int x,int y,int width,int height,
int c1,int c2,int c3,
int t1,int t2,int t3)
{
// this function initializes a sprite
int index;
sprite->x = x;
sprite->y = y;
sprite->width = width;
sprite->height = height;
sprite->visible = 1;
sprite->counter_1 = c1;
sprite->counter_2 = c2;
sprite->counter_3 = c3;
sprite->threshold_1 = t1;
sprite->threshold_2 = t2;
sprite->threshold_3 = t3;
sprite->curr_frame = 0;
sprite->state = SPRITE_DEAD;
sprite->num_frames = 0;
�����sprite->background = (unsigned char far *)_fmalloc(width * height+1);
// set all bitmap pointers to null
for (index=0; index<MAX_SPRITE_FRAMES; index++)
sprite->frames[index] = NULL;
} // end Sprite_Init
You’re probably starting to notice the use of some defines in the sprite functions and associated code. These are all defined in the file BLACK4.H and are named after what they do. For example, MAX_SPRITE_FRAMES is a constant that marks the upper limit on the number of frames of any sprite. Feel free to change these, but I have set everything up to give us more than what we need, so changes probably won’t be needed. All right, now let’s write a function that will delete or deallocate all the resources used by a sprite. This can be done by freeing all the memory used by the background and the sprite animation frames. Listing 4-14 contains the function to do just that.
=Listing 4-14 Function to delete a sprite
void Sprite_Delete(sprite_ptr sprite)
{
// this function deletes all the memory associated with a sprite
int index;
_ffree(sprite->background);
// now de-allocate all the animation frames
for (index=0; index<MAX_SPRITE_FRAMES; index++)
_ffree(sprite->frames[index]);
} // end Sprite_Delete
We can now create and destroy a sprite. It’s time to write the function that will extract the images for the animation cells of the sprite. The function only needs a few parameters. It needs the sprite itself (which we’ll send a pointer to), the cell coordinates from which to extract the bitmap, and finally, the frame number to place the bitmap into (which is just an array index into frames[]). The function to do this is shown in Listing 4-15.
Listing 4-15 Function to extract a sprtie bitmap image from a PCX file==
void PCX_Get_Sprite(pcx_picture_ptr image,
sprite_ptr sprite,
int sprite_frame,
int cell_x, int cell_y)
{
// this function is used to load the images for a sprite into the sprite
// frames array. It functions by using the size of the sprite and the
// position of the requested cell to compute the proper location in the
// pcx image buffer to extract the data from.
int x_off, // position of sprite cell in PCX image buffer
y_off,
y, // looping variable
width, // size of sprite
height;
unsigned char far *sprite_data;
// extract width and height of sprite
width = sprite->width;
height = sprite->height;
// first allocate the memory for the sprite in the sprite structure
sprite->frames[sprite_frame] = (unsigned char far *)_fmalloc(width * height + 1);
// create an alias to the sprite frame for ease of access
sprite_data = sprite->frames[sprite_frame];
// now load the sprite data into the sprite frame array from the pcx
// picture
x_off = (width+1) * cell_x + 1;
y_off = (height+1) * cell_y + 1;
// compute starting y address
y_off = y_off * 320; // 320 bytes per line
// scan the data row by row
for (y=0; y<height; y++,y_off+=320)
{
// copy the row of pixels
_fmemcpy((void far *)&sprite_data[y*width],
(void far *)&(image->buffer[y_off + x_off]),
width);
} // end for y
// increment number of frames
sprite->num_frames++;
// done!, let's bail!
} // end PCX_Get_Sprite
The function doesn’t do much except compute the position of the desired bitmap in the pcx_picture buffer, allocate the memory to hold the bitmap, and then copy the bitmap into the storage area. You will see more and more that game programming is mostly moving memory from one place to another. If you learn to do this efficiently, your games will be wickedly fast and make lots of money! Back to the function… As you can see, it does very little error checking; as a matter of fact, it doesn’t do any. If you want error checking (which in general you should), you will have to put it in yourself. I have omitted error checking in most functions so we can concentrate on the mechanics of the functions and their operation. Error checking is basic stuff and in most cases is implemented with a couple lines of code, so I’ll leave that up to you. Let’s take the functions we have and create a sprite. Although we can’t draw or move the sprite, it will be a good exercise to see how the functions are used. With that in mind, imagine that we want to create a sprite that is 24x24 pixels and load it with some animation frames from a PCX file on disk called SHIPS.PCX. The images are placed in template form within the upper-left corner of the PCX file and there are four of them. To load the bitmaps into the sprite, we must first create a PCX structure, a sprite, load the PCX file off disk, initialize the sprite, and then extract the four bitmaps from the PCX image. The code to do this follows:
pcx_picture imagery; // this will hold the artwork
sprite ship; // our little spaceship sprite
// initialize and load PCX file
PCX_Init((pcx_picture_ptr)&imagery);
PCX_Load((pcx_picture_ptr)&imagery,1);
// initialize the sprite to be 24x24 pixels at location (160,100)
Sprite_Init((sprite_ptr)&ship,160,100,24,24,0,0,0,0,0,0);
// extract the four bitmaps for sprite from row 0, columns 0,1,2,3
for (frame=0; frame<4; frame++)
PCX_Get_Sprite((pcx_picture_ptr)&imagery,
(sprite_ptr)&ship,frame,frame,0);
// done with PCX file so delete it
PCX_Delete((pcx_picture_ptr)&imagery);
The above fragment is all we need to create a sprite and load in the animation images. Now let’s learn how to display these phantom sprites!
Drawing Sprites
We draw sprites the same way we draw bitmaps. The source data is copied pixel by pixel to the destination buffer at some (x,y) screen position. Moreover, the pixel-bypixel copy may or may not take transparency into consideration. Hence, our sprite drawing function will have two sections within it to handle these two cases at the fastest possible speed. As an exercise, let’s discuss the logical steps to draw a sprite on the screen or in a double buffer. First, the sprite could possibly have multiple bitmaps that define it (unlike the simple bitmap structure). The sprite drawing function will use one of the bitmaps within the frames[] array as the source bitmap image. To draw any bitmap of the sprite, all we need is the bitmap, the position at which to draw it, and its size. We indeed have all of these variables within the sprite structure.
The function uses this information along with a destination buffer to draw the sprite. The function we’ll write will take only three parameters. The first parameter is a pointer to the sprite to be drawn, the second parameter is the destination memory buffer (destination memory context), and the third variable is a transparency flag used to direct the function to take transparent (black) pixels into consideration or not. Now that we have all the particulars down, let’s put it all together in Listing 4-16.
Listing 4-16 Function to draw a sprite
void Sprite_Draw(sprite_ptr sprite, unsigned char far *buffer,
int transparent)
{
// this function draws a sprite on the screen row by row very quickly
// note the use of shifting to implement multiplication
// if the transparent flag is true then pixels will be drawn one by one
// else a memcpy will be used to draw each line
unsigned char far *sprite_data; // pointer to sprite data
unsigned char far *dest_buffer; // pointer to destination buffer
int x,y, // looping variables
width, // width of sprite
height; // height of sprite
unsigned char pixel; // the current pixel being processed
// alias a pointer to sprite for ease of access
sprite_data = sprite->frames[sprite->curr_frame];
�����// alias a variable to sprite size
width = sprite->width;
height = sprite->height;
// compute number of bytes between adjacent video lines after a row of pixels
// has been drawn
// compute offset of sprite in destination buffer
dest_buffer = buffer + (sprite->y << 8) + (sprite->y << 6) + sprite->x;
// copy each line of the sprite data into destination buffer
if (transparent)
{
for (y=0; y<height; y++)
{
// copy the next row into the destination buffer
for (x=0; x<width; x++)
{
// test for transparent pixel i.e. 0, if not transparent then draw
if ((pixel=sprite_data[x]))
dest_buffer[x] = pixel;
} // end for x
// move to next line in destination buffer and sprite image buffer
dest_buffer += SCREEN_WIDTH;
sprite_data += width;
} // end for y
} // end if transparent
else
{
// draw sprite with transparency off
for (y=0; y<height; y++)
{
// copy the next row into the destination buffer
_fmemcpy((void far *)dest_buffer,(void far *)sprite_data,width);
// move to next line in destination buffer and sprite image buffer
dest_buffer += SCREEN_WIDTH;
sprite_data += width;
} // end for y
} // end else
The Sprite_Draw() function is simple but fast. Typically, a video game will spend 99 percent of the time drawing the screen; therefore, we want to make the drawing operations as fast as possible. The function begins by computing some values and assigning them to a pair of pointers. These pointers are dest_buffer and sprite_data, which refer to the sprite’s position in the destination buffer and the sprite bitmap itself. You will also notice that the width and height of the sprite are cached or aliased to local variables–this is for speed purposes. Remember, dereferencing a variable takes more time than accessing a simple type. The main body of the function begins and decides which subfunction will be used based on the transparency flag.
If the sprite is to be drawn with transparency, each pixel from the source bitmap must be tested to see if it’s 0 (which is black in most cases). Depending on the results of this test, the pixel will be copied to the next position in the destination buffer. The process is repeated on a line-by-line basis. Hence, in the function you see a loop that iterates through X values. When the inner loop completes, the outer Y loop adds the constants SCREEN_WIDTH and width (the width of the sprite) to the data pointers to ready them for the next iteration. Therefore, we can deduce that a sprite that is MxN pixels will run through the if statement M*N times! This is unavoidable if transparency is going to be used.
If transparency is turned off, we can simply blast the sprite bitmap line-by-line to the destination buffer using a memcpy() function. This is possible since we don’t need the inner X for loop to iterate through each pixel anymore. Therefore, the second section of the code takes care of the nontransparent case, and the whole operation takes place about three times faster. Consequently, use transparent sprites only when you must. You will usually only need transparency when a static background has sprites running around on it. The next function we need for the sprite animation system is a way to scan the background under a sprite before we draw it. The background under a sprite must be saved in many cases before it’s drawn so that the background image can later be restored.
Scanning Under Sprites
The function to scan the background works in much the same way as the drawing function does except that the destination buffer is now the background field within the sprite structure, and the source data is now the video buffer or a double buffer. Line by line, the source buffer is scanned into the background buffer of the sprite. Also, the scanned image has the same dimensions as the sprite image since, at worst, the sprite will corrupt an area defined by its own width and height. The function to scan an image from a source buffer into the sprite background is defined in Listing 4-17.
Listing 4-17 Function to scan the background under a sprite
void Sprite_Under(sprite_ptr sprite, unsigned char far *buffer)
{
// this function scans the background under a sprite so that when the sprite
// is drawn the background isn't obliterated
unsigned char far *back_buffer; // background buffer for sprite
int y, // current line being scanned
width, // size of sprite
height;
// alias a pointer to sprite background for ease of access
back_buffer = sprite->background;
// alias width and height
width = sprite->width;
height = sprite->height;
// compute offset of background in source buffer
buffer = buffer + (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y<height; y++)
{
// copy the next row out of image buffer into sprite background buffer
_fmemcpy((void far *)back_buffer,
(void far *)buffer,
width);
// move to next line in source buffer and in sprite background buffer
buffer += SCREEN_WIDTH;
back_buffer += width;
} // end for y
} // end Sprite_Under
The function begins the same way as the sprite drawing function does except that there is no need for a transparency flag, and thus, there is only one section of scanner code. The scanner code retrieves the data from the memory pointed to by buffer and stores it into the sprite’s background buffer. This operation is done a line at a time via memcpy(). (Note: _fmemcpy() is the FAR version, but conceptually they are the same.)
We are almost ready to make a small animation system; we simply need to write a function to replace the scanned background.
Erasing Sprites
The concept of erasing a sprite is a bit of a misnomer. We usually think of erasure as painting black or the background color on top of something. In the case of our sprite engine however, we will erase a sprite by replacing the previously scanned background. Therefore, the erasure function will use the bitmap in the background field of the sprite as the source data, and the destination will be either the video buffer or a double buffer.
The function that replaces the background also doesn’t suffer from the affliction of having to test for transparency; it can just blast the image back down line by line. In fact, there isn’t much difference between the scan function and the erase function other than the fact that the roles of the source and destination memory have been switched. Listing 4-18 shows the erasure function.
Listing 4-18 Function to erase a sprite
void Sprite_Erase(sprite_ptr sprite,unsigned char far *buffer)
{
// replace the background that was behind the sprite
// this function replaces the background that was saved from where a sprite
// was going to be placed
unsigned char far *back_buffer; // background buffer for sprite
int y, // current line being scanned
width, // size of sprite
height;
// alias a pointer to sprite background for ease of access
back_buffer = sprite->background;
// alias width and height
width = sprite->width;
height = sprite->height;
// compute offset in destination buffer
buffer = buffer + (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y<height; y++)
{
// copy the next from sprite background buffer to destination buffer
_fmemcpy((void far *)buffer,
(void far *)back_buffer,
width);
// move to next line in destination buffer and in sprite background buffer
buffer += SCREEN_WIDTH;
back_buffer += width;
} // end for y
} // end Sprite_Erase
We are just about ready to see the whole sprite animation system in action, but before we write a demo, let’s address an issue that will be important later on in the development of our games. We will need a way to copy the image from a PCX file into the double buffer, allowing us to use the double buffer as an offscreen animation page with a colorful background. To accomplish this simple task we could write the following loop:
for (index=0; index<double_buffer_size; index++)
double_buffer[index] = pcx_image->buffer[index];
The above fragment would do the job, but for loops are ugly and memcpy() works faster. Also, we may want to copy the image from the PCX file into a myriad of destinations; therefore, we shouldn’t lock ourselves into a hardcoded destination. With those factors in mind, Listing 4-19 contains a function that will take as parameters both a pcx_picture_ptr and a destination buffer.
Listing 4-19 Function to copy the data from a PCX file into a destination buffer
void PCX_Copy_To_Buffer(pcx_picture_ptr image,unsigned char far *buffer)
{
// this function is used to copy the data in the PCX buffer to another buffer
// usually the double buffer
// use the word copy function, note: double_buffer_size is in WORDS
fwordcpy((void far *)buffer,(void far *)image->buffer,double_buffer_size);
} // end PCX_Copy_To_Buffer
You will notice something if you look close enough-the memory movement function is called fwordcpy()'. This is not a C fuction (although it should be). I have create it manually to move WORDs at a time instead of BYTEs. The function has the same interface as memcpy(), but it will use the assemly language equivalent of a 16-bit memory move instead of an 8-bit memory move. The code for the function is shown in Listing 4-20.
Listing 4-20 A 16-bit WORD-based memory move function
void fwordcpy(void far *destination, void far *source,int num_words)
{
// this function is similar to fmemcpy except that it moves data in words
// it is about 25% faster than memcpy which uses bytes
_asm
{
push ds ; need to save segment registers i.e. ds
les di,destination ; point es:di to destination of memory move
lds si,source ; point ds:si to source of memory move
mov cx,num_words ; move into cx the number of words to be moved
rep movsw ; let the processor do the memory move
pop ds ; restore the ds segment register
} // end inline asm
} // end fwordcpy
The function takes as parameters a (void far *) pointer to both the source and destination memory buffer and then an integer representing the number of words to copy. The function is a general utility function and can be used anytime that we wish to move a large amout of memory a WORD at a time.
Putting Sprites in Motion
As an example of using the sprite engine, we’re going to write a demo that will load in a PCX file for a background, load PCX files for the sprite images, and then create a couple of sprites. The sprites will then be moved and animated on the video screen. The animation is achieved by displaying different frames of the sprites as a function of time. In most cases, the curr_frame element of the sprite structure will be updated each video frame, and as the frames are drawn, the sprites will animate. The question is, how do we move the sprites or translate them around the screen?
Translation is a simple concept in computer graphics and can be done in a couple lines of code. In the case of the sprite engine, we know that each sprite is drawn at its (x,y) position, which is recorded in the sprite structure. Hence, if we change these (x,y) coordinates, the sprite will move. To put it mathematically, if you wish to translate an object positioned at (xo,yo) by the values of (dx,dy), you would write:
xo = xo + dx; yo = yo + dy;
That’s all there is to it. The result of the transformation is graphically illustrated in Figure 4-15. Here we see a ship being moved 2 units in the X direction and 1 unit in the Y direction. If this translation operation is performed every game cycle or frame, the object (whatever it is ) will move at a constant rate.
There are of course other transformations that can be applied to objects, such as rotation and scaling. However, since the only objects we are considering right now are sprites, the only transformation that is simple enough to implement at this point is translation. Later in the book when we cover 3D graphics, we’ll cover all the complex transformations in grueling detail. But for now, let’s not get too carried away! Now for the demo that I promised. The name of the demo is WORMS.EXE and the C source is called WORMS.C. The program must be linked with BLACK3.OBJ and BLACK4.OBJ, and of course, the program will need the header files BLACK3.H and BLACK4.H. To run the program, type WORMS at the command line, and you will see a mushroom patch with a worm and an ant crawling in it. Press any key to exit the demo. Listing 4-21 contains the source for the demo.
Listing 4-21 Sprite animation demo program
// WORMS.C - A demo of sprites, clipping and double buffering
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
// G L O B A L S ////////////////////////////////////////////////////////////
pcx_picture image_pcx; // general PCX image used to load background and imagery
sprite worm,ant; // the worm and ant
// M A I N //////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
int index; // loop variable
// set the graphics mode to mode 13h
Set_Graphics_Mode(GRAPHICS_MODE13);
// create the double buffer
Create_Double_Buffer(200);
// load the imagery for worm
PCX_Init((pcx_picture_ptr)&image_pcx);
PCX_Load("wormimg.pcx", (pcx_picture_ptr)&image_pcx,1);
// intialize the worm sprite
Sprite_Init((sprite_ptr)&worm,160,100,38,20,0,0,0,0,0,0);
// extract the bitmaps for the worm, there are 4 animation cells
for (index=0; index<4; index++)
PCX_Get_Sprite((pcx_picture_ptr)&image_pcx,(sprite_ptr)&worm,index,index,0);
// done with this PCX file so delete memory associated with it
PCX_Delete((pcx_picture_ptr)&image_pcx);
// load the imagery for ant
PCX_Init((pcx_picture_ptr)&image_pcx);
PCX_Load("antimg.pcx", (pcx_picture_ptr)&image_pcx,1);
// intialize the ant sprite
Sprite_Init((sprite_ptr)&ant,160,180,12,6,0,0,0,0,0,0);
// extract the bitmaps for the ant, there are 3 animation cells
for (index=0; index<3; index++)
PCX_Get_Sprite((pcx_picture_ptr)&image_pcx,(sprite_ptr)&ant,index,index,0);
// done with this PCX file so delete memory associated with it
PCX_Delete((pcx_picture_ptr)&image_pcx);
// now load the background that the worm and ant will run around in
PCX_Init((pcx_picture_ptr)&image_pcx);
PCX_Load("mushroom.pcx",(pcx_picture_ptr)&image_pcx,1);
// copy PCX image to double buffer
PCX_Copy_To_Buffer((pcx_picture_ptr)&image_pcx,double_buffer);
PCX_Delete((pcx_picture_ptr)&image_pcx);
// scan under ant and worm before entering the event loop, this must be
// done or else on the first cycle the "erase" function will draw garbage
Sprite_Under((sprite_ptr)&ant,double_buffer);
Sprite_Under_Clip((sprite_ptr)&worm,double_buffer);
// main event loop, process until keyboard hit
while(!kbhit())
{
// do animation cycle, erase, move draw...
// erase all objects by replacing what was under them
Sprite_Erase((sprite_ptr)&ant,double_buffer);
Sprite_Erase_Clip((sprite_ptr)&worm,double_buffer);
// move objects, test if they have ran off right edge of screen
if ((ant.x+=1) > 320-12)
ant.x = 0;
if ((worm.x+=6) > 320)
worm.x = -40; // start worm back one length beyond the
// left edge of screen
// do animation for objects
if (++ant.curr_frame==3) // if all frames have been displayed then
ant.curr_frame = 0; // reset back to frame 0
if (++worm.curr_frame==4) // if all frames have been displayed then
worm.curr_frame = 0; // reset back to frame 0
// ready to draw objects, but first scan background under them
Sprite_Under((sprite_ptr)&ant,double_buffer);
Sprite_Under_Clip((sprite_ptr)&worm,double_buffer);
Sprite_Draw((sprite_ptr)&ant,double_buffer,1);
Sprite_Draw_Clip((sprite_ptr)&worm,double_buffer,1);
// display double buffer
Display_Double_Buffer(double_buffer,0);
// lock onto 18 frames per second max
Time_Delay(1);
} // end while
// exit in a very cool way
Screen_Transition(SCREEN_SWIPE_X);
// free up all resources
Sprite_Delete((sprite_ptr)&worm);
Sprite_Delete((sprite_ptr)&ant);
Delete_Double_Buffer();
Set_Graphics_Mode(TEXT_MODE);
} // end main
Let’s analyze the program. First the VGA is placed into mode 13h and the double buffer is created. Then the PCX file containing the worm imagery is loaded, and the worm is initialized and loaded with the four animation frames. Then the PCX buffer is deleted. The process is repeated for the ant, except this time the images for the ant are 12x6 pixels instead of 38x20 pixels as the worm was. The ant animation frames are loaded and the PCX memory is deleted. Finally, the program loads the last PCX file, which is the background for the demo, and copies the image into the double buffer.
At this point, the main game loop is ready to be entered, but first we must scan the backgrounds under the sprites. Remember, on the very first cycle the backgrounds must be scanned so they can be replaced. This is done right before the while loop. Once the while loop is entered, the animation cycle begins. First the sprites are erased (which is accomplished by replacing the background under them), then the sprites are moved, and their positions are tested against the right edge of the screen. If either of them has moved off the right edge, they are “warped” back to the left edge by resetting their respective X positions in their sprite structures. The next step of the program animates the sprites by incrementing the curr_frame element of each sprite structure. If the logic detects that the last frame has been reached, the curr_frame field is reset to 0.
The sprites are almost ready to be drawn, but before drawing them, we must scan under them with the Sprite_Under() functions. After this operation, the sprites are drawn and the double buffer is copied to the video buffer for viewing.
Once the video frame is displayed, a short period is allowed for the user to view it, so there is a call to Time_Delay(1). This will also synchronize the system to 18 FPS, and the animation will look smooth. Without this synchronization phase, the animation would run so fast it would be a blur. As an exercise, comment out the delay and see what I’m talking about. Anyway, after the delay completes its cycle, the process continues again until a key is pressed.
Now I want you to study the animation program’s code and running program. You should notice something that seems a bit odd. The sprite manipulation functions that draw the worm have an _Clip after them. These versions of the sprite functions perform clipping. I have placed them in the program to let you see clipping in action before we discuss it. You see, when the worm sprite is drawn on the screen, it is so big that if we draw it near the right edge of the screen it would extend past the last column, which is at X position 319. Hence, the standard sprite drawing program would crash or draw something incorrectly if a sprite was drawn that extended past the edges of the screen. Figure 4-16 shows this problem graphically. In the figure a bounding box represents the sprite (or any other game object) drawn at different positions on the screen. We can see that something has to be done to allow sprites to be drawn correctly in these cases. Otherwise, we will never be able to draw sprites that are partially visible. We will always have to stop them before their bounding boxes extend past a screen edge. This isn’t acceptable if the sprites are large. Alas, we must learn how to clip—guess what’s next!
Sprite Clipping
I know that I cheated a bit by using the clipped versions of the sprite functions in the demo program WORMS.EXE, but sometimes it’s better to see something and then try to explain it. Anyway, we need to rewrite the three main sprite functions to take clipping into consideration. If you recall, the original sprite structure had some elements at the end that looked like they had something to do with clipping. Here are those particular fields again for review:
int x_clip,y_clip; // clipped position of sprite
int width_clip,height_clip; // clipped size of sprite
int visible; // used by sprite engine to flag
// if a sprite was invisible last
// time it was drawn hence the background
// need not be replaced
These variables will help us write a new set of sprite functions that can clip the sprite image to the boundaries of the screen.
Sprites can be abstracted geometrically to rectangles that have size (widthxheight), where width and height are the width and height of the sprite. Therefore, we can assume that clipping sprites to the screen (which is a rectangle) is equivalent to clipping rectangles against a larger rectangle. Refer to Figure 4-16 again, which shows the different cases of clipping. The numerous cases in the figure can be boiled down to the following main cases:
- A sprite is completely invisible, that is, no portion of it overlaps the view
screen.
- A sprite is completely visible, that is, the entire portion of it is within the
view screen.
- The sprite’s X extents overlap either the right or left edge of the screen.
- The sprite’s Y extents overlap either the top or bottom edge of the screen.
- The sprite’s extents overlap both the X and Y edges of the screen, such as a
corner.
Case 1 is easily taken care of by not drawing the sprite at all. Case 2 is accomplished by drawing the sprite as usual without any clipping. Cases 3 through 5 are the hard part, and that’s what we’re going to concentrate on. For our discussion we are going to use the following conventions to make the process easier to understand. First, the screen will be used as a clipping rectangle; thus its dimensions are 320x200. Second, we will use a rectangle as the object to be clipped. The rectangle will be defined by its upper-left corner (x1,y1) and its lower-right corner (x2,y2). The rectangle will have size (widthxheight). Figure 4-17 is a representation of this setup.
Using our geometrical conventions, let’s see if we can’t come up with an algorithm that will clip the object to the screen boundary. First let’s implement cases 1 and 2. We do this by testing the corner points of the rectangle.
// Case 1
if ((x1<0 and x2<0) or (x1>319 and x2>319) or
(y1<0 and y2<0) or (y1>199 and y2>199) then invisible, do nothing
// Case 2
if (x1>0 and x2<319 and y1>0 and y2<199) then totally visible, draw as usual
Cases 3, 4, and 5 can all be taken care of at the same time. In essence, if cases 1 and 2 fail, the object (a rectangle) must be partially visible and overlaps the X edges of the screen, the Y edges of the screen, or both. Therefore, we will simply test to see if either of the endpoints of the rectangle do indeed extend past the edges of the screen, and if so, we will shrink them back to make them fit. Here’s the algorithm to do it:
// test X extents first if (x1<0) x1=0; if (x2>319) x2=319; // now test Y extents if (y1<0) y1=0; if (y2>199) y2=199;
Let’s test the algorithm on a couple examples and see if it works. Take a look at Figure 4-18, which shows the results of three different rectangles being drawn on the screen after being processed with the clipping algorithm. Rectangle 1 is completely invisible and would fail the test for case 1; therefore, it isn’t drawn. Rectangle 2 is totally visible since both endpoints are within the screen boundaries, and hence it’s drawn unscathed. Rectangle 3 fails the tests for both cases 1 and 2 and must therefore need clipping. We see that the endpoints for rectangle 3 are (–5,–4) and (10,20). The upper-left corner will be recomputed as (0,0) since both –5 and –4 are beyond left and top edges of the screen. The final clipped rectangle is defined by the endpoints (0,0) and (10,20).
We now have a complete algorithm that will clip a rectangular object to a rectangular clipping region such as the screen or double buffer. The next step is to implement this algorithm into our sprite engine so that the bitmaps of the sprites are drawn in a clipped manner. This is a bit tricky, but in essence the clipping of the sprite’s bounding box or rectangle is performed first. Then, based on the results of this, the proper sub-bitmap is drawn in the clipped rectangle that is representative of the visible portion of the sprite when drawn on the screen.
Figure 4-19 shows an example of a sprite being drawn with its bitmap clipped. First, the new clipped rectangle is computed, and then the proper subportion of the bitmap within the bitmap of the sprite is mapped down. The extra operations to implement clipping are nothing more than a couple of additions and multiplications. Hence, clipped sprites perform almost as well as nonclipped sprites. As a rule of thumb, the larger the sprite, the smaller the performance penalty for clipping it will be.
Now that we have all the technicalities of clipping down, let’s rewrite the sprite functions. First, the drawing function is shown in Listing 4-22.
Listing 4-22 Sprite drawing funtion that clips to the screen boundaries
void Sprite_Draw_Clip(sprite_ptr sprite, unsigned char far *buffer,
int transparent)
{
// this function draws a sprite on the screen row by row very quickly
// note the use of shifting to implement multiplication
// if the transparent flag is true then pixels will be drawn one by one
// else a memcpy will be used to draw each line
// this function also performs clipping. It will test if the sprite
// is totally visible/invisible and will only draw the portions that are visible
unsigned char far *sprite_data; // pointer to sprite data
unsigned char far *dest_buffer; // pointer to destination buffer
int x,y, // looping variables
sx,sy, // position of sprite
width, // width of sprite
bitmap_x =0, // starting upper left corner of sub-bitmap
bitmap_y =0, // to be drawn after clipping
bitmap_width =0, // width and height of sub-bitmap
bitmap_height =0;
unsigned char pixel; // the current pixel being processed
// alias a variable to sprite size
width = sprite->width;
bitmap_width = width;
bitmap_height = sprite->height;
sx = sprite->x;
sy = sprite->y;
// perform trivial rejection tests
if (sx >= (int)SCREEN_WIDTH || sy >= (int)double_buffer_height ||
(sx+width) <= 0 || (sy+bitmap_height) <= 0 || !sprite->visible)
{
// sprite is totally invisible therefore don't draw
// set invisible flag in structure so that the erase sub-function
// doesn't do anything
sprite->visible = 0;
return;
} // end if invisible
// the sprite needs some clipping or no clipping at all, so compute
// visible portion of sprite rectangle
// first compute upper left hand corner of clipped sprite
if (sx<0)
{
bitmap_x = -sx;
sx = 0;
bitmap_width -= bitmap_x;
} // end off left edge
else
if (sx+width>=(int)SCREEN_WIDTH)
{
bitmap_x = 0;
bitmap_width = (int)SCREEN_WIDTH-sx;
} // end off right edge
// now process y
if (sy<0)
{
bitmap_y = -sy;
sy = 0;
bitmap_height -= bitmap_y;
} // end off top edge
else
if (sy+bitmap_height>=(int)double_buffer_height)
{
bitmap_y = 0;
bitmap_height = (int)double_buffer_height - sy;
} // end off lower edge
// this point we know where to start drawing the bitmap i.e.
// sx,sy
// and we know where in the data to extract the bitmap i.e.
// bitmap_x, bitmap_y,
// and finally we know the size of the bitmap to be drawn i.e.
// width,height, so plug it all into the rest of function
// compute number of bytes between adjacent video lines after a row of
// pixels has been drawn
// compute offset of sprite in destination buffer
dest_buffer = buffer + (sy << 8) + (sy << 6) + sx;
// alias a pointer to sprite for ease of access and locate starting sub
// bitmap that will be drawn
sprite_data = sprite->frames[sprite->curr_frame] + (bitmap_y*width) +
bitmap_x;
// copy each line of the sprite data into destination buffer
if (transparent)
{
for (y=0; y<bitmap_height; y++)
{
// copy the next row into the destination buffer
for (x=0; x<bitmap_width; x++)
{
// test for transparent pixel i.e. 0, if not transparent then draw
if ((pixel=sprite_data[x]))
dest_buffer[x] = pixel;
} // end for x
// move to next line in destintation buffer and sprite image buffer
dest_buffer += SCREEN_WIDTH;
sprite_data += width; // note this width is the actual width of the
// entire bitmap NOT the visible portion
} // end for y
} // end if transparent
else
{
// draw sprite with transparency off
for (y=0; y<bitmap_height; y++)
{
// copy the next row into the destination buffer
_fmemcpy((void far *)dest_buffer,(void far *)sprite_data,bitmap_width);
// move to next line in destintation buffer and sprite image buffer
dest_buffer += SCREEN_WIDTH;
sprite_data += width; // note this width is the actual width of the
// entire bitmap NOT the visible portion
} // end for y
} // end else
// set variables in structure so that the erase sub-function can operate
// faster
sprite->x_clip = sx;
sprite->y_clip = sy;
sprite->width_clip = bitmap_width;
sprite->height_clip = bitmap_height;
sprite->visible = 1;
} // end Sprite_Draw_Clip
The function Sprite_Draw_Clip() is roughly the same as Sprite_Draw() except that two extra steps are performed. First the clipping is performed, which computes the proper sub-bitmap and its position and passes them to the next step of the function that draws the bitmap. Second, during the end of the function, the clipping information that was computed during this function is stored in those mystery variables in the sprite structure (x_clip, y_clip, width_clip, height_clip). This is so the Sprite_Erase_Clip( ) function (which we’ll see in a moment) doesn’t have to recompute all the clipping information. This is an important optimization and is possible because the position of a sprite won’t change in the time between drawing and erasing. Also, a very important flag that is set is the visibility flag named visible. If this flag is false, the erasure function will simply return to the caller, since it is being notified that the sprite wasn’t even drawn on the screen! This is another good optimization. There is no reason to replace the background if there was nothing drawn.
The erasure function works similarly in the nonclipped version except that the size of the region to be replaced is recorded in the variables width_clip and height_clip, and the position where the bitmap is to be replaced is recorded in x_clip and y_clip. Since x_clip and y_clip are computed by the drawing function, we can simply access them within the sprite structure without computing them. Listing 4- 23 contains the code for the clipped erasure function.
Listing 4-23 A clipped sprite erasure function
void Sprite_Erase_Clip(sprite_ptr sprite,unsigned char far *buffer)
{
// replace the background that was behind the sprite
// this function replaces the background that was saved from where a sprite
// was going to be placed
unsigned char far *back_buffer; // background buffer for sprite
int y, // current line being scanned
width, // size of sprite background buffer
bitmap_height, // size of clipped bitmap
bitmap_width;
// make sure sprite was visible
if (!sprite->visible)
return;
// alias a pointer to sprite background for ease of access
back_buffer = sprite->background;
// alias width and height
bitmap_width = sprite->width_clip;
bitmap_height = sprite->height_clip;
width = sprite->width;
// compute offset in destination buffer
buffer = buffer + (sprite->y_clip << 8) + (sprite->y_clip << 6)
+ sprite->x_clip;
for (y=0; y<bitmap_height; y++)
{
// copy the next row from sprite background buffer to destination buffer
_fmemcpy((void far *)buffer,
(void far *)back_buffer,
bitmap_width);
// move to next line in destination buffer and in sprite background buffer
buffer += SCREEN_WIDTH;
back_buffer += width;
} // end for y
} // end Sprite_Erase_Clip
As you can see, the function is very simple and almost identical to the nonclipped version except for the computation of position and size of the bitmap to replace.
The final function that we need to complete our bag of spells is the background scanning function. This function is very similar to the drawing function; that is, it will perform all the clipping calculations and then scan the visible portion of the screen that the sprite will disturb when drawn. You could argue that there was no reason to redo the calculations in the drawing function, and you would almost be correct. But in the case that the background doesn’t need to be scanned, such as a dynamic background game, the Sprite_Draw_Clip() wouldn’t have clipping information sent to it. Hence, there is a bit of redundancy here, but that’s life! Listing 4- 24 shows the code for the function.
Listing 4-24 A sprite background scanner that clips to the screen boundaries
void Sprite_Under_Clip(sprite_ptr sprite, unsigned char far *buffer)
{
// this function scans the background under a sprite, but only those
// portions that are visible
unsigned char far *back_buffer; // pointer to sprite background buffer
unsigned char far *source_buffer; // pointer to source buffer
int x,y, // looping variables
sx,sy, // position of sprite
width, // width of sprite
bitmap_width =0, // width and height of sub-bitmap
bitmap_height =0;
unsigned char pixel; // the current pixel being processed
// alias a variable to sprite size
width = sprite->width;
bitmap_width = width;
bitmap_height = sprite->height;
sx = sprite->x;
sy = sprite->y;
// perform trivial rejection tests
if (sx >= (int)SCREEN_WIDTH || sy >= (int)double_buffer_height ||
(sx+width) <= 0 || (sy+bitmap_height) <= 0)
{
// sprite is totally invisible therefore don't scan
// set invisible flag in structure so that the draw sub-function
// doesn't do anything
sprite->visible = 0;
return;
} // end if invisible
// the sprite background region must be clipped before scanning
// therefore compute visible portion
// first compute upper left hand corner of clipped sprite background
if (sx<0)
{
bitmap_width += sx;
sx = 0;
} // end off left edge
else
if (sx+width>=(int)SCREEN_WIDTH)
{
bitmap_width = (int)SCREEN_WIDTH-sx;
} // end off right edge
// now process y
if (sy<0)
{
bitmap_height += sy;
sy = 0;
} // end off top edge
else
if (sy+bitmap_height>=(int)double_buffer_height)
{
bitmap_height = (int)double_buffer_height - sy;
} // end off lower edge
// this point we know where to start scanning the bitmap i.e.
// sx,sy
// and we know the size of the bitmap to be scanned i.e.
// width,height, so plug it all into the rest of function
// compute number of bytes between adjacent video lines after a row of pixels
// has been drawn
// compute offset of sprite background in source buffer
source_buffer = buffer + (sy << 8) + (sy << 6) + sx;
// alias a pointer to sprite background
back_buffer = sprite->background;
for (y=0; y<bitmap_height; y++)
{
// copy the next row into the destination buffer
_fmemcpy((void far *)back_buffer,(void far *)source_buffer,bitmap_width);
// move to next line in destination buffer and sprite image buffer
source_buffer += SCREEN_WIDTH;
back_buffer += width; // note this width is the actual width of the
// entire bitmap NOT the visible portion
} // end for y
// set variables in structure so that the erase sub-function can operate
// faster
sprite->x_clip = sx;
sprite->y_clip = sy;
sprite->width_clip = bitmap_width;
sprite->height_clip = bitmap_height;
sprite->visible = 1;
} // end Sprite_Under_Clip
The question is, do we always need to clip? Not really. For example, if we already know that a sprite or object can never get near a screen edge, there is no reason to clip. Also, if the objects are very small, simply making them disappear won’t upset the player too much. For example, if an 8x8 sprite were about to cross an edge and you made it disappear, that’s not a big deal; but a 100x100 starship disappearing would be suspicious at the very least! When we implement the 3D graphics engine, clipping will be an absolute necessity, so get a grip on it, baby!
Now that we have a complete, basic animation package, let’s start thinking about some game-related topics such as collision detection. For example, how can we detect if two sprites have collided? Good question, guess what’s next?
Testing for Collisions
During the course of a video game, one of the main functions that the game code must perform each frame is collision detection. Collision detection is the technique of deciding if an object has collided with another object or some aspect of the environment. Since we are concentrating at this point on sprites and 2D space, we are only going to detect collision of 2D objects. When we make the leap to 3D space later in the book, we will extend these concepts to full 3D space.
Detecting a collision between two sprites can be done in a couple of ways. The first method, binary image overlap testing (BIOT), tests for a collision between two sprites by logically ANDing their bitmaps together in image space. Figure 4-20 shows this graphically. If there is a region that both sprites are occupying at the same time, the result of the logical AND operation will be a non-zero area of pixels. If any such area exists during the test, the sprites have collided and the proper action(s) should be taken.
BIOT is the most precise method of collision detection between bitmapped images. However, there is one drawback: the test will probably take more time than drawing the sprites! This is because a logical operation must be done for every single pixel in the sprites, and this is just too much computation to waste on collision detection. There are computer systems that actually perform these kinds of tests in hardware, but implementing them in software is too intensive. The solution is to simplify the collision detection by approximating the object’s geometry with a shape that can be more easily tested. The problem with a sprite’s bitmap is that it could be anything; but what if we were to use the bounding box of a sprite as the test shape?
Using the bounding box of a bitmap or sprite object for collision detection is a popular technique that gives good results most of the time and is orders of magnitude faster than BIOT. Here’s how it works: Say we have two sprites and we wish to test if they have collided. What we can do is represent them with their respective bounding boxes. Then instead of testing the sprite images for overlap, we test if the bounding boxes have overlapped. Figure 4-21 shows this process graphically. Sprite 1 is positioned at (x1,y1) and has dimensions (width_1xheight_1); sprite 2 is positioned at (x2,y2) and has dimensions (width_2xheight_2). To test if the bounding boxes have overlapped, we use the following algorithm:
if ( x1>= x2 and x1<x2+width_2 ) or ( x1+width_1 >= x2 and x1+width_1<x2+width_2) or ( y1>= y2 and y1<y2+height_2 ) or ( y1+height_1 >= y2 and y1+height_1<y2+height_2) then they have collided
Basically, the pseudocode tests the four corner points of object 1 to see if they are within object 2’s rectangular bounding box. The above algorithm can be optimized in many situations to increase the speed of collision testing even further. For example, if the width and height of the objects (sprites or whatever) are the same, another more clever technique can be used. Here’s such a test in real C code:
// compute amount of overlap in x and y
dx = abs(x2-x1);
dy = abs(y2-y1);
// test if overalap region is within sprite bounding box
if (dx<width && dy<height)
{
// a collision has occurred process it
} // end if collision detected
Of course, the abs() function isn’t free, but the above technique is a bit cleaner than testing all four points of object 1 against object 2. Anyway, you get the basic idea behind the different collision techniques. These techniques will be applicable to 3D. For example, when we evolve to 3D, we will use bounding spheres instead of bounding boxes for the collision detection.
Before moving on, let’s briefly discuss the accuracy of bounding box collision detection. In general, the technique works fine, and the player will never notice when the game is or isn’t detecting a proper collision. I always get a kick out of watching someone play a game and say, “I hit that!” They probably did, but what they don’t know is that sometimes collisions are miscalculated. When using the bounding box technique, the program will sometimes detect collisions that didn’t occur. This happens when the bounding boxes of the sprites or 2D game’s objects overlap, but the actual bitmaps don’t. Take a look at Figure 4-22 for a classic example. In the figure an alien ship and a missile are both within range of each other. Moreover, the missile’s bounding box has definitely encroached upon the alien’s bounding box. However, the missile hasn’t actually hit the alien yet. Granted, in a couple of cycles the alien will probably meet its maker, but at this point a collision should not be flagged.
However, with our technique a collision would be detected. This is the typical situation that occurs and can really upset a player. If the player is the alien, it’s possible that he might have the dexterity to move out of the way fast enough to evade the missile, but since the bounding boxes have intersected, it’s all over.
The only workable solution to this problem is to shrink the bounding box of the alien a bit to make sure that the box contains most of the “mass” of the alien and when a collision is detected it is a solid collision and not just an arm or grazing hit. To do this we compute the size of the bounding box and then shrink it by multiplying it by a factor less than 1.0. For example, most sprites take up from 70 to 100 percent of their bitmap area, so a multiplication factor of .75 is usually a good bet.
We have covered a lot of ground and have a complete sprite and animation system. I bet that we could write some decent 2D bitmapped games at this point if we really worked at it, but it’s time to get a taste of 3D graphics–at least simulated 3D.
Scrolling the World Around Us
One of the most popular genres of video games are scrolling games. A scrolling game is based on the concept of allowing the player to move around in an environment that is larger than the screen. In other words, the screen is a window to a larger universe. This is illustrated in Figure 4-23. Here we see a large universe that is 4000x4000 pixels large, but only a 320x200-pixel window of it is visible at any time. The player moves the window around the larger environment, and the contents of the window are mapped to the video screen or video buffer. Using this technique, a player can immerse himself in a huge environment.
As you might imagine, the memory requirements of a scrolling game can be intense. With a single 320x200-pixel window costing 64,000 bytes, a 10x10 screen universe would take 6.4 megabytes! Obviously, there must be other techniques used to create large scrolling universes that are based on more efficient memory models. And of course there are, but for now, we are going to concentrate on the method that uses one byte to represent one pixel. The reason for this is that we aren’t interested in making a general scrolling engine like the one you see in side scrolling games such as Mario Brothers or Sonic The Hedgehog. We are more interested in using scrolling to create background mountainscapes or cloud formations that will be backdrops for our 3D games. We will primarily be using scrolling to implement a special type of scrolling that is usually referred to as parallax scrolling.
Parallax Scrolling
Parallax scrolling is the technique of creating a scrolling environment (usually horizontally oriented) that has one or more layers in the vertical direction. Each one of these layers scrolls at different speeds to synthesize the effects of perspective at various distances. The result of scrolling layers of an image at different rates will be the simulation of motion in the plane where the viewer is looking perpendicular to the direction of motion. Figure 4-24 shows this setup graphically. We had a brief introduction to parallax scrolling in Chapter 1 when we discussed the evolution of 3D games. We learned that parallax scrolling was one of the first methods to “cheat” a 3D view.
We’ll be using parallax scrolling to create a simulated 3D view, but when coupled with foreground objects that are really 3D, the results will be quite impressive. So how do we create a parallax scrolling engine? Well, we should start off by thinking of the capabilities that we want it to have. If you take a look at Figure 4-25, you will see an eerie landscape of a distant world. If we were to drive a landspeeder across this landscape, the mountains would move slowly, and the grass and terrain close to us would move swiftly. Thus, if we could break the image into a collection of “layers” and then scroll each layer at different velocities, the illusion of parallax would be attained.
To accomplish the scrolling, we need to extract a layer of a given width and height and store it in some kind of bitmap structure. Then we draw the layer on the screen or double buffer at different horizontal positions while wrapping the region that extends beyond the right edge around to the other side. For example, say we defined the mountains as a single layer and extracted them as a single bitmap 320x45 pixels. Then we could draw this bitmap at some starting X position and some Y position, but there’s a problem. The bitmap is so big (320 pixels horizontally) that it would extend beyond the right edge of the screen or double buffer. We could clip it, but this would lose information. What we want to do is wrap the portion that extends beyond the right edge of the screen back to the left side and create a seamless image. Figure 4-26 shows this process graphically. The portion of the image that extends beyond the right edge of the screen is simply drawn at the left edge of the screen.
Now, there’s a little artistic problem with this method. The right and left edges must match or be able to wrap around. If they don’t, there will be a perceivable discontinuity. So we need to draw the artwork so that the right and left edge of a layer mesh together.
The parallax engine we create should be able to handle layers that are wider than 320 pixels just in case we want to have a large universe, but this isn’t a problem. If we wanted to make a layer that was, say 640x50, we could draw two mountainscapes and make sure they form a seamless connection. Then we would place the two layers in a PCX file and extract the first 320x50 section, and then connect the second 320x50 section to the right of the first section. This is shown in Figure 4-27. With this, we’ll have the power to create large scrolling universes.
Once we can extract a single layer from a PCX image we can create a group of layers by stacking them on top of each other in the vertical direction. Then by moving the layers horizontally while wrapping them, it will seem as if the viewpoint of the player is moving. And if each layer is moved at different velocities, perspective parallax will take place and the illusion will become a reality.
The Layers System
Now that we have a basic inventory of the capabilities that a parallax scrolling engine needs, let’s begin implementing it. In reality we need but a few functions to make a very robust system. The working set should include a pair of functions to create and delete a layer. These functions will do little more than allocate and deallocate memory and set a few variables. Next we need a function to build up a layer from smaller pieces. As we talked about a few moments ago, we may want to create a layer that is larger or smaller than the width of the screen; hence, we will “tile” pieces together to create a final layer. And finally, we need a function to draw the layer on the screen or double buffer. The function should take only a few parameters, such as the position to start drawing the layer (which will always wrap around), a transparency flag to allow for overlapping layers, and the layer to be used as a data source.
The data structure that we’ll use is hardly more complex than our first bitmap structure at the beginning of the chapter. As a matter of fact, the layer structure is identical (at this point in the game–but this may change later). Take a look:
// this is a typedef used for the layers in parallax scrolling
// note it is identical to a bitmap, but we'll make a separate typedef
// in the event we later need to add fields to it
typedef struct layer_typ
{
int x,y; // used to hold position information
// no specific function
int width,height; // size of layer, note:width must
// be divisible by 2
unsigned char far *buffer; // the layer buffer
} layer, *layer_ptr;
You will note one attribute of the structure definition that seems a bit stringent, and that’s the need for the width of the layer to be divisible by two. In other words, a layer must have an even number of bytes. This will allow us later to use WORD-size moves to render a layer if we need the extra speed. For now, this is just a suggestion, and layers don’t have to have a width that is a multiple of two. We are now ready to implement the functions that create and render layers. Let’s begin with the simple functions to create and destroy a layer.
Creating a Layer
To create a layer, we must allocate the proper amount of memory to hold the entire bitmap, which could be quite large. In fact, we have set no upper limit for the size of these layers. However, there is a memory model and PC-related limit that is in flux in a subtle way. And that’s the fact that we can’t allocate a single chunk of memory larger than 64K in any of the memory models other than HUGE. Hence, we must insure that the dimensions of a layer when multiplied together do not exceed 65,536 bytes, or else there will be trouble! The function to create a layer is shown in Listing 4-25.
Listing 4-25 The layer creation function
int Layer_Create(layer_ptr dest_layer, int width, int height)
{
// this function can be used to allocate the memory needed for a layer
// the width must be divisible by two.
if ((dest_layer->buffer = (unsigned char far *)_fmalloc(width*height+2))==NULL)
return(0);
else
{
// save the dimensions of layer
dest_layer->width = width;
dest_layer->height = height;
return(1);
} // end else
} // end Layer_Create
The function takes as parameters a pointer to the layer to create and the desired width and height. The function will then allocate the necessary memory and return a 0 if there isn't enough RAM availible for the operation (one of my few error checks!)
Deleting a Layer
It's always easier to destroy than to create, and that's what we'll do next. We simply need to deallocate the memory that was previously allocated to a layer and release it back to DOS. Listing 4-26 contains the code to do that.
Listing 4-26 Function to delete a layer
void Layer_Delete(layer_ptr the_layer)
{
// this function deletes the memory used by a layer
if (the_layer->buffer)
_ffree(the_layer->buffer);
} // end Layer_Delete
The function takes a single parameter, which surprisingly enough is the layer to delete. Note that the FAR version of free( ) is used to deallocate the memory. Remember always to be on your toes when allocating and deallocating memory if you are using mixed memory models—that is, mixing FAR and NEAR pointers in the same program.
Building a Layer
The next function we need in order to finish creating a layer is a function that builds up the layer’s bitmap. The function should work relatively the same as the functions that extract bitmaps for both the sprites and the primitive bitmap type, but it should allow multiple rectangular chunks of bitmapped images to be chained together to create a larger overall bitmap. The source of the bitmap image can be any memory context that has a width of 320 bytes or pixels. Therefore, a PCX buffer, the video buffer, or a double buffer are all possible source data buffers. The code to implement such a function is shown in Listing 4-27.
Listing 4-27 Function to build a layer out of smaller bitmaps
void Layer_Build(layer_ptr dest_layer,int dest_x, int dest_y,
unsigned char far *source_buffer,int source_x,int source_y,
int width,int height)
{
// this function is used to build up the layer out of smaller pieces
// this allows a layer to be very long, tall etc. also the source data buffer
// must be constructed such that there are 320 bytes per row
int y, // looping variable
layer_width; // the width of the layer
unsigned char far *source_data; // pointer to start of source bitmap image
unsigned char far *layer_buffer; // pointer to layer buffer
// extract width of layer
layer_width = dest_layer->width;
// compute starting location in layer buffer
layer_buffer = dest_layer->buffer + layer_width*dest_y + dest_x;
// compute starting location in source image buffer
source_data = source_buffer + (source_y << 8) + (source_y << 6) + source_x;
// scan each line of source image into layer buffer
for (y=0; y<height; y++)
{
// copy the next row into the layer buffer using memcpy for speed
_fmemcpy((void far *)layer_buffer,
(void far *)source_data,width);
// move to next line in source buffer and in layer buffer
source_data += SCREEN_WIDTH;
layer_buffer += layer_width;
} // end for y
} // end Layer_Build
Remember that this function may be called one or more times by the game code. It will continually build up the image as directed. The function takes quite a few parameters. It takes a pointer to the layer to build, a position in the layer buffer where the source image should be mapped, a pointer to a source buffer, and the position and size of the region to scan. You will notice that the function is basically the core code from the PCX_Get_Sprite() function with a few simplifications.
Drawing a Layer
The final ingredient of the layers system is a function to draw the layer! This is the most complex function of all. The reason for the complexity is that the layer’s bitmap must be drawn on the screen at some starting X position, but the portion that extends off the right edge of the screen must be wrapped around to the left to create a seamless image. This isn’t that hard to do, but off-by-one errors have to be paid attention to, or else! An off-by-one error is a common problem that plagues programmers and Cybersorcerers alike. A quick example will illustrate the point. If there are 100 parking spaces, how many lines divide the spaces? Your first response might be 100, but that would be wrong. There are 101! This is the basis of the offby- one problem. And to avoid it, take your time when doing simple calculations.
Anyway, back to the problem at hand. Our basic plan of attack to draw a layer is the following:
- Based on the destination position of the layer, compute the starting position
in the destination buffer.
- Based on the current (x,y) position of the layer, compute the starting
address of the bitmap that should be mapped onto the destination area.
- Compute the width of the bitmap that will extend off the right-hand portion
of the screen.
- Draw the right-hand portion from the starting X location. Then, starting
from 0, draw the remaining left-hand portion that was previously computed to overlap the right edge of the screen.
I agree, the explanation seems a bit hard to grasp, but the actual code should help iron out the details. So without further ado, the final function to complete our layer system is shown in Listing 4-28.
Listing 4-28 Function to draw a layer
void Layer_Draw(layer_ptr source_layer, int source_x, int source_y,
unsigned char far *dest_buffer,int dest_y,int dest_height,
int transparent)
{
// this function will map down a section of the layer onto the destination
// buffer at the desired location, note the width of the destination buffer
// is always assumed to be 320 bytes width. Also, the function will always
// wrap around the layer
int x,y, // looping variables
layer_width, // the width of the layer
right_width, // the width of the right and left half of
left_width; // the layer to be drawn
unsigned char far *layer_buffer_l; // pointers to the left and right halves
unsigned char far *dest_buffer_l; // of the layer buffer and destination
unsigned char far *layer_buffer_r; // buffer
unsigned char far *dest_buffer_r;
unsigned char pixel; // current pixel value being processed
layer_width = source_layer->width;
dest_buffer_l = dest_buffer + (dest_y << 8) + (dest_y << 6);
layer_buffer_l = source_layer->buffer + layer_width*source_y + source_x;
// test if wrapping is needed
if ( ( (layer_width-source_x)-(int)SCREEN_WIDTH ) >= 0)
{
// there's enough data in layer to draw a complete line, no wrapping needed
left_width = SCREEN_WIDTH;
right_width = 0; // no wrapping flag
} // end if
else
{
// wrapping needed
left_width = layer_width - source_x;
right_width = SCREEN_WIDTH - left_width;
dest_buffer_r = dest_buffer_l + left_width;
layer_buffer_r = layer_buffer_l - source_x; // move to far left end of layer
} // end else need to wrap
// test if transparency is on or off
if (transparent)
{
// use version that will draw a transparent bitmap(slightly slower)
// first draw left half then right half
// draw each line of the bitmap
for (y=0; y<dest_height; y++)
{
// copy the next row into the destination buffer
for (x=0; x<left_width; x++)
{
// test for transparent pixel i.e. 0, if not transparent then draw
if ((pixel=layer_buffer_l[x]))
dest_buffer_l[x] = pixel;
} // end for x
// move to next line in destination buffer and in layer buffer
dest_buffer_l += SCREEN_WIDTH;
layer_buffer_l += layer_width;
} // end for y
// now right half
// draw each line of the bitmap
if (right_width)
{
for (y=0; y<dest_height; y++)
{
// copy the next row into the destination buffer
for (x=0; x<right_width; x++)
{
// test for transparent pixel i.e. 0, if not transparent then draw
if ((pixel=layer_buffer_r[x]))
dest_buffer_r[x] = pixel;
} // end for x
// move to next line in destination buffer and in layer buffer
dest_buffer_r += SCREEN_WIDTH;
layer_buffer_r += layer_width;
} // end for y
} // end if right side needs to be drawn
} // end if transparent
else
{
// draw each line of the bitmap, note how each pixel doesn't need to be
// tested for transparency hence a memcpy can be used (very fast!)
for (y=0; y<dest_height; y++)
{
// copy the next row into the destination buffer using memcpy for speed
_fmemcpy((void far *)dest_buffer_l,
(void far *)layer_buffer_l,left_width);
// move to next line in double buffer and in bitmap buffer
dest_buffer_l += SCREEN_WIDTH;
layer_buffer_l += layer_width;
} // end for y
// now right half if needed
if (right_width)
{
for (y=0; y<dest_height; y++)
{
// copy the next row into the destination buffer using memcpy for speed
_fmemcpy((void far *)dest_buffer_r,
(void far *)layer_buffer_r,right_width);
// move to next line in double buffer and in bitmap buffer
dest_buffer_r += SCREEN_WIDTH;
layer_buffer_r += layer_width;
} // end for y
} // end if right half
} // end else non-transparent version
} // end Layer_Draw
The function's interface is very similar to Layer_Build() except that the roles of the parameters are reversed. In the case of the Layer_Draw() function, the source parameters describe the bitmap in the layer that is to be mapped to the destination buffer (which in most cases will be the double buffer). The only new parameter needed for the drawing function is the transparency flag. As in the sprite engine, this flag is used to select between the sections of code that take transparent pixels into account or simply blast the bits without hesitation. Of course, the second method is preferred since it’s faster, but in some cases the transparent version of the code will be used if we wish to have trees or other objects that extend into other layers and overlap. The example program that follows doesn’t need to use transparency since it cuts all the layers into nice horizontal strips that don’t overlap, but in general, transparency is a good feature to keep in place. We are now ready to wave our hands (or claws) and perform some Cybersorcery. Let’s use the layers system to create an impressive demo in a couple dozen lines of code.
Surfing on Alien Worlds
After I wrote the layers system, I was trying to think of something cool for the demo of it, and at about 3:00 A.M., after watching the movie Aliens II for the zillionth time, I came up with the demo you will soon see. It uses the landscape pictured earlier, in Figure 4-25, as the background along with a small alien I drew with Deluxe Animation. Six layers are created and then moved at different velocities to simulate parallax. The details of the layers are listed in Table 4-1.
The heights of each layer are determined by making sections of the grass that are nearer to the viewpoint larger and larger. I arrived at the layer velocities experimentally based on the motivation that the farther the layer from the viewpoint, the slower it should scroll.
Finally, an alien on a jet board is surfing on the surface of the grass. The alien has no animation frames, but a scarf fluttering in the wind would be a nice touch. The alien moves at a constant velocity of two pixels per frame. The code that draws him uses the clipped versions of the sprite engine. When the alien has completely moved off the right edge of the screen, he is warped back to the left edge. The animation in this demo uses the double buffer and hence is flicker free.
The name of the demo is ALIEN.EXE and the source is called ALIEN.C. As usual, to create an executable, you’ll need both BLACK3.OBJ and BLACK4.OBJ and their respective header files. Run the demo and check it out. To exit, just press a key. The source for the demo is shown in Listing 4-29.
Listing 4-29 A parallax scrolling demo
// ALIEN.C - A demo of parallax scrolling
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
// D E F I N E S /////////////////////////////////////////////////////////////
#define MOUNTAIN_Y 106 // the starting vertical position of the mountains
#define GRASS_1_Y 150 // the starting vertical positions of the grass
#define GRASS_2_Y 155 // layers
#define GRASS_3_Y 163
#define GRASS_4_Y 172
#define GRASS_5_Y 188
#define MOUNTAIN_HEIGHT (1+149-106) // the height of each layer
#define GRASS_1_HEIGHT (1+154-150)
#define GRASS_2_HEIGHT (1+162-155)
#define GRASS_3_HEIGHT (1+171-163)
#define GRASS_4_HEIGHT (1+187-172)
#define GRASS_5_HEIGHT (1+199-188)
// G L O B A L S ////////////////////////////////////////////////////////////
pcx_picture image_pcx; // general PCX image used to load background and imagery
sprite alien; // our rocket sleding alien
layer mountains, // the layers for the mountains and grass
grass_1,
grass_2,
grass_3,
grass_4,
grass_5;
int mountain_x=0, // positions of scan window in each layer
grass_1_x=0,
grass_2_x=0,
grass_3_x=0,
grass_4_x=0,
grass_5_x=0;
RGB_color fire_color = {63,0,0}; // used for engines
// M A I N //////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
int done=0; // main event loop exit flag
// set the graphics mode to mode 13h
Set_Graphics_Mode(GRAPHICS_MODE13);
// create the double buffer
Create_Double_Buffer(200);
// load the imagery
PCX_Init((pcx_picture_ptr)&image_pcx);
PCX_Load("alienimg.pcx", (pcx_picture_ptr)&image_pcx,1);
// intialize the alien sprite
Sprite_Init((sprite_ptr)&alien,160,160,32,18,0,0,0,0,0,0);
// extract the bitmap for the alien
PCX_Get_Sprite((pcx_picture_ptr)&image_pcx,(sprite_ptr)&alien,0,0,0);
// done with this PCX file so delete memory associated with it
PCX_Delete((pcx_picture_ptr)&image_pcx);
// now load the background that will be scrolled
PCX_Init((pcx_picture_ptr)&image_pcx);
PCX_Load("alienwld.pcx",(pcx_picture_ptr)&image_pcx,1);
PCX_Copy_To_Buffer((pcx_picture_ptr)&image_pcx,double_buffer);
PCX_Delete((pcx_picture_ptr)&image_pcx);
// create the layers
Layer_Create((layer_ptr)&mountains,SCREEN_WIDTH,MOUNTAIN_HEIGHT);
Layer_Create((layer_ptr)&grass_1, SCREEN_WIDTH,GRASS_1_HEIGHT);
Layer_Create((layer_ptr)&grass_2, SCREEN_WIDTH,GRASS_2_HEIGHT);
Layer_Create((layer_ptr)&grass_3, SCREEN_WIDTH,GRASS_3_HEIGHT);
Layer_Create((layer_ptr)&grass_4, SCREEN_WIDTH,GRASS_4_HEIGHT);
Layer_Create((layer_ptr)&grass_5, SCREEN_WIDTH,GRASS_5_HEIGHT);
// scan layers out of double buffer, could have easily scanned from PCX
// file...just personal taste
Layer_Build((layer_ptr)&mountains,0,0,
double_buffer,0,MOUNTAIN_Y,SCREEN_WIDTH,MOUNTAIN_HEIGHT);
Layer_Build((layer_ptr)&grass_1,0,0,
double_buffer,0,GRASS_1_Y,SCREEN_WIDTH,GRASS_1_HEIGHT);
Layer_Build((layer_ptr)&grass_2,0,0,
double_buffer,0,GRASS_2_Y,SCREEN_WIDTH,GRASS_2_HEIGHT);
Layer_Build((layer_ptr)&grass_3,0,0,
double_buffer,0,GRASS_3_Y,SCREEN_WIDTH,GRASS_3_HEIGHT);
Layer_Build((layer_ptr)&grass_4,0,0,
double_buffer,0,GRASS_4_Y,SCREEN_WIDTH,GRASS_4_HEIGHT);
Layer_Build((layer_ptr)&grass_5,0,0,
double_buffer,0,GRASS_5_Y,SCREEN_WIDTH,GRASS_5_HEIGHT);
// main event loop, process until keyboard hit
while(!kbhit())
{
// move the alien
if ( (alien.x+=2) > 320)
alien.x = -32;
// move each layer
if ((mountain_x+=1) >= 319)
mountain_x -= 320;
if ((grass_1_x+=2) > 319)
grass_1_x -= 320;
if ((grass_2_x+=4) > 319)
grass_2_x -= 320;
if ((grass_3_x+=8) > 319)
grass_3_x -= 320;
if ((grass_4_x+=14) > 319)
grass_4_x -= 320;
if ((grass_5_x+=24) > 319)
grass_5_x -= 320;
// draw layers
Layer_Draw((layer_ptr)&mountains,mountain_x,0,
double_buffer,MOUNTAIN_Y,MOUNTAIN_HEIGHT,0);
// update background layer positions
Layer_Draw((layer_ptr)&grass_1,grass_1_x,0,
double_buffer,GRASS_1_Y,GRASS_1_HEIGHT,0);
Layer_Draw((layer_ptr)&grass_2,grass_2_x,0,
double_buffer,GRASS_2_Y,GRASS_2_HEIGHT,0);
Layer_Draw((layer_ptr)&grass_3,grass_3_x,0,
double_buffer,GRASS_3_Y,GRASS_3_HEIGHT,0);
Layer_Draw((layer_ptr)&grass_4,grass_4_x,0,
double_buffer,GRASS_4_Y,GRASS_4_HEIGHT,0);
Layer_Draw((layer_ptr)&grass_5,grass_5_x,0,
double_buffer,GRASS_5_Y,GRASS_5_HEIGHT,0);
// draw the sprite on top of layers
alien.visible = 1;
Sprite_Draw_Clip((sprite_ptr)&alien,double_buffer,1);
// change color of fire
fire_color.red = 20 + rand() % 44;
Write_Color_Reg(32,(RGB_color_ptr)&fire_color);
// display double buffer
Display_Double_Buffer(double_buffer,0);
// lock onto 18 frames per second max
Time_Delay(1);
} // end while
// exit in a very cool way
Screen_Transition(SCREEN_SWIPE_Y);
// free up all resources
Sprite_Delete((sprite_ptr)&alien);
Delete_Double_Buffer();
Set_Graphics_Mode(TEXT_MODE);
} // end main
The program is quite short for all that it does. And most of the code is for initialization purposes. There is a good point to be made here about game design. You should always try to make your support functions very powerful and self-contained; this will alleviate pressure from the main() and general game logic.
Let’s briefly cover the steps of the program to make sure we are both in the same Cyberdimension! The program begins by creating the double buffer and loading the imagery for the alien and layers. The alien image and the layers are then extracted from the PCX files. Then the main loop of the program begins by translating the alien and scrolling the layers. You will note that there is no erasure phase. This is unnecessary here because the background is dynamic–it is redrawn every frame and the sprite is drawn on the background. Hence, it would be a waste to scan the background and replace it under the sprite each frame. Next, the layers are drawn and the alien is drawn on top of the scenery. Since the rendering is done in the double buffer, the double buffer is copied into the video buffer for viewing followed by a short delay, and the process repeats. When the player presses a key, the program terminates with a screen transition and releases all the resources back to DOS.
Locking onto the Signals from Cyberspace
Animation and timing go hand in hand when implementing a video game. We have already seen a delay technique used at the end of the game loop for most of the demos. A call is made to Time_Delay() to synchronize the PC to an 18 FPS rate. However, this technique is not the only synchronization technique, and there are better ways to synchronize a game to a time source. One such method is synchronizing to the vertical blank period.
We learned that the VGA card displays 70 frames a second, or in other words, the video memory is rasterized 70 times a second. In most cases we can’t possibly update video memory at a rate this high; but no matter what we do to the video memory image, it will be redrawn 70 times a second when in mode 13h or, for that matter, mode Z. This means that there is an opportunity to track this event and use it to our advantage. The video image is drawn from top to bottom; then the electron gun turns off and retraces to the top-left corner of the video screen, and the process repeats.
During this retrace period, we have two things going for us. First, the video memory buffer is not being accessed, and this is probably the best time to copy the double buffer into video memory with the least amount of wait states. Second, if we write a function that waits for a vertical retrace to occur, we can create a time source that has a resolution of 1/70th of a second, which is much better than the system clock’s resolution of 1/18th of a second.
The question is, how do we track the start of a vertical retrace? Well, the VGA card has many internal flags and registers that reflect the state of the VGA as a function of time. And since the VGA is responsible for drawing the video image on the screen, you can bet that somewhere within the VGA card’s many registers there is a bit that describes the state of the vertical retrace. The name of this bit is the vertical retrace flag, and it can be found within one of the VGA’s input status registers, actually, number one. By testing the vertical retrace flag, we can detect whether a retrace is in progress or video is being displayed. Using this fact, we can write a function that tracks the beginning and end of a vertical retrace period, and hence, use this to synchronize the transfer of the double buffer to the video buffer. Or if we wish, we can use it for a general high-resolution time source.
The register containing the vertical retrace flag is called the VGA input status register 1. It is located at I/O port 0x3DAh. Table 4-2 lists the bit definitions of the register.
The bit we are interested in is of course bit 3, which is the retrace bit. Actually, bit 0 looks kind of interesting, but we’ll leave that alone in this lifetime. Referring to the table, we see that the retrace is in progress when bit 3 is a 1; hence, we can write a simple procedure that waits for the start of a vertical retrace by polling this bit. The function is shown in Listing 4-30.
Listing 4-30 A vertical retrace tracking function
void Wait_For_Vertical_Retrace(void)
{
// this function waits for the start of a vertical retrace, if a vertical
// retrace is in progress then it waits until the next one
// therefore the function can wait a maximum of 2/70ths of a second
// before returning
// this function can be used to synchronize video updates to the vertical blank
// or as a high resolution time reference
while(_inp(VGA_INPUT_STATUS_1) & VGA_VRETRACE_MASK)
{
// do nothing, vga is already in retrace
} // end while
// now wait for start of vertical retrace and exit
while(!(_inp(VGA_INPUT_STATUS_1) & VGA_VRETRACE_MASK))
{
// do nothing, wait for start of retrace
} // end while
// at this point a vertical retrace is occurring, so return back to caller
} // end Wait_For_Vertical_Retrace
The Wait_For_Vertical_Retrace( ) will perform the function of tracking the start of a vertical retrace and then returning to the caller. Therefore, if a retrace is in progress, the function waits until the start of the next retrace. You might decide to take out this wait and just have the function return if a retrace is in progress, but this way, we know the exact state of the retrace instead of having a general idea that it’s somewhere between starting and finishing. In most cases, we’ll be using the vertical retrace as the synchronization to copy the double buffer to the video buffer or as a high-resolution timer source. However, the latter purpose won’t be as important when we learn, later on in the book, to reprogram the PC’s internal timer to any time base we wish.
3D Palette Animation
Although the use of color register animation has died out in the past few years, it is still a viable method to create a plethora of special effects, such as lighting and motion. For example, in the classic game Doom, by id Software, palette animation is used to create the most excellent lighting effects that occur when a weapon is fired or when a bad guy is blasted, not to mention all those scary hallways with flickering lights. Palette animation is the technique of drawing images on the screen in specific colors and then changing the values of the color registers that the images are drawn with to create some effect. For example, the screen transition functions that we wrote used palette animation to fade the lights. Hence, by changing values of color registers, we can make it seem as though we are updating the video buffer at incredible rates. As another example, imagine that you wish to make something look like it’s moving when it’s not. You can do this by drawing the image in a set of adjacent color registers and then rotating the colors from register to register. Say we drew a waterfall with the three color registers indexed by 10, 11, and 12. Then we could make the water look like it’s moving by rotating these colors using the following algorithm:
- temp = color register 10
- color register 10 = color register 11
- color register 11 = color register 12
- color register 12 = temp
In essence we have animated the colors, which will make the water look like it’s moving. Although creating motion in this way is a bit old-fashioned, it still has its uses and is a valid technique to add to our list of spells. As a matter of fact, color rotation and palette animation were used (and still are) to synthesize 3D motion. As an example of this, I have created a program that uses color register animation to make a PCX file of a 3D road look like it’s moving. The program is called SPEED.EXE and the C source is called SPEED.C. The program is shown in Listing 4-31.
Listing 4-31 A 3D palette animation demo
// SPEED.C - A demo of 3-D palette animation
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
// D E F I N E S //////////////////////////////////////////////////////////////
// the color register ranges used by the road and side markers
#define START_ROAD_REG 16
#define END_ROAD_REG 23
#define START_MARKER_REG 32
#define END_MARKER_REG 34
// G L O B A L S ////////////////////////////////////////////////////////////
pcx_picture image_pcx; // general PCX image used to load background and imagery
// M A I N ///////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
RGB_color color_1, color_2; // used to perform the color rotation
int index; // looping variable
// set the graphics mode to mode 13h
Set_Graphics_Mode(GRAPHICS_MODE13);
// load the screen image
PCX_Init((pcx_picture_ptr)&image_pcx);
// load a PCX file (make sure it's there)
if (PCX_Load("speed.pcx", (pcx_picture_ptr)&image_pcx,1))
{
// copy the image to the display buffer
PCX_Show_Buffer((pcx_picture_ptr)&image_pcx);
// delete the PCX buffer
PCX_Delete((pcx_picture_ptr)&image_pcx);
// remap color registers to more appropriate values
// remap the side marker colors to red, red, white
color_1.red = 63;
color_1.green = 0;
color_1.blue = 0;
Write_Color_Reg(START_MARKER_REG, (RGB_color_ptr)&color_1);
Write_Color_Reg(START_MARKER_REG+1,(RGB_color_ptr)&color_1);
color_1.green = 63;
color_1.blue = 63;
Write_Color_Reg(START_MARKER_REG+2,(RGB_color_ptr)&color_1);
// now color the road all grey except for three slices
color_1.red = 20;
color_1.green = 20;
color_1.blue = 20;
for (index=START_ROAD_REG; index<=END_ROAD_REG; index++)
Write_Color_Reg(index,(RGB_color_ptr)&color_1);
// now color three of the slices a slightly brighter grey
color_1.red = 30;
color_1.green = 30;
color_1.blue = 30;
for (index=START_ROAD_REG; index<=END_ROAD_REG; index+=4)
Write_Color_Reg(index,(RGB_color_ptr)&color_1);
// wait for a keyboard press
while(!kbhit())
{
// rotate road colors
// temp = r1
// r1 <--- r2 <---- r3 <---- ... ri-1 <---- ri
// ri = temp
Read_Color_Reg(END_ROAD_REG, (RGB_color_ptr)&color_1);
for (index=END_ROAD_REG; index>START_ROAD_REG; index--)
{
// read the (i-1)th register
Read_Color_Reg(index-1, (RGB_color_ptr)&color_2);
// assign it to the ith
Write_Color_Reg(index, (RGB_color_ptr)&color_2);
} // end rotate loop
// place the value of the first color register into the last to
// complete the rotation
Write_Color_Reg(START_ROAD_REG, (RGB_color_ptr)&color_1);
// rotate side marker colors
Read_Color_Reg(END_MARKER_REG, (RGB_color_ptr)&color_1);
for (index=END_MARKER_REG; index>START_MARKER_REG; index--)
{
// read the (i-1)th register
Read_Color_Reg(index-1, (RGB_color_ptr)&color_2);
// assign it to the ith
Write_Color_Reg(index, (RGB_color_ptr)&color_2);
} // end rotate loop
// place the value of the first color register into the last to
// complete the rotation
Write_Color_Reg(START_MARKER_REG, (RGB_color_ptr)&color_1);
// synchronize to 2/18th of second or 9 FPS
Time_Delay(1);
} // end main loop
// use a screen transition to exit
Screen_Transition(SCREEN_WHITENESS);
} // end if pcx file found
// reset graphics to text mode
Set_Graphics_Mode(TEXT_MODE);
} // end main
The program is trivial, but it looks impressive! The program begins by loading in the PCX image of the road. Next, the color registers that make up the road and edges of the road are remapped with better colors. At this point, the main event loop is entered and each set of colors is rotated. This makes the road look like it’s moving.
You could actually make a little 3D race game with this technique. You could draw about ten different angles of the road with a paint program, and then as the road conditions changed, you would display each different view while performing the color rotation. The overall effect would be a seamless racing game. Moreover, by changing the color rotation rate, you could speed up or slow down the virtual car.
For the real 3D games, palette animation finds its best use in lighting effects, but who knows, maybe there are some other effects we can implement with this technique?
Flipping Out
We’re almost ready to complete this chapter’s material, but as I promised, we are going to cover page flipping using mode Z as the example mode. Double buffering is used primarily with mode 13h, since most of the other video modes supported by the VGA used a planar memory configuration with multiple display pages. If you recall, mode Z is 320x400 and uses four memory planes, where each plane holds a different subset of the final image. Hence, the amount of memory used by mode Z is 128K; but since it is divided into four planes, the video buffer is addressed at locations A000:0000h to A000:7CFFh, which is only the first 32,000 bytes of the buffer. There is another 32,000 bytes waiting to be exploited. Figure 4- 28 shows the layout of the two display pages for mode Z.
We’re going to add a couple functions to our mode Z library, allowing us to write to the second video page and to select which page is displayed at any time. The motivation behind page flipping is similar to double buffering. By having two memory pages to draw on, we can display one page while updating the other; then we can display the other. This process is repeated frame by frame, and since the player will never see the screen being updated, flicker won’t occur.
What is the main difference between page flipping and double buffering? Well, conceptually they are the same, but in implementation page flipping uses memory on the VGA for both video pages, and there isn’t a secondary step of copying the image from the double buffer into the video buffer as there is in a double buffer system. Hence, page flipping can be a bit faster than double buffer systems since the copying step is no longer necessary. However, doing all the drawing in video memory will be a bit slower than standard memory since there is the possibility of wait states. Remember, if the VGA rasterizer wants to access video memory at the same time the CPU does, the CPU will be told to wait a cycle or two. But all in all, page flipping is a good way to implement smooth animation in a planar video mode on the VGA. Furthermore, if you wish to write a game using a higher resolution, such as 640x480, you will undoubtedly use a page flipping system instead of a double buffer system, so the knowledge gained by page flipping mode Z is useful.
Page flipping mode Z takes only two functions, a couple of defines, and a global variable or two. Let’s begin by defining the problem. First, to be able to draw in the second half of the video buffer’s 64,000 bytes is very simple. We could add a constant of 32,000 to all our video buffer operations. Or we can simply advance the video pointer by 32,000 bytes (the method we’ll use) so all of our current functions work without change. For example, mode Z is accessed via a pointer to the video buffer that is aliased to A000:0000h. We could do something like this to access both pages,
Page_0 = 0; Page_1 = 32000; // to access video page zero video_buffer[Page_0 + offset] = value; // to access video page one video_buffer[Page_1 + offset] = value;
where offset is the standard offset in the video buffer that we calculate based on the (x,y) coordinate of the desired pixel.
The above method will work, but we would have to rewrite the Write_Pixel_Z( ) function to take this into consideration. A better method would be to adjust the video_buffer pointer itself to point to either page. This is the method that we’ll use along with a secondary global variable to track the page that video_buffer is pointing to. Here are the defines and variables needed to implement the tracking system:
// mode Z page stuff #define PAGE_0 0 #define PAGE_1 1 unsigned char far *page_0_buffer = (unsigned char far *)0xA0000000L; unsigned char far *page_1_buffer = (unsigned char far *)0xA0008000L; int mode_z_page = PAGE_0;
You see, we have a separate pointer to both pages of video memory. To switch active pages, we simply assign one of the page pointers to the global video_buffer pointer. Magically, all video updates occur in the proper page. For example, to change the active page to page 0, we would do the following:
video_buffer = page_0_buffer;
And we would record this in the variable mode_z_page like this:
mode_z_page = PAGE_0;
That’s the basic idea behind setting the active page for video updates. Listing 4- 32 shows the function to do all this.
Listing 4-32 Setting the active work page for mode Z
void Set_Working_Page_Mode_Z(int page)
{
// this function sets the page that all mode Z functions will update when
// called
if (page==PAGE_0)
video_buffer = page_0_buffer;
else
video_buffer = page_1_buffer;
} // end Set_Working_Page_Mode_Z
The function takes a single parameter, which is the page to set the video system to. And the possible choices have been defined in the header file as PAGE_0 or PAGE_1. We can then use the remaining mode Z functions since they all function relative to the video pointer, which has been changed by the above function to the desired video page. The next question is, how do we tell the VGA to display the second page? That’s a good question and takes a bit more research. But luckily, there is a register in the VGA that is designed for just such a purpose. The VGA has a pair of registers that define the start address of the video memory that should be mapped to the video screen. These registers are within the CRT controller at locations 0X0Dh and 0X0Ch. Figure 4-29 shows the details of this mapping. All we need to do is reprogram these registers to point to the second 32,000 bytes of the video memory, and instantly the second page will be displayed. I have defined the I/O port of theses registers for ease of access. Here’s the definition:
// these are used to change the visual page of the VGA
#define CRT_ADDR_LOW 0x0D // the index of the low byte of the start address
#define CRT_ADDR_HI 0x0C // the index of the hi byte of the start address
Using this information, let’s write a function that will write the value of 0 or
32,000 into these registers depending on whether or not we want to view page 0 or
page 1. Listing 4-33 contains the function’s code.
====Listing 4-33 Function to change the start address of video memory in the CRT controller====
<pre>void Set_Visual_Page_Mode_Z(int page)
{
// this function sets the visual page that will be displayed by the VGA
if (page==PAGE_0)
{
// re-program the start address registers in the CRT controller
// to point at page 0 @ 0xA000:0000
// first low byte of address
_outp(CRT_CONTROLLER,CRT_ADDR_LOW);
_outp(CRT_CONTROLLER+1,0x00);
// now high byte
_outp(CRT_CONTROLLER,CRT_ADDR_HI);
_outp(CRT_CONTROLLER+1,0x00);
} // end if page 0
else
if (page==PAGE_1)
{
// re-program the start address registers in the CRT controller
// to point at page 1 @ 0xA000:8000
// first low byte of address
_outp(CRT_CONTROLLER,CRT_ADDR_LOW);
_outp(CRT_CONTROLLER+1,0x00);
// now high byte
_outp(CRT_CONTROLLER,CRT_ADDR_HI);
_outp(CRT_CONTROLLER+1,0x80);
} // end else page 1
// note: we could use WORD, but this is clearer, feel free to change them
} // end Set_Visual_Page_Mode_Z
The function takes a single parameter, which is the page to be displayed. You might think, why do we have two functions? Shouldn’t we always be viewing the page that is the working or active page? The answer is no! The whole reason for page flipping is to allow us to show the user one page while manipulating the other out of view.
As a demo of page flipping, the following program draws some spheres in each video page and then toggles between the two pages. Granted, the demo isn’t as cool as ALIENS.EXE, but it gets the concept across and that’s the important part. The name of the demo is SPHERES.EXE and the source is called SPHERES.C. It’s shown in Listing 4-34.
Listing 4-34 Page flipping demo
// SPHERES.C - A demo of mode Z page flipping
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
// F U N C T I O N S /////////////////////////////////////////////////////////
void Draw_Sphere(int x0,int y0,int radius, int color)
{
// this function will draw a sphere in
int x1,y1,y2, // location coordinates
angle; // used to track current angle
// interate thru 90 degrees and use symetry to draw circle
for (angle=0; angle<90; angle++)
{
// draw the sphere as a collection of vertical strips
x1 = radius*cos(3.14159*(float)angle/(float)180);
y1 = -radius*sin(3.14159*(float)angle/(float)180)*1.66;
// draw the next vertical strip
for (y2=y0+y1; y2<y0-y1; y2++)
{
Write_Pixel_Z(x0+x1,y2,color);
Write_Pixel_Z(x0-x1,y2,color);
} // end for y2
} // end for angle
} // end Draw_Sphere
// M A I N ////////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
int index; // loop variables
// set the graphics mode to mode Z 320x400x256
Set_Mode_Z();
// clear out all of display memory, only page 1 was cleared during set_mode_z
Set_Working_Page_Mode_Z(PAGE_1);
Fill_Screen_Z(0);
// set visual and working page to page 0
Set_Visual_Page_Mode_Z(PAGE_0);
Set_Working_Page_Mode_Z(PAGE_0);
// draw some colored spheres on this page
for (index=0; index<50; index++)
{
Draw_Sphere(20+rand()%280,20+rand()%360,rand()%15, 34 + rand()%6);
} // end for index
// now draw grey spheres on page 1
Set_Working_Page_Mode_Z(PAGE_1);
for (index=0; index<50; index++)
{
Draw_Sphere(20+rand()%280,20+rand()%360,rand()%15, 16 + rand()%16);
} // end for index
// now toggle between pages
while(!kbhit())
{
Set_Visual_Page_Mode_Z(PAGE_0);
Time_Delay(5);
Set_Visual_Page_Mode_Z(PAGE_1);
Time_Delay(5);
} // end while
// restore the video system to text
Set_Graphics_Mode(TEXT_MODE);
} // end main
Odds and Ends
We are at the end of our 2D animation journey. But before we leave and learn about input devices in the next chapter, I want to add a couple of functions to our repertoire that can be used with a double buffer system. The function listings and source are in BLACK4.C and BLACK4.H, but I want you to at least see the function prototypes so you won’t be surprised by them when we use them later. The double buffer modified functions are:
void Print_Char_DB(int xc,int yc,char c,int color,int transparent); void Print_String_DB(int x,int y,int color, char *string,int transparent); void Write_Pixel_DB(int x,int y,int color); int Read_Pixel_DB(int x,int y);
They are identical to the video buffer versions except the reference to video_buffer has been changed to double_buffer.
Summary
In this chapter we covered so many topics it’s amazing that we’re both still breathing! We learned about double buffer animation systems and implemented a sprite engine complete with clipping. We also learned how to load PCX images off disk. Furthermore, we got a taste of collision detection using bounding boxes. Also along the way, we created a multilayer scrolling system, which can be used to create a parallax scrolling environment that mimics 3D perspective of a moving viewpoint. Next, we learned how to synchronize our game to the heartbeat of Cyberspace via the vertical retrace period. Finally, we took another look at the new mode Z and did a little page flipping. Along the way, we got to see a multitude of demos that used all the techniques and code we’ve built up to this chapter. Now it’s time to let the users of our games have a bit of control, but not too much!
Continue
- Black Art of 3D Game Programming: Writing Your Own High-Speed 3D Polygon Video Games in C Table of Contents
- Black Art of 3D Game Programming, Chapter 5: Communicating with the Outside World
| Copyright 2006 Andre LaMothe |
|
