GrayScale, the effect of intermediate shades
of black and white, is achieved by flipping two or more pages of video
memory very quickly to the screen so that (in most cases) the flipping
is not noticable and appears as gray. If a pixel on one video memory
(let's call it bitplane 1) is set and the same pixel on another video memory
(bitplane 2) is reset, then when the two pixels are flipped against each
other, the screen pixel will appear gray. In this way, corresponding
pixels set on both bitplanes will turn the screen pixel black.
Changing the screen location from one video memory
to another is done simply by putting a value between $00 and $3c out port
0 that represents most significant byte of the ASIC address pointer (the
least significant byte is 0) plus $c000. $0a = $ca00; $3c = $fc00.
Now, the only problem is where we can find enough open RAM to afford our
two video memories. The first area is obvious: the standard video
memory from $fc00 to $ffff. The second (must be any even address
between $c000 and $fc00) will have to be placed within one of the only
two RAM locations spacious enough to hold a 1024 byte video memory: the
asm space and the graph memory (called _plotSScreen, at $c9f9). The
graph memory is by far the more commonly used, but to use it you actually
have to use the RAM from $ca00 to $cdff; you can mainipulate this memory
the same way you would the standard video memory.
To give the GrayScale an even flow and to not have
it interfere with code, the interrupt is used. To achieve three levels
of gray (white, gray, black), you would program the interrupt handler to
toggle betweeen video memories every interrupt; to achieve four level GrayScale
(white, light gray, dark gray, black), you would program the interrupt
handler to switch to the first bitplane on the first interrupt, switch
to the second bitplane on the third interrupt, back to the first on the
fourth, to the second on the sixth, etc., so that bitplane 2 is on the
screen twice as long as bitplane 1. The pixel color in this sort
of scheme would be determined as follows: white = reset on both bitplanes;
light gray = set only on bitplane1; dark gray = set only on bitplane 2;
black = set on both bitplanes.
The following is an interrupt handler I wrote for
4 level GrayScale using im 2 that is both fast (finishes quickly so that
you can return to your suspended code) and flickerless. There are
four main calls included as this routine: OpenGray - install interrupt
handler and enable interrupts; CloseGray - clear both video memories and
return to im 1; ClrMem - clear both memories; ClrGrf - clear just graph
memory. This also increments (UserCounter) once every interrupt.
OpenGray:
ld hl,int_start
;copy interrupt to where it'll exec, $8f8f
ld de,$8f8f
ld bc,int_end-int_start
ldir
di
;disable interrupts so that we can set up shadow registers
;we're going to keep all data in the shadow registers; when we end the interrupt, it will be like ;jumping back up to the beginning again because at the start of each interrupt, the shadow ;registers will contain the values they had when the interrupt ended
exx
;get shadow registers
ld b,$3c
;B will be the byte we put out the port to toggle video mems
ld c,0
;C will be the port we put it out (port 0)
ld d,3
;D will count the number of interrupts
ld e,%110110
;E will be the toggle byte used to XOR between $3c and $0a
ld hl,UserCounter
;(HL) will be the user counter
exx
;store them away
ld hl,$8e00
;make 256 byte vector table at $8e00
ld (hl),$8F
ld bc,256
ld d,h
ld e,l
inc e
ldir
ld a,$8E
ld i,a
;load interrupt vector register with vector table
im 2
;set interrupt mode
ei
;start the interrupt
ret
;call is over
CloseGray:
im 1
;reset interrupt mode for OS
ld a,$3C
;make sure video mem is active
out (0),a
clrMem:
;clear video mem and graph mem
call _clrLCD
clrGrf:
;clear graph mem only
ld hl,_plotSScreen+6 ;$ca00
ld d,h
ld e,l
inc de
ld (hl), 0
ld bc,1024
ldir
;clear it
ret
int_start:
;the actual interrupt handler
ex af,af'
exx
;swap suspended registers for our interrupt registers
in a,(3)
;if bit 1 in port 3 is set
bit 1,a
;the LCD is updating so leave
jr z,leave_int
;this won't happen unless the batteries are very low
inc (hl)
;increment user counter
out (c),b
;put video mem in B out port 0
dec d
;decrement interrupt counter
call z,reset_int_counter
;when it becomes 0, reset it back to 3 and
ld a,d
; flip to dark gray bitplane
cp 1
;if D is 1
call z,change_pages
;flip to light gray bitplane
leave_int:
in a,(3)
;the following must be done or the calc will crash
rra
;I don't know what it does exactly (it's from the ROM)
ld a,c
adc a,9
out (3),a
ld a,$0B
out (3),a
ex af,af'
;get back suspended code registers
exx
ei
reti
;return from interrupt
int_end:
reset_int_counter:
;reset int counter d when it is 0
ld d,3
change_pages:
;toggle between memories in B with XOR toggle byte in E
ld a,e
xor b
ld b,a
ret
;use this when you need a counter (defined to be one byte before the interrupt on page 1)
UserCounter equ $8f8e
Once you have the GrayScale interrupt handler running, you have two video memories to write to. ROM calls, such as _puts, will only write to the second bitplane layer. If you want to use GrayScale sprites, you will have to put one sprite to video memory and a second to put a sprite to $ca00. You can modify a PutSprite routine to accomplish this or you can get one that's already been modified from the routines section of ticalc.org.
Let's switch back to single layer video memory and
sprites. There are two main methods of setting each byte of a sprite
to video memory: you can OR it, which will set each pixel in the sprite
regardless of whatever is currently in video memory; and you can XOR it,
which will reset the pixel if it's set in both the sprite and video memory.
The advantage to XOR is that by putting the sprite a second time to the
same video memory location will yield the original background. Very
useful when moving sprites, but with one drawback: if there's a background,
it will corrupt the sprite image on screen. So, you can use XOR to
put a sprite to the screen that can be erased easily but will be affected
by the background, or you use OR to put a true sprite to the screen but
that can't be erased without destroying the background. Either way,
you don't really win unless you have no background and use XOR or unless
you completely refresh the LCD and use OR.
Using OR will of course only put pixels to the screen,
leaving all other pixels around it transparent. If you want some
pixels to be incontrovertably cleared, you must use what's called a masked
sprite. A masked sprite consists of two elements: the mask, which
is the size of the sprite and indicates whether or not the corresponding
bit in the sprite should be transparent or solid white, and the sprite.
The following is an example of the use of an 8x8
sprite clipping routine (a clipped sprite is one that does not show
up on the other side of the screen when it moves off the screen) written
by Jimmy Mardell using masked sprites, called ASCR. This routine
also saves the background behind the sprite to an 8x8 data area so that
it can be put back to screen as the sprite moves, solving the problem of
ORing the sprite and not being able to recover the background. The
routine is too long for me to copy here, but you can find it in the routines
section of ticalc.org or with the sqrxz source code. The input to
the PutSprite_MSB routine (ASCR) is x,y in BC; background storage pointer
in DE; sprite followed by bitmask pointer in HL.
start:
ld hl,bitmap
;load a background labeled bitmap
ld de,$fc00
ld bc,1024
ldir
sub a
;clear A
ld (walrusX),a
;start sprite at top right of LCD
ld (walrusY),a
mainLoop:
call _pause
;wait for a keypress
call refresh
;erase all current mobile sprites
call keyHandler
;handle keypresses
call updateSprites
;put all mobile sprites back to the screen
jr mainLoop
refresh:
ld hl,walrusX
;walrus is our only sprite in this example, erase it
ld b,(hl)
;ld B with the walrus's X coordinate
inc hl
ld c,(hl)
;ld C with the walrus's Y coordinate
ld hl,walrusSpace
;erases by copying stored background
jp PutSprite
;call a PutSprite that use ld rather than OR or XOR
keyHandler:
call _getky
;same as GET_KEY in asm86.h; gets a keycode without pausing
cp kUp
jr z,move_up
cp kDown
jr z,move_down
cp kRight
jr z,move_right
cp kLeft
jr z,move_left
cp kExit
jr z,exit
ret
move_left:
;if it moves off the screen, no problem ... it's clipped!
ld hl,walrusX
dec (hl)
ret
move_right:
ld hl,walrusX
inc (hl)
ret
exit:
pop hl
;trash program counter on top of stack
ret
;return to homescreen or shell
updateSprites:
ld hl,walrusX
;update walrus w/ new coordinates
ld b,(hl)
;ld B with the walrus's X coordinate
inc hl
ld c,(hl)
;ld C with the walrus's Y coordinate
ld hl,walrus
;point HL to walrus sprite
ld de,walrusSpace
;point DE to storage background for walrus sprite
jp PutSprite_MSB
;put the sprite (in ASCR) <call x / ret>
walrusX:
;define areas of RAM to store important data
.db 0
;these are the x and y coordinates of the walrus sprite
walrusY:
.db 0
;a masked sprite
walrus:
.db %00000000
.db %00001100
.db %00111110
.db %01111011
.db %11111100
.db %11111000
.db %01111010
.db %11111110
;mask
.db %00000000
.db %00001100
.db %00111110
.db %01111111
.db %11111110
.db %11111100
.db %01111010
.db %11111110
walrusSpace:
;8 bytes where the background can be stored behind the walrus
.dw 0,0,0,0
;.dw = define word
When writing a large game like what this simple program
might turn out being, design is integral, especially inside the main loop.
Design your game structure on paper before you program anything, taking
into account everything that might need to occur within the main facet
of execution. Not only will your code be cleaner and faster, but
your debugging time and number of hacks will be greatly reduced.
Don't think that the "coolest" programmers wouldn't waste their time with
paper and pencil because that is precisely the way the best programmers
are able produce their games so quickly and efficiently. So, do yourself
a favor :-)
There are three general steps you must take when
creating a mobile sprite such as the walrus in this example. The
first step, you need to put the sprite to video memory using a some kind
of sprite routine (obviously). Second, you need to remove the sprite
the next time that your main loop requires the sprite to be moved -- this
can be accomplished by XORing out the sprite, replacing the sprite with
a stored background, or by completely refreshing the entire screen.
Third, update the coordinates so that the moving sprite can be put to it's
new location. The best order within a main loop is: refresh, update
coordinates, update sprites; but keep in mind that you can't XOR out a
sprite or replace a stored background unless you've already put the sprite
at least once. These steps may seem pretty obvious, but you'd be
suprised how easy it is to start out implementing a bad design.
Just as important to game design are well structured
variables. In this example I didn't need to store the walrus coordinates
to (wlarusX) and (walrusY), but as the game gets larger I won't be able
to simply use the registers; do this from the beginning of the process
or else you will most likely get into a mess trying to push and pop registers
all over the place. In general, you should always store sprite coordinates
in some RAM location. Also note that I could have shaved a few bytes
off program size by defining the RAM locations on page 1 (remember that
page 1 is 16k of empty space swapped into $8000 - $bfff at the start of
each asm program) for walrusSpace and walrus coordinates using equates.
Sprite animation is not difficult either, but if you tried implementing it, you probably screwed up. First, if your animated sprite is also a mobile sprite you will have to use a call that finds the current sprite frame rather than just pointing to the sprite. This call will get the pointer to the sprite frame by looking up its sprite in an animation table (a database of the current sprite frames for each sprite currently on the screen). In the main loop of your program, call an animation controller to update the frames of each sprite; to give this an even duration, you may wish to make use of the interrupt by, for instance, setting a flag in the interrupt that's waited for by the animation controller or by directly writing the animation controller into your interrupt handler. How you store the animation when it's not on the screen is entirely up to you, so long as the animation controller knows how to read it. If you need an example, look in the Sqrxz source code or maybe I'll post one to ticalc.org.
Programming beyond this -- ray-casting, side scrolling, collision detection -- even though it involves the use of graphics, is no longer a simple matter of using graphics per se, but more a matter of employing algorithms specific to the situation. If you want information on how to implement these kinds of designs, you shouldn't have a problem find a web page that goes over the principles involved.