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! ^_^