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
Solid sprites
This is the simple structure that we will use to store a sprite:
Transparent sprites
RLE transparent sprites
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.
This will be encoded as follows:
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.
Restoring backgrounds
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().
Graphic 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.
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
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]
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:
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:
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
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.
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.
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
}
}
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
}
}
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.
000000000111224567864450000000
00,09,01,14, 11122456786445, 00,07
a b c d e f g
/*
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;
}
}
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.
// 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;
}
}
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.
// 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, ®);
// 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, ®);
// 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
}
}
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.
e.g.
makespr *.pcx /r
makespr *.pcx
makespr b*.pcx c*.pcx
makespr b*.pcx c*.pcx /r
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
void draw_string(char *s, int x, int y, int text_colour);