Alex Russell's Dos Game Programming in C for Beginners

Introduction . Chapter 1 . Chapter 2 . Chapter 2.1 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6

Chapter 2.1

More Graphic Primitives

A sprite is a small bitmap that is animated on the screen. One of the basic things done in a game is to animate sprites quickly on the screen. Virtually everything that moves in a 2D game is a sprite.

Solid sprites

The simplest type of sprite is the solid sprite. The whole rectangle of the sprite is drawn to the screen, and nothing can be seen 'through' it. This boils down to copying a rectangle of memory to the off screen buffer.

This is the simple structure that we will use to store a sprite:

WORD width
WORD height
Bytes bitmap (width*height bytes)

// blit (draw) a solid sprite
// sprite points to a chunk of memory that contains:
// width (2 bytes), height (2 bytes), bitmap (width*height bytes)

void blit_sprite(unsigned char far *spr, int x, int y)
{
	unsigned char far *p;
	int width, height;

	// make p point to starting point in off_screen
	p=off_screen + y*screen_width + x;

	// get the width and height, and make spr point to the
	// start of the bitmap
	_fmemcpy(&width, spr, 2);
	spr+=2;
	_fmemcpy(&height, spr, 2);
	spr+=2;

	while ( height-- )
		{
		_fmemcpy(p, spr, width); // copy one line of sprite to off_screen
		spr+=width;		     // move to next line in sprite
		p+=screen_width;	     // move to next line on screen
		}

}

Transparent sprites

Solid sprites are not used that often in games, except for tiles. Usually an irregular shaped image is what you want to draw, e.g. a monster. To draw a sprite with transparent parts the simplest method is to simply skip pixels that are a certain 'key' colour. We will use colour zero (0) as our transparent colour. This is a simple method to implement, but it isn't very quick as you have to do a compare for every pixel. Once we master this method a faster method will be presented.

// draw a sprite, skipping pixels that are zero.
// This is a SLOW way to draw a transparent sprite
void blit_sprite_masked(unsigned char far *spr, int x, int y)
{
	unsigned char far *p;
	int width, height, w, adj;

	// make p point to starting point in off_screen
	p=off_screen + y*screen_width + x;

	// get the width and height, and make spr point to the
	// start of the bitmap
	_fmemcpy(&width, spr, 2);
	spr+=2;
	_fmemcpy(&height, spr, 2);
	spr+=2;

	// adj is the amount to move p to get to the next line
	// we will be moving p by width as we draw
	adj=screen_width - width;

	while ( height-- )
		{
		w=width;
		while ( w-- )	// draw one 'line' of the sprite
			{
			if ( *spr )  // check if *spr is zero
				{
				*p++=*spr++;  // draw one pixel, move p, and spr one to left
				}
			else
				{
				spr++;	// skip one pixel
				p++;
				}

			}

		p+=adj;   // move down one line
		}

}

RLE transparent sprites

Doing a compare for each pixel is a slow way to draw a transparent sprite. A better (one of many better ways) is to encode the sprite with RLE (Run Length Encoding). RLE is a simple compression method that we will alter to speed up drawing transparent sprites. This is a two step process, 1. The sprite is converted to a RLE sprite, 2. We draw the RLE sprite. A RLE sprite has codes imbedded in it that tell us to skip, or draw strings of pixels. A string of pixels just means some pixels in a row.

To make a RLE sprite we read in the sprite, and count zero, and non zero pixels. These are then replaced with codes that indicate when we skip, and when we draw. This speeds things up because we only have to do a compare at the end of each string, instead of for each pixel.

Example

A zero byte means the next byte is the number of bytes to SKIP. A non-zero bytes means the next byte is the number of pixels to draw, and it will be followed by that number of pixels.

Lets look at one line of a simple bitmap that has zero bytes on each end.
000000000111224567864450000000

This will be encoded as follows:

 00,09,01,14, 11122456786445, 00,07
 a  b  c  d   e               f  g

Byte a is a zero, so the next byte will be the number of pixels to skip. Byte b is 9, the number of pixels to skip Byte c is a 1 so the next byte will be the number of pixels to draw. Byte d is 14 which means the there are 14 pixels to draw Byte e is the start of the 14 pixels to draw Byte f is zero so the next byte is the number of pixels to skip Byte g is the number of pixels to skip which brings us to the end of the line

There are also other methods, for example a sprite can be compiled which turns it into code that draws the sprite. You then draw the sprite by actually 'calling' the sprite as a function.

/*
blit a RLE encoded sprite

   0 byte next byte is number of bytes to skip
   1 byte next byte is number of bytes to draw
*/
void blit_sprite_rle(unsigned char far *spr, int x, int y)
{
	unsigned char far *p, cnt;
	int width, height, w, adj;

	// make p point to starting point in off_screen
	p=off_screen + y*screen_width + x;

	// get the width and height, and make spr point to the
	// start of the bitmap
	_fmemcpy(&width, spr, 2);
	spr+=2;
	_fmemcpy(&height, spr, 2);
	spr+=2;

	// adj is the amount to move p to get to the next line
	// we will be moving p by width as we draw
	adj=screen_width - width;

	while ( height-- )
		{
		w=0;
		while ( w < width )
			{
			if ( *spr )
				{
				// set cnt to the number of pixels to draw
				spr++;
				cnt=*spr;
				spr++;
				while ( cnt-- ) // draw pixels
					*p++=*spr++;
				}
			else
				{
				// set cnt to the number of pixels to skip
				spr++;
				cnt=*spr;
				spr++;
				p+=cnt;  // skip cnt pixels
				}

			w+=cnt;
			}

		p+=adj;
		}

}

Restoring backgrounds

Now we can draw sprites of any shape on the screen. The next step would be to animate the sprite. One immediate problem is that we will need to erase the sprite as we move it. This is done by saving what is UNDER the sprite before we draw it, then restoring what we saved before drawing the next frame. When animating many sprites these saved patches of screen must be restored in the reverse order that they were saved in. We will cover getting a rectangle of memory from the off screen buffer here, and go into more detail on restoring screens later.

To save what is under the sprite is as simple as copying a rectangle of memory from the off screen buffer into a temporary sprite. This is just the opposite of blit_sprite().

// get a patch of screen from off screen and save it as a sprite
// spr must point to enough memory to hold width*height + 4 bytes
void get_sprite(unsigned char far *spr, int x, int y, int width, int height)
{
	unsigned char far *p;

	// copy width and height to the sprite
	_fmemcpy(spr, &width, 2);
	spr+=2;
	_fmemcpy(spr, &height, 2);
	spr+=2;

	// make p point to starting point in off_screen
	p=off_screen + y*screen_width + x;

	// copy memory from off_screen to the sprite
	while ( height-- )
		{
		_fmemcpy(spr, p, width);
		spr+=width;
		p+=screen_width;
		}
} 

Graphic Text

All games require that some text be printed on the screen. This is done by drawing the text on the screen pixel by pixel using various fonts as a template. The VGA card has a number of built in fonts that can be accessed, and we can create our own fonts. Fonts are usually stored in a compact format where each bit defines whether a pixel is on or off. This saves space, and as fonts are monochrome, all the information we need to draw it. This is the way the VGA stores its own ROM fonts. We will be learning how to use the VGA's built in fonts to draw text.

Drawing text requires a few steps: ask the bios where the fonts are, decide which font to use, and actually draw the text. We will implement this with three functions: text_init(), set_font(), and draw_char(). To draw a string of text we will call draw_char() repeatedly.

text_init() will call the video bios to get the address of the 8x8 ROM font, and the 8x14 ROM font and save both addresses to pointers so we can access them quickly. set_font() will simply select one of these fonts as our current font.

// graphic text global vars ------------------------------------------------------
int CharHeight;
int CharWidth;
unsigned char far *FontPtr;	// pointer to current font
unsigned char far *F8x8Ptr;     // pointers to the two VGA ROM fonts
unsigned char far *F8x14Ptr;

// get the address of the two VGA ROM fonts and save them.
// make the 8x8 font active

void text_init(void)
{
	struct REGPACK reg;

	reg.r_ax=0x1130;    // 30 get info on current set, 11 character info
	reg.r_bx=0x0300;    // get 8x8 font info

	intr(0x10, &reg);

	// es:bp points to 8x8 font
	F8x8Ptr=MK_FP(reg.r_es, reg.r_bp);

	reg.r_ax=0x1130;  // 30 get info on current set, 11 font info
	reg.r_bx=0x0200;	// get 8x14 font info

	intr(0x10, &reg);

	// es:bp points to 8x14 font
	F8x14Ptr=MK_FP(reg.r_es, reg.r_bp);

	// make the 8x8 font the active font
	FontPtr=F8x8Ptr;
	CharWidth=8;
	CharHeight=8;

}

void set_font(int font_id)
{
	if ( font_id == 0 )
		{
		FontPtr=F8x8Ptr;
		CharWidth=8;
		CharHeight=8;
		}
	else
		{
		if ( font_id == 1 )
			{
			FontPtr=F8x14Ptr;
			CharWidth=8;
			CharHeight=14;
			}
		else
			{
			// user defined font, not implemented

			}

		}
}

// c is the character to draw
// must call text_init first!
// this code assumes all fonts are 8 pixels wide, stored as a bitmapped byte

void draw_char(int c, int x, int y, int forecolour, int backcolour)
{
	unsigned char far *p, far *fnt;
	int width, height, adj;
	unsigned char mask;

	// make p point to the screen
	p=off_screen + y*screen_width + x;
	adj=screen_width - CharWidth;

	// make fnt point to the start of the character we want to draw.
	// characters are 1 byte wide, and height bytes tall
	fnt=FontPtr + c*CharHeight;

	height=CharHeight;
	while ( height-- )
		{
		width=CharWidth;
		mask=128; 			// bit mask: 10000000
		while ( width-- )		// assumes width of 8
			{
			if ( (*fnt) & mask )	 // is this bit set?
				{
				// draw pixel
				*p++=forecolour;
				}
			else
				{
				// draw background colour
				*p++=backcolour;
				}

			mask>>=1;  // shift mask to check next bit
			}

		p+=adj;  // next line on screen
		fnt++;   // next line of font
		}
}

For anything but a test game you will want to create fonts that look nicer than the built in VGA fonts. To do this you have to create a font, load this font into memory, and write code to draw it. If you use the same format as the VGA fonts you can re-use the above code. Another option that makes text look nicer is to implement proportional fonts. That is, fonts were each character can be a different width. One easy way to do this is limit all characters to a maximum of 8 pixels, and store the width for each character in a separate table. Then the only change you make to the code is the check for the width.

Loading images from drawing programs

All the art for your game will be created in commercial drawing programs which will save their files in many different formats, none of which will be convenient to use in a game. To use the art from drawing programs you must know the file format that the artwork is stored with, and how to convert the stored file into a linear bitmap. The general technique is: read the artwork into memory, convert it into a linear bitmap in memory, save the linear bitmap to disk in a in a format that is easy to use in the game. For large games the size of the art might mean you will want to store it compressed, and uncompress at runtime. For this course we will be storing all our sprites, and other game art as uncompressed linear bitmaps with a width and height header.

We will cover one simple file format now: PCX. PCX files are stored with a small header, RLE encoded artwork, and a 256 colour palette at the end. PCX files were originally for art less than 256 colours, and they just added the 256 colour palette at the end. The RLE encoding used for PCX files is very simply, but doesn't give very good compression. For information on other file formats try: http://www.wotsit.org

We will now develop a small program to read in PCX files, and write them out as linear bitmaps, or RLE bitmaps that can be directly load into memory, and drawn with the code we have already written.

The program will take command line parameters for the file names, and which format to write them out in.

Program Outline

Read one of the specified files into memory. Decode into a plain linear bitmap. If RLE is requested, convert to RLE. Save to disk.

MAKESPR.EXE USE: Makespr filename. [/r]


e.g.
makespr *.pcx /r
makespr *.pcx
makespr b*.pcx c*.pcx
makespr b*.pcx c*.pcx /r

Filename can contain wildcards. There can be any number of file names. If /r is used the output will be RLE sprites, otherwise plain sprites are saved.

The interesting functions are:
pack_shape();
unpack_pcx();
save_bitmap();
save_bitmap_rle();
main();

/*

    makespr.c

    Internet: alexander.russell@telus.net
    Copyright 1998, October 5 by Alex Russell, ALL rights reserved

    Created - 1998/10/5

    History:
        New file

*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// a simple sprite
typedef struct
   {
   unsigned short width, height;
   unsigned char far *bitmap;
   }
shape_t;

// for all pcx files (far as I know)
typedef struct
   {
   char manu; /// usually 10
   char version;
   char encoding;
   char bits_per_pixel;
   int xmin, ymin, xmax, ymax;
   int hres, yres;
   char palette[48];  // not used by 256 color pcx
   char reserved;
   char color_planes;
   int bytes_per_line;
   int palette_type;
   char spare[58];
   }
pcx_head_t;


#define PAL_SIZE 768
#define PCX_HD_SIZE 128

#define BSIZE 4096

// we are in medium model, so fread() expects a NEAR data pointer.
// Therefore we have to read the file into near mem, then copy it
// to the far buffer using _fmemcpy()

// allocate far memory, and load a file into that memory.
// only for files <64Kb in size!!!

unsigned char far *far_load(char *fname, unsigned int *size)
{

	FILE *fp;
	unsigned char *b;
	unsigned char far *p, far *t1;
	unsigned int u, i;

	*size=0;
	p=NULL;

	fp=fopen(fname, "rb");
	if ( fp )
		{
		u=filelength(fileno(fp));
		*size=u;
		p=farmalloc(u);
		if ( p )
			{
			t1=p;
			b=malloc(1024); // malloc 1Kb on the near heap
			if ( b )
				{
				while ( u )
					{
					if ( u < 1024 )
						{
						i=fread(b, 1, u, fp);
						if ( i == u )
							{
							_fmemcpy(t1, b, u);
							u=0;
							}
						else
							break; // error, quit
						}
					else
						{
						i=fread(b, 1, 1024, fp);
						if ( i == 1024 )
							{
							_fmemcpy(t1, b, 1024);
							t1+=1024;
							u-=1024;
							}
						else
							break;  // error
						}
					}

				free(b);
				}
			}

		fclose(fp);
		}

	return p;

}


// used to write out the far bitmaps to file, just like fwrite(), but it accepts
// far pointers for `s'
/* ---------------------- far_fwrite() ------------------- March 10,1997 */
int far_fwrite(char far *s, unsigned int m, unsigned int n, FILE *fp)
{
   long i=0;
   int j;
   char *b;
   long size;

   b=malloc(BSIZE);
   if ( b )
      {
      size=(long )m*n;
      while ( size )
         {
         if ( size >= BSIZE )
            {
            _fmemcpy(b, s, BSIZE);
            size-=BSIZE;
            j=BSIZE;
            }
         else
            {
            _fmemcpy(b, s, size);
            j=size;
            size=0;
            }
         i+=fwrite(b, 1, j, fp);
         s+=j;
         }

      free(b);
      }
   else
      printf("out of near mem\n");

   return(i);
}


/*

   pack a shape to speed up drawing TRANSPARENT shapes

   run length encoded
   0 byte next byte is number of bytes to skip
   1 byte next byte is number of bytes to draw

   only useful for TRANSPARENT SHAPES

   all color zero areas will be transparent
   MAX width and height is 255!!!!!
   packed line by line for speed
   this is NOT a good way to pack stuff if interested in saving space
   returns size of packed bitmap
	shp2->bitmap must already be allocated and big enough for the RLE sprite

*/
/* ---------------------- pack_shape() ------------------- April 15,1993 */
int pack_shape(shape_t *shp1, shape_t *shp2)
{
   char far *s0, far *s1, far *t;
   short num, x, y;
   unsigned short size=0;

   s0=shp1->bitmap;
   s1=shp2->bitmap;

   for ( y=0; y < shp1->height; y++ )
      {
      x=0;
      while ( x < shp1->width )
         {
         if ( *s0 )
            {
            /* bytes isn't a zero so copy over until end of line or a zero */
            num=0;
            *s1++=1;
            t=s1;    /* save where the count goes */
            s1++;
            size+=2;

            /* copy and count non-zero bytes */
            while ( *s0 && x < shp1->width )
               {
               num++;
               x++;
               *s1++=*s0++;
               size++;
               }

            /* store count */
            *t=num;
            }
         else
            {
            /* count number of zeros to skip and store count */
            num=1;
            *s1++=0;
            s0++;
            x++;
            size++;

            while ( *s0 == 0 && x < shp1->width )
               {
               num++;
               x++;
               s0++;
               }

            *s1++=num;
            size++;
            }
         }
      }

   return size;

}


/* for 256 color PC images ONLY, works with dpaint images
   Make sure b is big enough for unpacked image.
   Don't use this to get ideas for compression as
   there are much better ways to do even simple RLE encoding.
*/
/* ---------------------- unpack_pcx() ------------------ January 4,1995 */
int unpack_pcx(char *fname, unsigned char far *b, unsigned char *pal,
               int *p_width, int *p_height)
{
   int err=0, i;
   unsigned int size;
   pcx_head_t pcx;
   unsigned char far *t1, far *raw;

   raw=far_load(fname, &size);  // load the whole file into mem,

   if ( raw )
      {
      if ( size > PCX_HD_SIZE )
         {
         _fmemcpy(&pcx, raw, PCX_HD_SIZE);  // far memcpy
         // get width and height
         *p_width =pcx.xmax - pcx.xmin + 1;
         *p_height=pcx.ymax - pcx.ymin + 1;
         // get palette
         // the palette is just tacked onto the end of the file
         _fmemcpy(pal, raw+size-PAL_SIZE, PAL_SIZE);

	// must shift the palette values as the VGA only uses 6 bits	
         for ( i=0, t1=pal; i < PAL_SIZE; i++, t1++ )
            *t1>>=2;

         // this type of de-compression is common to most pcx files

         t1=raw+PCX_HD_SIZE; // skip header

         // decompress raw into b -------------------------------
         size-=PCX_HD_SIZE; // skip header
         size-=PAL_SIZE; // don't include palette at end
         while ( size )
            {
            if ( (*t1 & 0xc0) == 0xc0 ) // are the two high bits set?
               {
               // hi bits set, its a run
               i=*t1 & 0x3f;    // mask with 00111111 to get size of run
               t1++;            // next byte is color to repeat
               size--;
               while ( i-- )
                  *b++=*t1;
               t1++;
               size--;
               }
            else
               {
               *b++=*t1++;  // not a run, copy one byte
               size--;
               }
            }

         farfree(raw);
         }
      else
         err=2;
      }
   else
      err=1;

   return(err);
}


int save_bitmap(unsigned char far *buff, unsigned int width,
                unsigned int height, char *fname)
{
	char new_name[25], *t1;
	FILE *fp;
	int err=0;

	// make a new file name, by removing the pcx extension, and
	// adding a m13 extension

	strcpy(new_name, fname);
	t1=new_name;
	while ( *t1 && *t1 != '.' )
		t1++;

	if ( *t1 == '.' )
		{
		t1++;
		strcpy(t1, "m13");
		}
	else
		strcat(new_name, ".m13");

	// write out the file
	fp=fopen(new_name, "wb");
	if ( fp )
		{
		fwrite(&width, 1, 2, fp);
		fwrite(&height, 1, 2, fp);
		far_fwrite(buff, 1, width*height, fp);

		fclose(fp);
		}
	else
		{
		printf("ERROR! creating file: %s\n", new_name);
		err=1;
		}


	return err;

}

int save_bitmap_rle(unsigned char far *buff, unsigned int width,
                unsigned int height, char *fname)
{
	char new_name[25], *t1;
	FILE *fp;
	shape_t in, out;
	unsigned int size;
	int err=0;

	// make a new file name, by removing the pcx extension, and
	// adding a rle extension

	strcpy(new_name, fname);
	t1=new_name;
	while ( *t1 && *t1 != '.' )
		t1++;

	if ( *t1 == '.' )
		{
		t1++;
		strcpy(t1, "rle");
		}
	else
		strcat(new_name, ".rle");

	// as much memory as we will ever need
	out.bitmap=farmalloc(64000u);
	if ( out.bitmap )
		{
		in.width=width;
		in.height=height;
		in.bitmap=buff;


		out.width=in.width;
		out.height=in.height;

		// turn the linear bitmap into a RLE bitmap
		size=pack_shape(&in, &out);
		if ( size )
			{
			// write out the file
			fp=fopen(new_name, "wb");
			if ( fp )
				{
				fwrite(&width, 1, 2, fp);
				fwrite(&height, 1, 2, fp);
				far_fwrite(out.bitmap, 1, size, fp);

				fclose(fp);
				}
			else
				err=3;

			}
		else
			err=1;

		farfree(out.bitmap);
		}
	else
		err=2;

	return err;
}

void main(int argc, char *argv[])
{
	int is_rle=0;
	int i, width, height;
   	struct ffblk fb;
	unsigned char far *buff;
	unsigned char pal[PAL_SIZE];  // we just throw it away

	printf("Makespr V1.0e\n");
	printf("Alex Russell's dos mode13h graphic course\n");


	// argc is the number of parameters passed in on the command line
	// including the name of the exe which is always the first parm
	if ( argc < 2 )
		{
		printf("USE: makespr filename... [/r]\n");
		return;
		}

	// check for the RLE save flag in the command line parms
	// argv[] is an array of pointers to the command line parameters
	for ( i=1; i < argc; i++ )
		{
		if ( *argv[i] == '/' )
			{
			if ( *(argv[i] + 1) == 'r' )
				{
				is_rle=1;
				break;
				}
			}
		}


	// memory to load the raw PCX into
	buff=farmalloc(64000u);
	if ( buff )
		{
		// for each file name, create a sprite
		for ( i=1; i < argc; i++ )
			{
			if ( *argv[i] != '/' )
				{
				// it isn't a /r switch, so lets process it
				memset((char *)&fb, 0, sizeof(fb));
				// filename can have wild cards, so look for all matching files
				// see your c compiler run time lib manual for info on findfirst()
				if ( !findfirst(argv[i], &fb, 0) )
					{
            				do
						{
						printf("Processing: %s\n", fb.ff_name);
						// load and unpack the pcx into buff
						if ( !unpack_pcx(fb.ff_name, buff, pal, &width, &height) )
							{
							// save the unpacked image
							if ( is_rle )
               							save_bitmap_rle(buff, width, height, fb.ff_name);
							else
								save_bitmap(buff, width, height, fb.ff_name);
							}
						}
            				while ( !findnext(&fb) );
            				}
				}
			}

		farfree(buff);
		}
	else
		printf("OUT of mem! [1]\n");

}	

/* ------------------------------ EOF -------------------------------- */

Chapter 2.1 Exercises

1. Write a function to draw a solid sprite that doesn't use memcpy().

2. Write a function to draw a string of text on the screen. Use this prototype:
void draw_string(char *s, int x, int y, int text_colour);

3. Write a function to completely fill the screen with a solid sprite. Do not fill the right and bottom edge if it will cause the sprite to go past the edge of the screen.

4. Write a program that draws two sprites side by side, gets both sprites from the off screen buffer as ONE large sprite, and finally draws this new large sprite below the original two sprites.

5. Write a program that tiles a solid sprite on the screen, completely filling it (as 4) then re- tiles a transparent tile on top. Specify the names of both sprites on the command line.

6. Explain in plain language how a PCX file is laid out, and how it compresses images.

7. Write a function that compares the speed of masked versus RLE transparent sprites. Do not wait for the vertical blank when timing. Clock() may be used to time. Try various sprites. Which method is faster? What type of sprite sees the largest speed up?

8. Write a program that loads solid sprites specified on the command line, and shows them on the screen, one at a time. ESC quits, any other key shows the next sprite. Centre the sprites in the middle of the screen.

9. Extend the draw_text() function to handle variable width fonts to a max of 8 pixels wide. The height will remain fixed. Use any format that is convenient to store the widths.

10. Write a function to load any other file format other than PCX, convert to a linear bitmap, and save as a .M13 file. Use makespr as a template, and support the same command line features. A simple file format, if you have access to dpaint, is LBM/BBM. BMP files are another option, but they have a relatively complex file format.

Click here for chap21.zip (28 KB)

Introduction . Chapter 1 . Chapter 2 . Chapter 2.1 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6

Copyright 1998 (c), Alex Russell, All rights reserved