Tao Te KaChing
Workin' the cash register of the Great Tao

Programming the Atari 2600, and Me - Part 6

I think it's safe to say that you, I, and everyone else on the planet is now sick of talking about cycle counting.  We've already looked a little into coloring the background by setting the COLUBK address before a scanline is drawn.  But what about those pesky sprites?

Right off the bat, let's get this out: there are no drawing routines.  Zip.  Nada.  None.  Let me qualify this.  We now live in the blissful days of GDI, DirectX, OpenGL, even Flash for f--k's (folk's?) sake!  We take for granted our DrawLine and FillEllipse methods, optionally hardware-driven matrix transforms, and 232 colors with transparency.  The Atari VCS laughs at us and calls us 'p--sies' (as in cats or willows) directly to our faces.  And it kind of has a leg to stand on...

We have seven "objects" we can work with: two playfield objects, two player objects, two missle objects, and a ball object.  Drawing these objects is always in essence the same two or three tasks:
    - per scanline, is (a line of) the object residing on this scanline?
    - if so, enable the bit(s) for that object that will get color for that scanline
    - optionally, set the color for the object, so that that color will be used for that scanline
Therefore, we still need to know what scanline we're on to know to draw a sprite onto it.  With the possibility of a frame with seven objects utilizing a particular scanline, and a scanline only having 76 machine cycles to possibly do those three tasks for all seven objects, do we begin to see the pure art of assembly sculpting required with cycle counting to fit everything in?  Oh...you will.

Now some of these objects are “easier” to render than others.  The missles and ball objects, for instance, are really just enabling a bit on a particular scanline.  Done.  Player objects get more complex.  Let's look at a smiley-face player object:

happy8(1)x8-values

So, I'd be thrown out of the programmer's union if I didn't show a grid with bit values.  Here's our smiley face.  All player objects are 8 bits (a byte!) wide.  The right side shows the byte value for each row of smiley's face.  While you are setting up your current scanline within the 192 (for NTSC) display scanlines, you would need to check and see if smiley is on that scanline, and if so, set the byte for the respective row in smiley's face you're on.  For example, let's say smiley is player 0 and his top row starts on the 8th scanline in our display.  Then, when we get to the 8th scanline, we would set the player 0 graphics address (GRP0) to 60.  On the next scanline, we would set GRP0 to 126, then 219 on the next, and so on until the 16th scanline, where smiley is no longer being drawn, where we would need to finish with setting GRP0 to 0.  The TIA is simple and just draws on each scanline for player 0 whatever bits are on in GRP0, for whatever color is stored at COLUP0.  This automatically shows one constraint:

happy8(1)x8.badmulticolor

is impossible to draw (unless you use player 1 for the eyes, I guess), since you can only set the color for the enabled bits, and not per-bit.

happy8(1)x8.okmulticolor

This is fine, although, as the picture notes, we'll have the extra expense of setting the color for the player for each scanline.

We can also change smiley's size by setting the NUSIZ0 address:

3 sizes

Single, double, and quad-width rastering.  Basically NUSIZ0 tells the TIA chip how many clocks to "use up” per “pixel”.  The Stella guide (pg.40) is not extremely clear on this, as it leaves it ambiguous whether or not bits D4 and D5 are just for missles or also somehow affect players.  We can test this with the following code:

                processor 6502
                include "vcs.h"
                include "macro.h"

                seg   org $F000

VAR_TEMP            = $80
VAR_PLYR0_Y            = $81
VAR_PLYR0_HEIGHT        = $82
VAR_NUSIZ0            = $83
VAR_NUSIZ0_CNTR            = $84


Initialize
                ; === set background color
               LDA #0
               STA COLUBK
               STA VAR_NUSIZ0
               STA VAR_NUSIZ0_CNTR
                
                ; === set player Y position
               LDA #107
               STA VAR_PLYR0_Y
               LDA #8
               STA VAR_PLYR0_HEIGHT
                
                ; === set player "ball" color
               LDA #$1F
               STA COLUP0
                
                ; === set player ball location
                
; ----------------------------------------------------------------------------------- MAIN LOOP ( "kernel" )
Kernel
                ; ---------------------------- perform VSYNC
               LDA #2
               STA VSYNC
                
                
               STA WSYNC
               STA WSYNC
                
                ; ---------------------------- clear our player graphics here
               LDA #0
               STA GRP0
               STA WSYNC
                
               STA VSYNC
                
                
                ; ---------------------------- perform VBLANK
               LDA #255
               AND VBLANK
               ORA #2
               STA VBLANK
                
                
                ; flip through bits D0, D1, D2, D4, and D5 for NUSIZ0
               INC VAR_NUSIZ0_CNTR
               LDA #60
               CMP VAR_NUSIZ0_CNTR
               BNE __nxt3
                
               LDA #0
               STA VAR_NUSIZ0_CNTR
               INC VAR_NUSIZ0
               LDA #8
               CMP VAR_NUSIZ0
               BNE __nxt
                
               LDA #16
               STA VAR_NUSIZ0
               JMP __nxt3
__nxt
               LDA #24
               CMP VAR_NUSIZ0
               BNE __nxt2
                
               LDA #32
               STA VAR_NUSIZ0
               JMP __nxt3
__nxt2
               LDA #40
               CMP VAR_NUSIZ0
               BNE __nxt3
                
               LDA #0
               STA VAR_NUSIZ0
__nxt3
               LDX VAR_NUSIZ0
               STX NUSIZ0
                
               STA WSYNC
                
                repeat 35
               STA WSYNC
                repend  LDX #0
               LDY #0
               LDA #253   
               AND VBLANK    
                
               STA WSYNC
               STA VBLANK    

                ; ---------------------------- 192 picture lines
PICTURE_LINES
               CPX #192
               BEQ OVERSCAN 
DRAW_PLYR_0
               CPX VAR_PLYR0_Y 
               BNE DRAW_PLYR_0_N 
DRAW_PLYR_0_Y
               LDY #8
               JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_N
               NOP
               JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_DO
               CPY #0
               BEQ DRAW_PLYR_0_DO_N 
DRAW_PLYR_0_DO_Y
               DEY
               LDA GFXOBJ_Ball,Y
               JMP DRAW_PLYR_0_FINAL 
DRAW_PLYR_0_DO_N
               LDA #0
DRAW_PLYR_0_FINAL
               STA GRP0
                
               STA WSYNC
               INX
               JMP PICTURE_LINES
OVERSCAN
                ; ---------------------------- 30 overscan lines
                repeat 30
               STA WSYNC
                repend  JMP Kernel
; ----------------------------------------------------------------------------------- MAIN LOOP END

GFXOBJ_Ball
    dc.b    #60, #126, #195, #189, #255, #219, #126, #60
    
                org $FFFA

                .word Initialize          ; NMI
                .word Initialize          ; RESET
                .word Initialize          ; IRQ

                end 

Here's some adjustments if you want Rainbow Smiley:

                processor 6502
                include "vcs.h"
                include "macro.h"

                seg   org $F000

VAR_TEMP            = $80
VAR_PLYR0_Y            = $81
VAR_PLYR0_HEIGHT        = $82
VAR_NUSIZ0            = $83
VAR_NUSIZ0_CNTR            = $84


Initialize
                ; === set background color
               LDA #0
               STA COLUBK
               STA VAR_NUSIZ0
               STA VAR_NUSIZ0_CNTR
                
                ; === set player Y position
               LDA #107
               STA VAR_PLYR0_Y
               LDA #8
               STA VAR_PLYR0_HEIGHT
                
                ; === set player "ball" color
               LDA #$1F
               STA COLUP0
                
                ; === set player ball location
                
; ----------------------------------------------------------------------------------- MAIN LOOP ( "kernel" )
Kernel
                ; ---------------------------- perform VSYNC
               LDA #2
               STA VSYNC
                
                
               STA WSYNC
               STA WSYNC
                
                ; ---------------------------- clear our player graphics here
               LDA #0
               STA GRP0
               STA WSYNC
                
               STA VSYNC
                
                
                ; ---------------------------- perform VBLANK
               LDA #255
               AND VBLANK
               ORA #2
               STA VBLANK
                
                
                ; flip through bits D0, D1, D2, D4, and D5 for NUSIZ0
               INC VAR_NUSIZ0_CNTR
               LDA #60
               CMP VAR_NUSIZ0_CNTR
               BNE __nxt3
                
               LDA #0
               STA VAR_NUSIZ0_CNTR
               INC VAR_NUSIZ0
               LDA #8
               CMP VAR_NUSIZ0
               BNE __nxt
                
               LDA #16
               STA VAR_NUSIZ0
               JMP __nxt3
__nxt
               LDA #24
               CMP VAR_NUSIZ0
               BNE __nxt2
                
               LDA #32
               STA VAR_NUSIZ0
               JMP __nxt3
__nxt2
               LDA #40
               CMP VAR_NUSIZ0
               BNE __nxt3
                
               LDA #0
               STA VAR_NUSIZ0
__nxt3
               LDX VAR_NUSIZ0
               STX NUSIZ0
                
               STA WSYNC
                
                repeat 35
               STA WSYNC
                repend  LDX #0
               LDY #0
               LDA #253   
               AND VBLANK    
                
               STA WSYNC
               STA VBLANK    

                ; ---------------------------- 192 picture lines
PICTURE_LINES
               CPX #192
               BEQ OVERSCAN 
DRAW_PLYR_0
               CPX VAR_PLYR0_Y 
               BNE DRAW_PLYR_0_N 
DRAW_PLYR_0_Y
               LDY #8
               JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_N
               NOP
               JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_DO
               CPY #0
               BEQ DRAW_PLYR_0_DO_N 
DRAW_PLYR_0_DO_Y
               DEY 
               LDA COLOR_Ball,Y
               STA COLUP0
               LDA GFXOBJ_Ball,Y
               JMP DRAW_PLYR_0_FINAL 
DRAW_PLYR_0_DO_N
               LDA #0
DRAW_PLYR_0_FINAL
               STA GRP0
                
               STA WSYNC
               INX
               JMP PICTURE_LINES
OVERSCAN
                ; ---------------------------- 30 overscan lines
                repeat 30
               STA WSYNC
                repend  JMP Kernel
; ----------------------------------------------------------------------------------- MAIN LOOP END

GFXOBJ_Ball
    dc.b    #60, #126, #195, #189, #255, #219, #126, #60
COLOR_Ball
    dc.b    #159, #130, #119, #92, #74, #68, #253, #31
    
                org $FFFA

                .word Initialize          ; NMI
                .word Initialize          ; RESET
                .word Initialize          ; IRQ

                end 

Either way, we see that bits D4 and D5 do not affect player objects, just D0, D1, and D2.  Also, NUSIZ0 doesn't just affect player width; it can give us clones, too, albeit just single-width.  Activision's Laser Blast quickly comes to mind (I actually defeated that game!  That and Yars Revenge, I think…).  See the grey vehicle-things at the bottom?  (picture shamelessly purloined from AtariAge)

s_LaserBlast_2

On another cycle-counting side note, I'd like to note that the code that cycles through D0, D1, D2, D4, and D5 was written on-the-fly in like 5 minutes.  This is not to try and sound all boastful n' shite.  Rather, I'm going to bring up how damn helpful Kowalski's 6502 simulator is.  I threw the code into the simulator:

sim_0

and debugging it with values that would pass through the most costly “path” of instructions, I saw that only 56 cycles were taken up:

sim_1

This was incredibly useful, because then I knew I didn't need to trim it down or anything; I just needed to follow it with a WSYNC call to easily stay in sync.  If it had been over 76 cycles, I would have had to break it in two to make sure one part operated within one scanline, and the second in the following.  Below is the code.  If you're using the Kowalski simulator, try it out in there with different values for VAR_NUSIZ0 and VAR_NUSIZ0_CNTR.

                *= $1000
NUSIZ0        = $80
VAR_NUSIZ0    = $81
VAR_NUSIZ0_CNTR    = $82
               LDA #39
               STA VAR_NUSIZ0
               LDA #59
               STA VAR_NUSIZ0_CNTR
                
                ; reset cycle counter here
                
               INC VAR_NUSIZ0_CNTR
               LDA #60       
               CMP VAR_NUSIZ0_CNTR
               BNE __skip
                    
               LDA #0       
               STA VAR_NUSIZ0_CNTR
               INC VAR_NUSIZ0    
               LDA #8       
               CMP VAR_NUSIZ0    
               BNE __nxt    
                
               LDA #16       
               STA VAR_NUSIZ0    
               JMP __skip    
__nxt
               LDA #24       
               CMP VAR_NUSIZ0    
               BNE __nxt2
                    
               LDA #32       
               STA VAR_NUSIZ0    
               JMP __skip
__nxt2
               LDA #40
               CMP VAR_NUSIZ0
               BNE __skip
                
               LDA #0
               STA VAR_NUSIZ0
__skip
               LDX VAR_NUSIZ0
               STX NUSIZ0
                
               BRK ; 19 cycles if 0, 56 cycles max

Next post we'll try adding some missles and see how collisions work.  Until then, take the player code above and try messing around with it.  The player can be 1 to 192 scanlines high, but will always be 8 bits wide.  What needs to change to accomodate different height players?  Also, the vertical positioning code is more than grossly inefficient.  How might you make it better?

,,,,,,,,,,,,,,,,,

COMMENTS