Issue 2 - April 2000


Writing your own GET & PUT routines


 

Writer: Matthew River Knight

You will recall that last month, under the Tips, Hacks, and Tricks section of the magazine, we discussed how to do page flipping in screen mode 13h. We wrote a fast PCOPY routine to do this for us, and we briefly discussed how to PSET/POINT to/from these offscreen pages. Unfortunately however, it is not possible to really do much with those short routines.

In any 2D game, the most important routines are probably GET and PUT. Without these two vital routines, it would be very difficult, if not impossible to make a 2D game, running at an acceptable speed. Consequently, I have decided to write a nice long tutorial, explaining step-by-step, exactly how to write your own ASM GET/PUT routines for use with offscreen 13h pages!

The GET/PUT format:

We are going to be using SCREEN 13 for these routines, since it is the most popular screen mode, and also has the nice property of being very easy to use. The whole screen is built up by a 320*200 pixel bitmap with 256 possible colors. Each pixel occupies one byte in the VGA memory, and that memory starts at the address A000:0000h. The first byte contains the index(color) of the pixel at coordinates 0,0, the second bye contains the index of the pixel at 0,1, the 320th byte contains the index of the pixel at 1,0, and so forth.

When you GET a sprite, it is stored in a Qbasic array. The first word in that array holds the width of the sprite multiplied by 8. The second word holds the height of the sprite. Fortunately this value is the actual value, unlike the width, which is multiplied by 8. After all this, the pixel indexes come as bytes stored in the same way as the SCREEN 13 bitmap.

Writing a PUT routine:

We are going to start off by writing a PUT routine. It would be nice if we could write a routine a little better than the standard Qbasic PUT. Obviously it will be much faster than PUT, and it will also be able to PUT in offscreen pages, but let's add another feature. I'll bet that if you have ever made a game in Qbasic, you have been bothered by the fact that whenever you use PUT, a big black box surrounds the sprite, completely obliterating the background. Oh boy, oh boy, yes indeed...well, this PUT routine that we are soon going to write is going to have an invisible color. This means that one of the colors in the sprite will not be drawn on the screen. With this feature, your sprites can have irregular edges and holes in them because a certain color will be skipped when the sprite is drawn on the screen. We'll make color 0 the invisible color.

To start off with, we need to figure out what variables we need to pass to the routine. First of all, we need to pass the x and y coordinates of the screen where we want the sprite to be drawn. The sprite should be stored in an array, and all arrays start with an offset address of 0 in memory, so we just need to pass the segment address. Since this routine must also be capable of PUTing to offscreen pages, it is necessary that we pass the segment and offset address of where we want to PUT the sprite. For example, if we just wanted to PUT the sprite straight onto the screen, then we would pass &HA000 as the segment and 0 as the offset. If we wanted to PUT onto an offscreen page, then we would have to pass the segment and offset address of the array being used as the offscreen page.

The routine will also need to know the dimensions of the sprite, however, that's stored in the sprite data so we can obtain those values inside the assembly routine itself.

So, let's make the CALL to the routine like this:

CALL ABSOLUTE(BYVAL DSeg%, BYVAL DOff%, BYVAL VARSEG(Sprite%(0)), BYVAL X%, BYVAL Y%, SADD(asmput$))

where:

DSeg% is the segment address of the screen page.
DOff% is the offset address of the screen page.
Sprite% is the array of the sprite to be drawn.
X% is the position on the x-axis where the sprite is to be drawn.
Y% is the position on the y-axis where the sprite is to be drawn.
asmput$ is the string containing the machine PUT code.

Okay, now we can write our PUT routine! I decided to make this routine with Absolute Assembly, by Petter Holmberg, since this is the method of proggin in ASM that is used by most ASM newbies in the Qmunity. If you don't have Absolute Assembly, don't worry, I have included it with this issue of the mag for ya!

PUSH DS ;Allow passing of Qbasic variables.
PUSH BP
MOV BP, SP

MOV BX, [BP+0C];Get the segment address of the sprite array.
MOV DS, BX
XOR SI, SI ;Set SI to 0.

MOV BX, [BP+10] ;ES = screen buffer segment.
MOV ES, BX
MOV DX, [BP+0A] ;DX = x coordinate.
MOV AX, [BP+08] ;AX = y coordinate.
MOV BX, AX ;BX = AX = y coordinate.
MOV CL, 8 ;Multiply y with 256.
SHL AX, CL
MOV CL, 6 ;Multiply y with 64.
SHL BX, CL
ADD AX, BX ;Add the results together.
ADD AX, DX ;AX(320*y) = AX + DX(x coordinate).

MOV BX, [BP+0E] ;BX = screen buffer offset.
ADD AX, BX ;AX(320*y +x) = AX + BX.
MOV DI, AX ;Set DI to AX.

LODSW ;Get width from sprite data.
MOV CL, 3 ;Divide the width by 8 to get actual width.
SHR AX, CL
MOV BX, AX ;Store the width in BX.
LODSW ;Store the height in AH.
MOV AH, AL

MOV DX, 140 ;DX = 320 - sprite width.
SUB DX, BX

Yloop: CMP AH, 0 ;Is height zero?
JE EndProg ;Yes --> goto EndProg.
MOV CX, BX ;No --> make CX equal to sprite width.
XLoop: LODSB ;Load a pixel from DS:SI into AL.
CMP AL, 0 ;Is the pixel color 0?
JE SkipPixel ;Yes --> dont draw this pixel.
STOSB ;No --> copy the pixel in AL to ES:DI.
DEC DI ;Compensate for pixel skipping code.
SkipPixel: INC DI ;Skip a pixel.
LOOP XLoop ;Next loop of XLoop.
ADD DI, DX ;Move to the next line of the sprite.
DEC AH ;Decrement height.
JMP YLoop ;Next loop of YLoop.

EndProg: POP BP ;Return to Qbasic.
POP DS
RETF A

Now that we've written that code up, we need to run it through Absolute Assembly, so the machine code can be generated. Lucky for you, however, I have already done this for you! And, I have made a working example bit of code to use the routine! Check it out! It's PUT.BAS, included with this issue of QBCM! ^_^

Writing a GET routine:

Now that we have done a PUT routine, you are going to find it very easy to write a GET routine, since it works on very much the same principle, but in the reverse! You probably already have a very good idea how to make this routine yourself, but just in case you don't, we'll guide you through the coding of it right here, step-by-step!

As with the PUT routine, we first must decide what variables we are going to have to pass to the routine. Firstly, we are going to need to pass the segment and offset address of the screen page. We then need to pass the segment address of the sprite array. In Qbasic, when you use GET, you always need to enter the variables/values x1%, y1%, x2%, y2%, and these variables are also going to be passed with our routine!

So, let's make the CALL to the routine like this:

CALL ABSOLUTE(BYVAL dbuffseg%, BYVAL dbuffoff%, BYVAL VARSEG(image%(0)), BYVAL x1%, BYVAL y1%, BYVAL x2%, BYVAL y2%, SADD(asm$))

where:

duffseg% is the segment address of the screen page.
dbuffoff% is the offset address of the screen page.
image% is the sprite array we are going to GET the data into.
x1% is the x coordinate of the top left corner of the sprite.
y1% is the y coordinate of the top left corner of the sprite.
x2% is the x coordinate of the bottom right corner of the sprite.
y2% is the y coordinate of the bottom right corner of the sprite.
asm$ is the string containing the machine GET code.

Okay, now we can write our GET routine! Once again, this routine has been written to use Absolute Assembly.

PUSH DS ;Allow passing of Qbasic variables.
PUSH BP
MOV BP, SP

MOV BX, [BP+10];Get the segment address of the sprite array.
MOV ES, BX
XOR DI, DI ;Set DI to 0.

MOV BX, [BP+14] ;DS = screen buffer segment.
MOV DS, BX
MOV DX, [BP+0E] ;DX = x1 coordinate.
MOV AX, [BP+0C] ;AX = y1 coordinate.
MOV BX, AX ;BX = AX = y1 coordinate.
MOV CL, 8 ;Multiply y1 with 256.
SHL AX, CL
MOV CL, 6 ;Multiply y1 with 64.
SHL BX, CL
ADD AX, BX ;Add the results together.
ADD AX, DX ;AX(320*y1) = AX + DX(x1 coordinate)
MOV BX, [BP+12] ;BX = screen buffer offset.
ADD AX, BX ;Add BX(screen buffer offset) to AX(320*y1+x1)
MOV SI, AX ;Set SI to AX.

MOV BX, [BP+0A] ;Get the width of the sprite - (x2-x1)+1
SUB BX, [BP+0E]
INC BX
MOV AX, BX ;Copy BX(sprite width) into AX.
MOV CL, 3 ;Multiply width by 8.
SHL AX, CL
STOSW ;Copy (width * 8) to the sprite array.

MOV AX, [BP+08] ;Get the height of the sprite - (y2-y1)+1
SUB AX, [BP+0C]
INC AX
STOSW ;Copy height to the sprite array.

MOV DX, 140 ;DX = 320 - width.
SUB DX, BX

yloop: CMP AX, 0 ;Is height zero?
JE Endprog ;Yes --> goto Endprog.
MOV CX, BX ;No --> number of times to do xloop = BX
xloop: MOVSB ;Copy byte at DS:SI to ES:DI.
LOOP xloop ;Next loop of xloop.
ADD SI, DX ;Point at next line of the sprites height.
DEC AX ;Decrement height.
JMP yloop ;Next loop of yloop.

EndProg: POP BP ;Return to Qbasic.
POP DS
RETF E

Well there ya go! The code is done! All you have to do now is run the code through Absolute Assembly, and then you can start using this stuff! Well, actually, you won't need to do that because I have already done it for you! Check out GET.BAS, which is included with this issue of the mag!

That's it for this tut. With this new knowledge, in addition to what you learned last issue about writing a PCOPY routine, you can make a pretty cool game, certainly much better than you would have done without this! ^_^