Abe's Demoschool

part 3: sprites, linking C & assembler

Hello and welcome to part 3 of Abe's Demoschool. Last part was pretty messy so this time I'll show you how to split a demo up into several files and link them all together into one executable file. In this part we'll also cover sprite's and how to show a TGA picture. I think it's enough with the TGA format since most drawing programs can convert pictures into the TGA format. The TGA format is also one of the most simple formats.

WHAT IS A SPRITE?

A sprite is a rectangular 2-dimensional picture made up of pixels that can be drawn on different positions on the screen. Games often use sprites. In a pinball game the ball is a sprite and in DOOM the monsters are sprites. A car in a car-game viewed from above could consist of 4 sprites. One showing the car facing left, one up, one right and one down. All the computer has to do is to determine where the car is on the screen and which direction it is heading and then draw the right sprite at that position. To make the car turn smoother it should be made up of at least 8 sprites, facing 8 directions.

To make the sprite routines as fast as possible I used 32-bit instructions (movsd) that works with 4 bytes at a time. Because of that, the number of pixels in the x-direction has to be divisible by 4 (ie 4,8,16,20,32,64). The sprites can have any number of pixels in the y-direction. As usual we are in mode 13h (MCGA) which means there are 256 colors and that every pixel in a sprite is one byte in memory.

This is the way the sprites are stored in memory: First 2 bytes (one integer) telling the height of the sprite followed by 2 bytes telling the width of the sprite. After that comes all the pixels in the sprites from left to right, top to bottom. Every pixel is one byte. This is called a raw-format. Some raw formats have the width first but I have the height first because it fits the sprite routines better.

EX: A sprite with height 20 pixels and width 16 pixels (16 is divisble by 4)

20, 0, 16, 0    //first the height and width
                //then the raw data
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0         Row 1 16 bytes
0,0,0,0,0,34,56,6,6,56,34,0,0,0,0,0     Row 2 16 bytes

And So On for 18 more rows.

It is too much work to enter all the sprite-data by hand. You have to use some kind of drawing program to draw the sprites. With the drawing program save the sprite in TGA-format. Be shure to save it in 256-color mode TGA-format. Then convert it from 256-color-TGA-format to raw-sprite-format with tga2spr which comes with abedemo3.zip. I wrote the tga2spr program a long time ago and it isn't very nice but it works. If you want to make the code faster and clearer, please do, and mail me a copy of the result.

Here are the sprite routines that comes with the source code:

loadsprite( Name, Sprite);              //loads a sprite from disk
show32spr( x, y, Sprite, Destination);  //Shows a sprite (using fast 32-bit instructions)
                                        //at (x,y) on the Destination
showtrans( x, y, Sprite, Destination);  //As show32spr but with color 0 transparent
erase32bit( x, y, Width, Height, Destination); //erases a rectangular area on the Destination
get32spr(x, y, source, sprite, width, height); //gets a sprite from the source
                        //Source is either the visible screen or a virtual screen

Loadsprite loads a sprite to Sprite. Sprite is an array that has to be at least as big as the sprite it will contain.
Ex:

char Sprite[2 + MAXWIDTH*MAXHEIGHT];
loadsprite("mysprite.spr", Sprite);

show32spr copies the sprite to (x,y) at the destination with the help of the 32-bit instruction movsd
Ex:

screen=0x0a0000000;
show32spr(10,20, Sprite, screen);

screen=0x0a0000000 makes screen a pointer to the screen (A000:0000), this also gives a warning when you compile the program but who cares, it's works (on my compiler). Then Sprite is drawn with the top left corner at (10,20) to the screen.

Showtrans shows a sprite with color 0 transparent. Wherever the sprite has the color 0 the background will shine through the sprite. Showtrans is slower than show32spr, so if the background is black use show32spr instead.

Erase32bit draws a black rectangle with the upper left corner at (x,y) on the Destination with width Width and height Height :-). It is used to erase a sprite if the background is black. If the background is not black you have to save the background in a temporary backgroundsprite before you draw a sprite to the screen. To erase it you just draw the saved background over the sprite.

Get32spr is used to save the background as described above.

!CAUTION These routines all suppose that the screen is 320 bytes in width.
!CAUTION If you use a virtual screen, it has to be 320 bytes in width.

All the spriteroutines are in the file asmdemo3.asm and they contain comments.

HOW CAN I SHOW MORE THAN ONE SPRITE ON THE SCREEN WITH ONLY ONE PALETT?

This is a problem. If yo also have a picture in the background and several sprites you have to figure out a way for all the sprites to share the same palette.

The easiest way is to draw all the sprites and pictures width the same program and the same palette. But if you want to use a scanned picture or make sprites in ie 3d Studio then the sprites and pictures will use one palette each. Because of this problem I wrote a program that takes one palette and one tga-picture and then transformes the tga-picture to use the closest colors in the palette. I also wrote a program that takes one palette and one sprite with it's palette and transformes the sprite to use the colors in the first palette. To use the programs first make a palette that contains as many colors as possible of the colors in the sprites and pictures that will use the palette. Then transform the sprites and pictures with the programs mentioned above (and below). These programs are called fittga and fitspr and also comes with abedemo3.zip. They are even worse coded and really needs a facelift. If anyone does this, please send me a copy. I'm sure there must be shareware programs that can accomplish the same task much better, If you should stumble over one please mail the adress to me.

To use fittga and fitspr you have to manually change the filenames in the beginning of the main function. You could change this to make the program take the filenames as parameters to the program instead.

Look at the file tga.txt for more information on the TGA-format.

TO COMBINE C AND ASSEMBLER

The sprite routines are written in assembler but the main program is written in C. How does it work? It is really pretty easy. When you compile a C-program to an exe-file you actually do two things. First you compile the C-source to a .obj file, then you link the .obj file to a .exe file. You may not have noticed this if you use an integrated enviroment like Borland C++ 3.1, which automatically compiles, links and runs the program by a simple keypress (Ctrl F9).

To combine a file written in assembler with a file written in C: assemble the assembly-file, compile the c-file and link the .obj files to an .exe file.

A function written in assembler is called exactly like a function written in C from the C-program. Just tell the compiler that there are functions written outside of the C-file whit the command extern.

Ex, suppose the wtsync() function is written in assembler in another file. Then the C-program will call it like this:

#include       /* Don't include stdio.h if you don't have to */
                        /* it'll only increase the size of the .exe */
extern void wtsync(void);       /* tell the compiler that the wtsync */
                                /* code is somewhere else */
void main(void)
{
        .
        .
        .
        wtsync();               /* wtsync is called the same way as usual */
        .
        .
        .
}
And the assembler file will look like:
        
        P386N                   ;allow 386-instructions
        IDEAL                   ;use tasm's ideal mode
        MODEL small             ;(data < 64k) and (code < 64k)
        CODESEG

        PUBLIC  _wtsync         ;make wtsync public so that
                                ;it can be called from the c-program

PROC    _wtsync NEAR		;NEAR means that the PROCess is
				;NEAR callable, that is that the code
        .                       ;for the PROCess is in the same segment
        .			;as the caller's code. This is always
        .			;the case if you use model small
        
        ret                     ;don't forget the ret or else the
                                ;program will crash
ENDP    _wtsync

        END
Notice the _ before wtsync in the assemblerfile. You have to add an _ before the function name in the assembler file to make it callable from c. Don't write the _ in the c-file. Suppose wtsync is written in the file asmtest.asm and the c-file is ctest.c. There are a number of ways to build the .exe. I usually compile the assembler file/files with tasm and use Borland's command line compiler, bcc for the c-source:

With Borland C++ write:
        tasm /ml asmtest
        bcc -c ctest
        bcc -ms ctest.obj asmtest.obj

With Turbo C write:
        tasm /ml asmtest
        tcc -c ctest
        tcc -ms ctest.obj asmtest.obj
tasm /ml makes the .obj of the assemblerfile. /ml tells tasm to turn on case sensitivity, this is because C uses case sensitivity.

bcc -c makes the .obj of the cfile. -c means only compile, no linking.

bcc -ms links the objectfiles to an executable .exe-file. -ms tells bcc to use memory model small.

The result is a file called ctest.exe because ctest.obj was befor asmtest.obj at the time of the linking.

That's it. There are lots of advantages writing some of the functions in pure assembler. One advantage is that we can use 386 specific instructions like movsd. That isn't possible with inline assembler like we used in part 2. The C-file will also be a lot smaller, which makes it easier to read and understand. The disadvantage is the writing of the assembler-funktions, that can be hard if you're not used to it (I give you the assembler-functions for free to use and enhance). I have learned to master turbo assembler with the help of the book: mastering turbo assembler by Tom Swan.

IMPORTANT WHEN MIXING ASSEMBLY AND C:

* Sending arguments from C to assembler: use the ARG directive (looke at the source-code for examples). Here's the shell of a routine that takes arguments:

                ARG     xpos:word, . . .        ;put all arguments here
                push    bp              ;this has to do with the stack
                mov     bp,sp           ;don't bother too much about it right now
                .                       ;just put push bp and mov bp,sp first in
                .                       ;the routine and pop bp last
                .                       ;if you're using arguments
                .
                mov     ax,[xpos]       ;use the argument (YES IT IS THIS SIMPLE)
                .                       ;remember the braces []
                .
                .
                pop     bp
                ret

* Sending returnvalues back to the C-program:

Returnvalues are sent in ax and dx depending on the size

                SIZE            RETURNREGISTER  EXAMPLE ON DATATYPE
                Byte            al              char
                Word (2 byte)   ax              int
                Dword (4 byte)  dx:ax           char far*

* Always save ds, si and di on the stack if you change them in the routine

Or else the program will probably chrash because C uses ds,si and di. You are allowed to change AX, BX, CX, DX and ES without saving them. It doesn't matter for the C-program.

* Avoid local variables in the assembler routines. Use registers instead, they are much faster. If you have to use local directve LOCAL which works like the ARG directive.

You may have noticed that I used ideal mode in the wtsync example. Ideal mode only works with TASM. There are only small differencies in IDEAL mode. The differences makes IDEAL mode easier to read and understand. I think of the differences as improvements. Ideal mode is getting pretty big on the internet too, many of the newer assembly-sources you find on internet are in ideal mode.

EX: some differences in Ideal mode

	Normal			Ideal mode
	.386			p386n
	.MODEL small		MODEL small
	.DATA			DATASEG
	.CODE			CODESEG
	name PROC NEAR		PROC name NEAR
	name ENDP		ENDP name

A GREAT advantage with Ideal mode is the ARG directive which makes parameters to assembly routines as easy to handle as parameters to a C-function. Like I mentioned there is also the LOCAL directive for local variables.

THE SAMPLEPROGRAM DEMO3.C:

All functions in demo3 are written in the assembler file asmdemo3.asm The C-source is in demo3.c. Demo3.c is pretty much explains itself but there are a couple of functions in the assembly-file that needs more explanation. Showpic reads a picture, stored as raw-data, to the screen or a virtual screen. If you're using a virtual screen it must be 320 pixels wide. The program uses a virtual screen the same size as the visible screen 320*200 = 64000 bytes. First, memory is allocated for the virtual screen using a malloc function which is declared in the asmdemo3.asm. Then the picture is loaded into the virtual screen and a palette is loaded and set. Then the virtual screen is flipped over to the visible screen using a digital dissolve effekt which I will talk more about later.

After the dissolve effekt comes the main loop where a transparent sprite bounces across the screen. All the drawing and erasing is made on the virtual screen, when one frame is done the virtual screen is flipped over to the visible screen and the next frame is calculated . . . The main loop follows these steps:

        1.      Save the background where the sprite will be drawn
        2.      Draw the sprite
        3.      Flip virt over to the screen
        4.      Erase by drawing the saved background over the sprite on the virtual screen
        5.      calculate new coordinates for the sprite, GoTo 1

Before the program is finished the allocated memory must be released using the free function which also is declared in asmdemo3.asm.

The Dissolve effekt uses an interesting algoithm which gives all numbers between 1 - FFFFh in random order, no number comes up more than once before all numbers have come up !!!

PSEUDOCODE:

        
        set the offset to any number but zero (ie 1)
        for(i=0;i<0x0FFFF;i++)
        {
                Do something with the offset    (like flip one point to the)
                .                               (screen using the offset)
                .
                .
                Shift the offset right 1 bit    (like dividing by 2)
                if the shifted bit was 1        (if carry is set)
                then offset = offset xor 0x0b400
        }

This way offset will take on all values between 1 - FFFFh. Don't ask me how, but it really works. I got the idea from Graphic Gems I side 221.

See the file targa.txt for a description of the TGA-format.

The next part will be about 2d and 3d vectorgraphics. I'm currently optimizing the routine that draws a filled triangle to the screen, hopefully it'll get fast. You lucky weezers will get the routines served on a silverplate, but hey, it's the 90's.

DISCUSSION:

Most computers today have hardwaresupport for drawing sprites (Already the Commodore 64 had it). It's usually called something like bitblt and that means Bit Blast. On the PC, most graphic cards also do have bitblt, but there's no standard so you can't use the BitBlt. Because it'll only work on your own computer and the computers that have similar graphiccard to your one. But if you want to try, there's information about various cards in the book "Programming the Ega, Vga and SuperVga Cards" by Richard Ferraro.

Download abedemo3.zip


Sprit'em
Says: Sir Abe
elecronic mail:	dat94avi@bilbo.mdh.se
snail mail:	Albert Veli
		Spisringsg. 9
		724 76 V�ster�s

Back to the Demoschool.