Part 12:
Advanced Graphics and GrayScale

 

    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.