The sprite editor we'll be exploring in this chapter is the same one that is incorporated into the game editor introduced in Chapter 3. The actual code file used to implement the sprite editor is SPRITE.C. The global definitions that the editor needs are found in EDITDEFS.H. The sprite editor is the biggest coding project that I've introduced so far. Table 7.1 provides a description of the main functions used to help you navigate through the source file.
Table 7.1 Functions Used in SPRITE.C
Function | Description |
activate_sprite_editor() | Main event loop for sprite editor |
animate_sprite_list() | Displays all sprites in sequence |
array_to_sprite() | Copies from array to working variables |
bitmap_to_grid() | Copies sprite from RAM to fat bit grid |
bounding_box() | Sets bounding box limits |
calculate_sprite_size() | Eliminates blank rows and columns |
check_sprite_suffixes() | Checks for '.PCX' on import files |
clear_sprite() | Sets all pixels to background color |
delete_sprite() | Removes a sprite from the list |
draw_sprite_editor() | Draws the screen |
edit_sprites(void) | Main calling function |
flip_sprite(void) | Rotates the sprite around a vertical axis |
flood_fill_sprite(void) | Flood fills an area |
get_minimal_sprite() | Trims empty rows and columns |
get_sprite() | Copies sprite from video memory to RAM |
import_sprite(void) | Imports a sprite from a PCX file |
init_sprite() | Initializes sprite editor |
init_this_spritelist() | Loads from disk |
load_edit_sprites(void) | Loads a new sprite list, then calls edit_sprites |
load_sprites(void) | Initializes and loads sprites from disk |
mask_sprite() | Removes transparent (black) background |
move_grid_boundary() | Moves the grid boundary around |
next_sprite() | Views/edits next sprite in the list |
previous_sprite() | Views/edits previous sprite in the list |
put_spritenum() | Displays number of current sprite on status line |
restore_this_sprite() | Copies sprite from RAM to fat bit grid |
save_sprite() | Saves the sprite list to disk |
set_grid_boundary() | Turns grid boundary on or off |
set_sprite_background_color() | Selects a background color |
set_sprite_foreground_color() | Selects a foreground color |
set_sprite_grid() | Draws a small rectangle on fat bit grid |
set_sprite_point() | Draws a point on sprite area |
show_sprite_coords() | Displays coordinates in status area |
sprite_to_array() | Copies from working variables to array |
transpose_sprite_colors() | Sets all background color pixels to foreground color |
undo_sprite() | Undoes last edit (works as a toggle) |
update_sprite_old() | Updates undo information |
xor_horiz_line() | Creates horizontal xor line for crosshairs |
xor_vert_line() | Creates vertical xor line for crosshairs |
The first task of designing any usable utility like the sprite editor is drawing the screen. The screen should be attractive and functional, but not too fancy. Keeping in mind that our sprite editor takes precedence over form, we'll draw the editor as cleanly and simply as possible. As shown in Figure 7.1, the sprite editor has six main parts. Let's look at how each of these parts is created.
Figure 7.1 The main parts of the sprite editor.
If you recall from Chapter 3, the fat bit grid is the rectangular area that shows a magnified version of part of the sprite. Here, a pixel in the sprite is represented by a small rectangle in the fat bit grid, as shown in Figure 7.2. The sprite may be edited by clicking on rectangles in the fat bit grid. The general idea is that it's easier to see magnified parts of the sprite, and it's easier to guide the mouse to a rectangle, which is a bigger target than a single pixel.
Figure 7.2 Using the fat bit grid to edit sprites.
The fat bit grid is built by drawing a single large black rectangle, then using crisscrossing gray rectangles to create a grid. The width of the fat bit grid is 128 pixels. This value is divided by lines at four-pixel increments, creating a total of 32 grid boxes in the horizontal direction. Similarly, the 40 grid boxes in the vertical direction take 160 pixels. The code to create the grid is located in the function draw_sprite_editor():
/* draw the background for the fat bit grid */ fg_setcolor(black); fg_rect(8,136,26,186); /* use vertical bars to divide into 32 horizontal grid boxes */ x = 8; fg_setcolor(gray); for (i = 0; i <= 32; i++) { fg_rect(x,x,26,186); x += 4; } /* use horizontal bars to divide into 40 vertical grid boxes */ y = 26; for (j = 0; j <= 40; j++) { fg_rect(8,136,y,y); y += 4; } /* fill the squares with gray dots */ for(i = 0; i < 32; i++) { for(j = 0; j < 40; j++) { x = (i * 4) + 10; y = (j * 4) + 28; fg_setcolor(gray); fg_point(x,y); } }
Each box in the fat bit grid is initially set to black with a gray dot in the middle of it. This indicates the sprite has a zero pixel at this location. Color zero is the transparent color, so a black box with a gray dot represents a transparent pixel.
The fg_point() function draws a point.
void fg_point(int ix, int iy);* ix is the point's x coordinate
* iy is the point's y coordinate
The fat bit grid shows only a part of the whole sprite. For example, it may show our sprite Tommy's legs, waist, and shoulders, but not his head. We also want to look at the whole sprite while it's being edited, so we allocate an area of the screen for this purpose. The sprite area is located to the right of the fat bit area. It consists of a rectangle 96 pixels wide and 96 pixels tall, which should be plenty large enough for most of our sprites. Certain sprites, such as a big enemy, could be larger than 96x96 pixels, and would have to be handled as a special case. But we're not designing the sprite editor for special cases, we're designing it to handle large quantities of ordinary sprites. After all, our primary concern is streamlining the process of importing artwork into the game. A smaller sprite area will allow us to handle more sprites in RAM at once, so the 96x96 pixel size seems to be about right.
Since the sprite area shows more of the sprite than the fat bit grid, we need some way of highlighting which part of the sprite is currently being edited. The grid boundary is a hollow box 32 pixels wide by 40 pixels tall. It outlines the part of the sprite that is currently visible in the fat bit grid. You can use your mouse to move the grid boundary around, which will cause different parts of the sprite to appear in the fat bit grid. The grid boundary is drawn using an xor method, so that drawing the box in the same location twice will make it disappear. The code to make the grid boundary appear and disappear is in the set_grid_boundary() function:
void set_grid_boundary(int status) { /* turn the grid boundary on if it is off */ if (status == ON && grid_boundary == OFF) { fg_mousevis(OFF); fg_setcolor(white); /* use an xor box to draw the grid boundary */ fg_boxx(144+x_offset,144+x_limit,26+y_offset,26+y_limit); grid_boundary = ON; } /* turn the grid boundary off if it is on */ else if (status == OFF && grid_boundary == ON) { fg_mousevis(OFF); fg_setcolor(white); /* use an xor box to erase the grid boundary */ fg_boxx(144+x_offset,144+x_limit,26+y_offset,26+y_limit); grid_boundary = OFF; } }
We have 256 palettes available to us. These are displayed at the far left side of the screen. In general, I try to use only the first 32 palettes in any sprite, and reserve the other 224 palettes for the background. Palette 0 is usually black, and will be transparent in a sprite. Menus are drawn in 32 rows of eight rectangles each. This is done conveniently in a loop in the draw_sprite_editor() function as follows:
/* palettes */ y = 26; for (i = 0; i < 32; i++) { fg_setcolor(i); fg_rect(291,293,y,y+4); fg_setcolor(i+32); fg_rect(294,296,y,y+4); fg_setcolor(i+64); fg_rect(297,299,y,y+4); fg_setcolor(i+96); fg_rect(300,302,y,y+4); fg_setcolor(i+128); fg_rect(303,305,y,y+4); fg_setcolor(i+160); fg_rect(306,308,y,y+4); fg_setcolor(i+192); fg_rect(309,311,y,y+4); fg_setcolor(i+224); fg_rect(312,315,y,y+4); y += 5; }
The foreground and background colors are highlighted in two larger rectangles at the bottom of the screen. The foreground color is the color used when the left mouse button is pressed. The background color is used when the right mouse button is pressed.
The user can select a sprite editor command by using the mouse to choose an item from a menu. The menu is displayed on the right side of the screen next to the palettes. Each menu item has its first letter highlighted in blue, which allows us to select a menu item either by clicking on it, or typing the highlighted letter. To highlight the first letter, we need to make a temporary string containing only the first letter of the menu item and null terminator. Then we overwrite the menu string with the temporary string:
char *string[] = { "Clear", "Hflip", "Trans", "Fill", "Undo", "Mask", "Bound", "Import", "Save", "Del", "Next", "Prev", "Quit" }; /* draw the menu area in white and outline it in black */ fg_setcolor(white); fg_rect(248,289,26,156); fg_setcolor(black); fg_box(248,289,26,156); /* null terminate the temporary string, which will hold the first letter of each menu item */ temp_string[1] = '\0'; x = 252;y = 34; for (i = 0; i < 13; i++) { /* draw the menu item */ fg_setcolor(black); put_bstring(string[i],x,y); /* highlight the first letter of the menu item in blue */ temp_string[0] = string[i][0]; fg_setcolor(blue); put_bstring(temp_string,x,y); y+=10; }
Two-way communication is necessary for any worthwhile utility. A program needs to tell us what its current status is, and what it expects from us. We, in turn, need an area to give information to the program.
The status area in the sprite editor is a white rectangular bar at the bottom of the screen. It asks us questions like "Save the sprite file Yes/No?," and we give it answers like "Y" or "N." We may also type in a filename in this area.
Other information about the editing process is displayed in this area, including the current x and y position of the mouse cursor on the fat bit grid, the current sprite number in the list, and whether we are currently performing a flood fill.
The status area is drawn in white with a black bar above it as shown here:
/* bottom status area */ fg_setcolor(white); fg_rect(0,319,190,199); fg_setcolor(black); fg_rect(0,319,189,189);
After drawing the components of the sprite editor, we load and initialize the sprites. Recall from Chapter 3 that sprites are saved in files with an LST extension. Each file can contain a maximum of 10 sprites, which is about all the sprites that will fit into RAM at one time in the sprite editor. A sequence of sprites is called a sprite list, and each LST file contains one sprite list. I find it convenient to store similar sprites in a single sprite list; for example, all the walking sprites go in one sprite list, and all the shooting sprites go in another sprite list. Only one sprite list can be loaded into the sprite editor at a time.
Sprites are stored in structures, and the sprite list is stored in RAM as an array of structures. The sprite structures are declared at the top of the SPRITE.C file as follows:
typedef struct _sprite /* sprite structure */ { char far *bitmap; int width; int height; int xorg; int yorg; int bound_x; int bound_y; int bound_width; int bound_height; } SPRITE; SPRITE sprite[10]; /* sprite list array */
Since the sprites are rather large--9,216 bytes each--only a limited number of them can be stored in RAM at one time. We allow room for 10 sprites in the array. If you have more than 10 sprites, put them in two sprite lists--which means two separate files. .
The code to read sprites from a file is in the init_sprites() function and looks like this:
/* open the file */ if ((tstream = fopen(spritelist_fname,"rb")) != NULL) { /* how many sprites are there? */ fread(&nsprites,sizeof(int),1,tstream); /* read one sprite at a time */ for (i = 0; i < nsprites; i++) { fread(&sprite_width,sizeof(int),1,tstream); fread(&sprite_height,sizeof(int),1,tstream); if (sprite_width <= 0) sprite_width = 1; if (sprite_height <= 0) sprite_height = 1; nbytes = sprite_width*sprite_height; fread(&sprite_xorg,sizeof(int),1,tstream); fread(&sprite_yorg,sizeof(int),1,tstream); fread(&sprite_boundx,sizeof(int),1,tstream); fread(&sprite_boundy,sizeof(int),1,tstream); fread(&sprite_boundwidth,sizeof(int),1,tstream); fread(&sprite_boundheight,sizeof(int),1,tstream); fread(bitmap,sizeof(char),nbytes,tstream); /* is there room for another sprite in RAM? */ if ((sprite[i].bitmap = malloc(9216)) != NULL) { /* store the sprite in a RAM array */ sprite_to_array(i); } /* out of room -- that was the last sprite! */ else { nsprites = i+1; break; } } fclose(tstream); /* current sprite is sprite 0 */ current_sprite = 0; array_to_sprite(current_sprite); } /* no file open */ else { memset(bitmap,0,9216); sprite_xorg = 144; sprite_yorg = 121; sprite_width = 96; sprite_height = 96; sprite_boundx = 0; sprite_boundy = 0; sprite_boundwidth = 96; sprite_boundheight = 96; current_sprite = 0; sprite[current_sprite].bitmap = malloc(9216); }
The first integer value in the file is the number of sprites in the current sprite list. Following that is the data for each sprite. Each sprite has a width and height, an x and y origin, bounding box information, and bitmap data. If the sprite editor cannot open the file , it initializes the sprite by assigning some default values to the sprite structure members. The x and y origin are assumed to be 144 and 121, respectively. The width and height are assigned the values 96, and the bounding box is assigned an area as large as the largest possible sprite.
The sprite is stored in RAM twice, once as a temporary working copy of the sprite we are currently editing, and once as a more permanent copy stored in the sprite array. When we want to edit a different sprite--for example the "next" or "previous" sprite--we copy the current sprite into the sprite array, then we copy the desired sprite out of the sprite array into the temporary working variables. This is accomplished in the functions sprite_to_array() and array_to_sprite(), as follows:
void sprite_to_array(int n) { int nbytes; sprite[n].width = sprite_width; sprite[n].height = sprite_height; sprite[n].xorg = sprite_xorg; sprite[n].yorg = sprite_yorg; sprite[n].bound_x = sprite_boundx; sprite[n].bound_y = sprite_boundy; sprite[n].bound_width = sprite_boundwidth; sprite[n].bound_height = sprite_boundheight; nbytes = sprite_width*sprite_height; memcpy(sprite[n].bitmap,bitmap,nbytes); } void array_to_sprite(int n) { int nbytes; sprite_width = sprite[n].width; sprite_height = sprite[n].height; sprite_xorg = sprite[n].xorg; sprite_yorg = sprite[n].yorg; sprite_boundx = sprite[n].bound_x; sprite_boundy = sprite[n].bound_y; sprite_boundwidth = sprite[n].bound_width; sprite_boundheight = sprite[n].bound_height; if (sprite_width <= 0) sprite_width = 1; if (sprite_height <= 0) sprite_height = 1; nbytes = sprite_width*sprite_height; memcpy(bitmap,sprite[n].bitmap,nbytes); }
Once the sprite data is in the temporary working variables, it may be manipulated in RAM and blitted to the screen as needed.
An ASCII file, called the sprite data file, contains the names of all the sprite lists. Usually I like to call this file SPRITE.DAT, or if there is more than one, I will call them SPRITE0.DAT, SPRITE1.DAT, and so on. The idea is that you'll have different sprites in different levels, but there will be some overlap between levels. While some sprites will appear in more than one level, you don't want to store the sprite data more than once, you just want to keep track of the filenames in each level. So you have a sprite list file for each level that specifies which sprites need to be loaded for that level. Currently, the sprite editor is designed to handle 13 sprite lists of 10 sprites each, which means you can have 130 unique sprites per level. I don't know if you really need that many sprites in a level; it seems like a generous number. If you have more sprites than that, you will definitely face memory problems. On the other hand, if your game has many small sprites, and you need room for more than 130 sprites, you can either increase the size of the sprite data file or use two sprite data files.
After we have loaded the sprites, we'll need to view them. We'll use Fastgraph's fg_drwimage() function to display the sprite in the sprite area:
fg_move(sprite_xorg,sprite_yorg); fg_drwimage(bitmap,sprite_width,sprite_height);
This code is used frequently to display sprites. For example, in the previous_sprite() function, the last sprite in the list is displayed, as shown here:
void previous_sprite() { /* display the previous sprite */ if (current_sprite > 0) { update_sprite_old(); set_grid_boundary(OFF); get_minimal_sprite(); sprite_to_array(current_sprite); current_sprite--; array_to_sprite(current_sprite); fg_mousevis(OFF); fg_setcolor(0); fg_rect(144,239,26,121); fg_move(sprite_xorg,sprite_yorg); fg_drwimage(bitmap,sprite_width,sprite_height); get_sprite(); bitmap_to_grid(); put_spritenum(); } }
The fg_drwimage() function displays an image stored as a mode specific bitmap. The image will be positioned so that its lower-left corner is at the graphics cursor position (or the text cursor position in text video modes).
void fg_drwimage (char *map_array, int width, int height);
*map_array is the arbitrary-length array containing the bitmap.
*width is the width in bytes of the bitmap.
*height is the height in bytes (pixel rows) of the bitmap.
void bitmap_to_grid() { register int i,j; int color; int x,y; int byte_ptr; fg_mousevis(OFF); get_sprite(); /* copy to fat bit grid */ byte_ptr = 0; for (j = 95; j >= 0; j--) { for (i = 0; i < 96; i++) this_sprite[i][j] = bitmap[byte_ptr++]; } /* set the points in the fat bit, if they have changed */ for (i = 0; i < 32; i++) { for (j = 0; j < 40; j++) { x = x_offset+i; y = y_offset+j; if (this_sprite[x][y] != old_sprite[x][y]) { color = (int)this_sprite[x][y]; set_sprite_grid(i,j,color); } } } }
We've introduced another global array here. This is a two-dimensional array called this_sprite. I find it convenient to edit pixels in an array that has the same width and height as the sprite being edited. The this_sprite array contains the same data as the bitmap array, but is organized a little differently. Since Fastgraph displays bitmaps from bottom to top, and we're addressing pixels from top to bottom, we have to do a little conversion here.
I'm sometimes asked why Fastgraph displays bitmaps from the bottom to the top, when memory is usually addressed from the top to the bottom. There are a couple of good reasons for this, and one not-very-good reason. The good reasons are, we're usually more concerned with the bottom of a sprite than the top of a sprite. We're interested in the location of Tommy's feet, but we don't much care where the top of his head is. Similarly with fonts, we're more concerned about where the bottom of a letter is than the top. We want our letters to line up on a given line even if they're different heights. For these reasons, when we first developed Fastgraph it seemed like the proper way to address a bitmap was from the lower-left corner. We made that decision many years ago. But since then, we've wondered if we made the right decision. These days it's standard to address a bitmap from the upper-left corner. The problem is, if we change Fastgraph now, we'll break everybody's code. People hate it when you do that. For now, Fastgraph will continue to address bitmaps from the lower- left corner, but we may provide an option in the future to change the location of a bitmap origin. If you have a strong opinion about this, I suggest you let Ted know how you feel about it. If enough people ask him to change the code, he'll probably do it.
Another array is introduced in the bitmap_to_grid() function: the old_sprite array. This array holds the undo information for the sprite editor. Whenever a sprite is changed, the old sprite is stored. If you make an undesirable change, you can go back to the earlier sprite by selecting the undo option. As we update the fat bit grid, we compare the current sprite to the old sprite. If the byte has not changed, there is no reason to redraw the rectangle. This saves a significant amount of time in redrawing the fat bit editor.
Each square in the fat bit editor is updated using the function set_sprite_grid(). This function simply calculates the position and draws the rectangle as follows:
void set_sprite_grid(int i,int j,int color) { /* draw a little rectangle on the fat bit editor */ int x,y; fg_mousevis(OFF); x = (i * 4) + 9; y = (j * 4) + 27; this_sprite[i+x_offset][j+y_offset] = (char)color; fg_setcolor(color); fg_rect(x,x+2,y,y+2); if (color == 0) { fg_setcolor(gray); fg_point(x+1,y+1); } }
Notice that if the current color is zero (the transparent color), a small gray dot is displayed in the box to show that it's a transparent pixel.
The fat bit grid is updated by clicking on the squares. These squares then change color, depending on what the current color is and which mouse button was pressed. As the fat bit grid is updated, the sprite itself must also be updated. This is done by drawing a point in the sprite area, using the set_sprite_point() function:
void set_sprite_point(int x,int y,int color) { /* just set a point to the current color */ fg_mousevis(OFF); fg_setcolor(color); fg_point(x,y); }
When the sprite is modified in RAM, as with some of the editing functions, it must then be copied back to video memory. The restore_this_sprite() function updates both the sprite area and the fat bit grid:
void restore_this_sprite() { register int i,j; int color; int x,y; for (i = 0; i < 32; i++) { for (j = 0; j < 40; j++) { x = x_offset+i; y = y_offset+j; if (this_sprite[x][y] != old_sprite[x][y]) { color = (int)this_sprite[x][y]; set_sprite_grid(i,j,color); } } } set_grid_boundary(OFF); fg_mousevis(OFF); for(i = 0; i < 96; i++) { for(j = 0; j < 96; j++) { if (this_sprite[i][j] != old_sprite[i][j]) { color = (int)this_sprite[i][j]; fg_setcolor(color); fg_point(144+i,26+j); } } } set_grid_boundary(ON); }
Copying the sprite from video memory back into RAM is accomplished using the get_sprite() function. This is done to update the bitmap array, which may then be copied to the sprite list array or written to a file:
void get_sprite() { /* get the sprite from the sprite area, store in a RAM bitmap */ sprite_width = 96; sprite_height = 96; sprite_xorg = 144; sprite_yorg = 121; fg_move(sprite_xorg,sprite_yorg); fg_getimage(bitmap,sprite_width,sprite_height); }
This collection of functions moves the sprite data around between RAM and video memory. As mentioned earlier, there may be more optimal ways to accomplish this, but these functions seem to do the job well enough. Now that we have pretty good control over our sprite data, we can start to do things with it. The first thing we want to do is be able to move the data around in the fat bit grid.
Since the fat bit grid displays a magnified version of the sprite, it can only display a part of the sprite at any one time. For example, if we are currently looking at Tommy's head and we want to modify his feet, we'll need to move the visible area down a few pixels, as shown in Figure 7.3. We do this by dragging the grid boundary around.
Figure 7.3 Moving the visible area down to view another portion of a sprite.
We start by defining a global variable called grid_boundary:
int grid_boundary; /* flag - is boundary box on or off? */
The grid_boundary variable is a global boolean integer value. It will be set to either ON or OFF. The grid boundary will be drawn as an xor box, so if it is on, drawing it again will turn it off, and vice versa.
Four other global variables define the extents of the grid boundary. These are changed as the grid boundary is dragged around:
int x_offset; /* x location of the grid boundary box */ int x_limit; /* width of the grid boundary box */ int y_offset; /* y location of the grid boundary box */ int y_limit; /* height of the grid boundary box */
Every time the bitmap is copied to or from the sprite area, the grid boundary must be turned off, then turned back on when the operation is complete. The code to turn the grid boundary off and on looks like this:
void set_grid_boundary(int status) { /* turn the grid boundary on if it is off */ if (status == ON && grid_boundary == OFF) { fg_mousevis(OFF); fg_setcolor(white); /* use an xor box to draw the grid boundary */ fg_boxx(144+x_offset,144+x_limit,26+y_offset,26+y_limit); grid_boundary = ON; } /* turn the grid boundary off if it is on */ else if (status == OFF && grid_boundary == ON) { fg_mousevis(OFF); fg_setcolor(white); /* use an xor box to erase the grid boundary */ fg_boxx(144+x_offset,144+x_limit,26+y_offset,26+y_limit); grid_boundary = OFF; } }
Similarly, the grid boundary is dragged by repeatedly turning it off and on as the mouse is moved. After the grid boundary is moved to a new location, the fat bit editor is redrawn to show the desired part of the sprite.
Although the sprite editor contains many powerful editing features, in general, sprites are not usually created in the sprite editor. It is most common to create sprites in a paint program and import the sprite images into the sprite editor. There are several reasons for this approach. Most artists are more comfortable using their favorite paint program, and they'll be more productive using familiar tools. Also, there is simply more functionality in a paint program. You can look at many sprites simultaneously, for example, and you can superimpose sprites on top of each other to gauge animated movements. Also, sprites saved in PCX files can be imported into a number of popular programs, including animation programs where sprite movement can be prototyped. The import_sprite() function assumes that you have created sprites elsewhere and stored them in a PCX file. The PCX file should be in a 320x200x256 resolution, which will be compatible with the video mode we are using. This shouldn't be any problem. If your PCX file was created at some other resolution, it is easy enough to find a commercial or shareware program to convert it. The import_sprite() function is used often, so it was designed to be reasonably user friendly. Here's the function code:
void import_sprite() { /* import a PCX file */ unsigned char key,aux; char fname[13]; int error; char *strptr; int index; int buttons,count; int old_xmouse,old_ymouse; int corner_x,corner_y; int dx,dy; int skip; int x,y,x2,y2; skip = FALSE; fg_mousevis(OFF); fg_setcolor(black); put_bstring("PCX file name:",80,197); put_bstring(sprite_pcxname,170,197); /* try to read in a filename */ fg_getkey(&key,&aux); if (key == CR) strcpy(fname,sprite_pcxname); else get_bstring(fname,170,197,12,key,0); error = FALSE; strptr = strchr(fname,'.'); /* period in string */ if (strptr > 0) { index = (int)(strptr - fname); if (index > 8)error = TRUE; else if ((strcmpi(&fname[index],".pcx") == 0) || fname[index+1] == '\0') error = FALSE; if (!error && fname[index+1] == '\0') strcat(fname,"pcx"); } /* no period in string */ else { if (strlen(fname) > 8) error = TRUE; if (!error) strcat(fname,".pcx"); } if (!error) { if (!file_exists(fname)) error = TRUE; } if (error) { fg_setcolor(white); fg_rect(80,289,190,199); fg_setcolor(black); put_bstring("File not found.",80,197); wait_for_keystroke(); } else { set_grid_boundary(OFF); strcpy(sprite_pcxname,fname); /* display the PCX file on page 1 */ fg_setpage(1); fg_move(0,0); fg_showpcx(fname,1); fg_setvpage(1); fg_mousepos(&xmouse,&ymouse,&buttons); old_xmouse = xmouse; old_ymouse = ymouse; /* draw crosshairs */ fg_setcolor(white); xor_horiz_line(0,319,ymouse); xor_vert_line(xmouse,0,199); /* move the crosshairs around until the left button is pressed */ while(buttons == 0) { fg_waitfor(1); fg_mousepos(&xmouse,&ymouse,&buttons); if (xmouse != old_xmouse || ymouse != old_ymouse) { xor_horiz_line(0,319,old_ymouse); xor_vert_line(old_xmouse,0,199); xor_horiz_line(0,319,ymouse); xor_vert_line(xmouse,0,199); old_xmouse = xmouse; old_ymouse = ymouse; } /* check for the Escape key */ fg_intkey(&key,&aux); if (key == ESC) { skip = TRUE; break; } } /* clear the crosshairs */ xor_horiz_line(0,319,ymouse); xor_vert_line(xmouse,0,199); /* return to sprite editor if Esc was pressed */ if (skip) { fg_setpage(0); fg_setvpage(0); fg_mouselim(0,319,0,199); fg_setcolor(white); fg_rect(80,289,190,199); fg_waitfor(3); fg_mousebut(1,&count,&xmouse,&ymouse); fg_mousebut(2,&count,&xmouse,&ymouse); return; } /* no more crosshairs, now draw a box around the sprite */ corner_x = xmouse; corner_y = ymouse; y2 = MIN(199,ymouse+95); x2 = MIN(319,xmouse+95); fg_mouselim(xmouse+2,x2,ymouse+2,y2); /* move the box around until a button is pressed */ while(buttons > 0) { fg_waitfor(1); fg_mousepos(&xmouse,&ymouse,&buttons); if (xmouse != old_xmouse || ymouse != old_ymouse) { fg_boxx(corner_x,old_xmouse,corner_y,old_ymouse); fg_boxx(corner_x,xmouse,corner_y,ymouse); old_xmouse = xmouse; old_ymouse = ymouse; } /* check for the Esc key interrupt */ fg_intkey(&key,&aux); if (key == ESC) { skip = TRUE; break; } } /* clear the box */ fg_boxx(corner_x,xmouse,corner_y,ymouse); /* make sure it is a non-zero sprite */ if (corner_x >= xmouse || corner_y >= ymouse) skip = TRUE; /* sprite not imported */ if (skip) { fg_setpage(0); fg_setvpage(0); fg_mouselim(0,319,0,199); fg_setcolor(white); fg_rect(80,289,190,199); fg_waitfor(3); fg_mousebut(1,&count,&xmouse,&ymouse); fg_mousebut(2,&count,&xmouse,&ymouse); return; } /* sprite imported, calculate width and height */ dx = xmouse - corner_x; dy = ymouse - corner_y; /* get the sprite */ fg_move(corner_x,ymouse); fg_getimage(bitmap,dx,dy); /* back to sprite editor */ fg_setpage(0); fg_setvpage(0); /* clear the old sprite */ fg_setcolor(0); fg_rect(144,239,26,121); x = 144 + (96 - dx)/2; y = 122 - (96 - dy)/2; fg_move(x,y); /* draw the new sprite */ fg_drwimage(bitmap,dx,dy); update_sprite_old(); bitmap_to_grid(); } /* fix the mouse limits */ fg_mouselim(0,319,0,199); /* clear the status bar */ fg_setcolor(white); fg_rect(80,289,190,199); /* clear the mouse buttons */ fg_waitfor(3); fg_mousebut(1,&count,&xmouse,&ymouse); fg_mousebut(2,&count,&xmouse,&ymouse); sprite_changed = TRUE; }
The first thing the import_sprite() function does, after declaring local variables, is prompt for a filename by displaying a message on the status line. The function even provides a default filename (the name of the last PCX file imported). A carriage return accepts the default, any other key triggers a call to the get_bstring() function which will accept bitmapped character input and store the result in the fname string. The fname string is then put through a series of tests. First, we check for a period in the string. If it is there, we check that the period and the characters following the period match the string ".pcx." We use the C strcmpi() function to do the comparison without regard to upper- and lowercase letters. If we find a period with no characters after it, we append the PCX file extension.
Similarly, if we don't find period in the string, and if the string is less than eight characters, we append the file extension. This allows us to type in filenames in a hurry, without worrying about typing in the file extension. We then check that the file exists. If it does not exist, or if there was any other error in typing in the filename, we display an error message, "file not found," and wait for a keystroke.
If we get past the filename error checking, we can proceed with the import. First, though, we turn off the grid boundary, in preparation for writing to the sprite area. Then we copy the PCX filename into the global string pcxname so we can use this name as the default on the next import.
The PCX file is displayed on page 1, and we use Fastgraph's fg_setvpage() function to make page 1 the visual page. At this point, we can see the PCX file and whatever sprite images may be on it. The mouse cursor is off, but mouse movement is tracked using crosshairs. This is accomplished by drawing xored horizontal and vertical lines passing through the current mouse position (as returned by fg_mousepos()). The crosshairs allow us to move around freely in a loop until either the left mouse button is pressed, or the Esc key is pressed.
Images are selected in the PCX file by positioning the crosshairs on the upper-left corner, and then holding the left mouse button down and dragging the mouse to the lower-right corner of the image. As the button is held down, the crosshairs are replaced by an xor box, similar to the one used in the grid boundary. The box shrinks and grows as the mouse moves. We ensure that the mouse can only move down and to the right by using the fg_mouselim() function to constrain the mouse to the part of the screen below and to the right of the upper-left corner of the image. If we decide that the upper-left corner is in the wrong place, no problem--the Esc key allows us to break out of this function at any time and start over.
After the sprite is outlined with the xor box, we are ready to import it into the sprite editor. First we turn off the xor box so we have a clean image on page 1. Then we calculate the width and height of the image and use Fastgraph's fg_getimage() function to grab the sprite and store it in the bitmap array. The sprite editor screen is made visible again, and the bitmap is blitted to the sprite area using Fastgraph's fg_drwimage() function. The destination position is calculated on the fly--the sprite will be roughly centered in the sprite area. The fat bit grid is updated by calling the bitmap_to_grid() function. Finally, the screen is restored to what it was before. The status area is cleared, the mouse limits are reset to the whole screen, and the mouse buttons are "cleared" in preparation for the next action. The sprite import is complete.
As I mentioned before, sprites are generally not created in the sprite editor. They are created in a paint program, such as Deluxe Paint or NeoPaint, and imported into the sprite editor. Once in the sprite editor, the editing functions are used to do minor touch-ups, or to change the orientation of a sprite. I'm not going to document all of the sprite editing functions here, because they are simply not that interesting, but I'll show you a few of them and describe how they work. If you want to see the others, take a look at the code in the SPRITE.C file.
The clear_sprite() function turns all the pixels in the bitmap to the background color. It does this by modifying the this_sprite array, then updating the fat bit grid. It also clears the sprite area by drawing a rectangle over it. Notice how the grid boundary is turned off before the sprite area is modified, then turned back on:
void clear_sprite() { register int i,j; /* clear the sprite to the background color */ update_sprite_old(); for(i = 0; i < 96; i++) { for(j = 0; j < 96; j++) { this_sprite[i][j] = (char)background_color; } } /* also set the fat bit grid */ for (i = 0; i < 32; i++) { for (j = 0; j < 40; j++) { set_sprite_grid(i,j,background_color); } } set_grid_boundary(OFF); fg_setcolor(background_color); fg_rect(144,239,26,121); set_grid_boundary(ON); sprite_changed = TRUE; }
The clear_sprite() function introduces another global variable: sprite_changed. This global variable is a flag that tells us whether or not a sprite has been modified. Before we exit the sprite editor, we'll have a look at this flag. If we notice that a sprite has been changed, we'll prompt ourselves to save the new sprite data to disk.
The flip_sprite() function can be very useful. If the sprite is drawn in the wrong orientation, for example facing left when you want it to face right, it's sometimes convenient to import it as is and just flip it once you get it in the sprite editor. The flip_sprite() function modifies the this_sprite array by copying it to a temporary array in reverse, and then copying the temporary array back into the main array as shown in Figure 7.4. Before it does that, flip_sprite() saves the sprite data in the old_sprite array. The restore_this_sprite() function is then called to copy the new sprite to the sprite area and the fat bit grid.
void flip_sprite() { /* rotate by 180 degrees */ char temp_grid[96]; register int i,j; for(j = 0; j < 96; j++) { for(i = 0; i < 96; i++) { old_sprite[i][j] = this_sprite[i][j]; temp_grid[i] = this_sprite[95-i][j]; } for(i = 0; i < 96; i++) this_sprite[i][j] = temp_grid[i]; } restore_this_sprite(); sprite_changed = TRUE; }
Figure 7.4 Reversing the this_sprite array.
The transpose_sprite_colors() function changes all pixels of one color to another color. If the current background color is green, and the foreground color is red, all the green pixels will be changed to red . Once again, the modifications are done in RAM by modifying the this_sprite array, and the changes are copied to video memory using the restore_this_sprite() function.
void transpose_sprite_colors() { /* set everything that is the background color to the foreground color */ register int i,j; for(i = 0; i < 96; i++) { for(j = 0; j < 96; j++) { old_sprite[i][j] = this_sprite[i][j]; if (this_sprite[i][j] == (char)background_color) this_sprite[i][j] = (char)foreground_color; } } restore_this_sprite(); sprite_changed = TRUE; }
Throughout this discussion, we've talked about sprites as full 96x96 bitmaps. It's convenient to edit fixed-sized sprites, but we would not want to put them in our game that way. A 96x96 bitmap takes almost 9K of storage! It would be very wasteful to put a sprite that size in our game. We need to trim the sprite down to the smallest size, then record the width and height. The calculate_sprite_size() function determines the sprite origin, width, and height by eliminating all the transparent pixels on the top, bottom, left, and right:
void calculate_sprite_size() { /* Figures out the smallest rectangle containing the entire sprite */ int x,y; int bottom,top,left,right; unsigned char *sprite; bottom = 95; top = 0; /* impossible values for the edges */ left = 95; right = 0; sprite = bitmap; for (y = 0; y < 96; y++) for (x = 0; x < 96; x++) if (*sprite++) /* found a non-transparent pixel! */ { if (x<left) left = x; /* if (further left) new left edge */ if (x>right) right = x; /* if (further right) new right edge */ if (y<bottom) bottom = y; /* if (further down) new bottom edge */ if (y>top) top = y; /* if (further up) new top edge */ } if (left == 95) /* if (left edge still impossible) sprite must be empty */ { sprite_width = 1; /* give empty sprites a 1-pixel size for grins */ sprite_height = 1; sprite_xorg = 144; sprite_yorg = 122; } else { sprite_width = right - left + 1; sprite_height = top - bottom + 1; sprite_xorg = 144 + left; sprite_yorg = 122 - bottom - 1; } }
This function looks complicated but it really isn't. It handles four cases: the bottom, top, left, and right. It figures out the smallest rectangle containing the entire sprite by setting the left edge to something impossibly large like the rightmost edge of the bitmap, x=95. It walks through the sprite, and if it sees a pixel that's nearer the left edge, it pulls the left edge over to where that pixel is. The same sort of thing happens for the right, top, and bottom edges.
We end up with four values: right, left, bottom, and top. The width of the sprite is calculated by subtracting the left from the right and adding one. Similarly, the height of the sprite is calculated by subtracting the bottom from the top and adding one. The x and y origins are calculated in terms of the sprite area on the sprite editor screen. These values are not used in the game itself.
The case of a blank sprite is handled by giving it a width and height of one pixel. This prevents problems in trying to read null data from a file. In general, it's best to not store empty sprites, but in case you accidentally do, it's nice to write code that can handle it.
Anytime you can write code that will handle a tedious task for you, you are ahead of the game. Game programmers should get in the habit of looking for solutions like this. Even though you can solve the sprite reduction problem by hand (simply by editing the sprite in a paint program) you don't want to do that. It's too boring. Writing code to solve a problem is always less boring than solving the problem by brute force.
Cover |
Contents |
Downloads
Fastgraph Home Page |
books |
Magazine Reprints
Copyright © 1998 Ted Gruber Software Inc. All Rights Reserved.
Awards |
Acknowledgements |
Introduction
Chapter 1 |
Chapter 2 |
Chapter 3 |
Chapter 4 |
Chapter 5 |
Chapter 6
Chapter 7 |
Chapter 8 |
Chapter 9 |
Chapter 10 |
Chapter 11 |
Chapter 12
Chapter 13 |
Chapter 14 |
Chapter 15 |
Chapter 16 |
Chapter 17 |
Chapter 18
Appendix |
License Agreement |
Glossary |
Installation Notes |
Home Page
So you want to be a Computer Game Developer