[stella] How to Draw a Playfield [long]

Subject: [stella] How to Draw a Playfield [long]
From: Nick S Bensema <nickb@xxxxxxxxxxxx>
Date: Mon, 3 Mar 1997 00:21:23 -0700 (MST)
; How to Draw A Playfield.
; by Nick Bensema  9:23PM  3/2/97
; Atari 2600 programming is different from any other kind of programming
; in many ways.  Just one of these ways is the flow of the program.
; Since the CPU must hold tha TIA's hand through all graphical operations,
; the flow ought to go like this:
; Clear memory and registers
; Set up variables
; Loop:
;    Do the vertical blank
;    Do game calculations
;    Draw screen
;    Do more calculations during overscan
;    Wait for next vertical blank
; End Loop.
; What I will do is create an outline, and explain everything I can.
; This program will display "HELLO" and scroll it down the screen.
; In writing this program, I will take the opportunity to show you
; how a few simple modifications can completely change a program's
; appearance or behavior.  I will invite you to comment out a few
; lines of code, and alter others, so that you can observe the results.
; I will be using DASM for now.  Conversion to A65 should be trivial.
	processor 6502
	include vcs.h

	org $F000
Temp       = $80
PlayfieldY = $90

; The 2600 powers up in a completely random state, except for the PC which
; is set to the location at $FFC.  Therefore the first thing we must do is
; to set everything up inside the 6502.
	SEI  ; Disable interrupts, if there are any.
	CLD  ; Clear BCD math bit.
; You may feel the need to use the stack, in which case:
	LDX  #$FF
	TXS  ; Set stack to beginning.
; Now the memory and TIA registers must be cleared.  You may feel the
; need to write a subroutine that only clears out a certain section of
; memory, but for now a simple loop will suffice.
; Since X is already loaded to 0xFF, our task becomes simply to ocunt
; everything off.
	LDA #0
B1      STA 0,X
; The above routine does not clear location 0, which is VSYNC.  We will
; take care of that later.
; At this point in the code we would set up things like the data
; direction registers for the joysticks and such.  
	JSR  GameInit
; Here is a representation of our program flow.
	JSR  VerticalBlank ;Execute the vertical blank.
	JSR  CheckSwitches ;Check console switches.
	JSR  GameCalc      ;Do calculations during Vblank
	JSR  DrawScreen    ;Draw the screen
	JSR  OverScan      ;Do more calculations during overscan
	JMP  MainLoop      ;Continue forever.
; It is important to maintain a stable screen, and this routine
; does some important and mysterious things.  Actually, the only
; mysterious part is VSYNC.  All VBLANK does is blank the TIA's
; output so that no graphics are drawn; otherwise the screen
; scans normally.  It is VSYNC which tells the TV to pack its
; bags and move to the other corner of the screen.
; Fortunately, my program sets VBLANK at the beginning of the
; overscan period, which usually precedes this subroutine, so
; it is not changed here.
	LDX  #0
	LDA  #2
	STA  VSYNC ;Begin vertical sync.
	STA  WSYNC ; First line of VSYNC
	STA  WSYNC ; Second line of VSYNC.
; But before we finish off the third line of VSYNC, why don't we
; use this time to set the timer?  This will save us a few cycles
; which would be more useful in the overscan area.
; To insure that we begin to draw the screen at the proper time,
; we must set the timer to go off just slightly before the end of
; the vertical blank space, so that we can WSYNC up to the ACTUAL
; end of the vertical blank space.  Of course, the scanline we're
; going to omit is the same scanline we were about to waste VSYNCing,
; so it all evens out.
; Atari says we have to have 37 scanlines of VBLANK time.  Since
; each scanline uses 76 cycles, that makes 37*76=2888 cycles.
; We must also subtract the five cycles it will take to set the
; timer, and the three cycles it will take to STA WSYNC to the next
; line.  Plus the checking loop is only accurate to six cycles, making
; a total of fourteen cycles we have to waste.  2888-14=2876.
; We almost always use TIM64T for this, since the math just won't
; work out with the other intervals.  2880/64=44.something.  It
; doesn't matter what that something is, we have to round DOWN.
	LDA  #44
; And now's as good a time as any to clear the collision latches.
	LDA #0
; Now we can end the VSYNC period.
	STA  WSYNC ; Third line of VSYNC.
	STA  VSYNC ; (0)
; At this point in time the screen is scanning normally, but
; the TIA's output is suppressed.  It will begin again once
; 0 is written back into VBLANK.
; Checking the game switches is relatively simple.  Theoretically,
; some of it could be slipped between WSYNCs in the VBlank code.
; But we're going for clarity here.
; It just so happens that I'm not going to check any game switches
; here.  I'm just going to set up the colors, without even checking
; the B&W switch!  HA!
       LDA #0
       STA COLUBK  ; Background will be black.
; Minimal game calculations, just to get the ball rolling.
	INC PlayfieldY   ;Inch up the playfield

; This is the scariest thing I've done all month.
	BNE DrawScreen ; Whew!
	STA VBLANK  ;End the VBLANK period with a zero.
; Now we can do what we need to do.  What sort of playfield do
; we want to show?  A doubled playfield will work better than
; anything if we either want a side scroller (which involves some
; tricky bit shifting, usually) or an asymmetrical playfield (which
; we're not doing yet).  A mirrored playfield is usually best for
; vertical scrollers.  With some creativity, you can use both in your
; game.
; The "score" bit is useful for drawing scores with playfield graphics
; as Combat and other early games do.  It can also create a neat effect
; if you know how to be creative with it.  One useful application of
; score mode would be always having player 1 on the left side, and
; player 0 on the right side.  Each player would be surrounded in the
; opposite color, and the ball graphic could be used to stand out
; against either one.  On my 2600jr, color from the right side bleeds
; about one pixel into the left side, so don't think it's perfect.
; It's really far from perfect because PC Atari does not implement
; it at all; both sides appear as Player 0's color.  A26 does, though.
; To accomodate this, my routine puts color values into
; COLUP0 for the left side, and COLUP1 for the right side.  Change
; the LDA operand to 0 or 1 to use the normal system.  The code in
; the scanning loop accounts for both systems.
	LDA  #2
; Initialize some display variables.
	;There aren't any display variables!
; I'm going to use the Y register to count scanlines this time.
; Realize that I am forfeiting the use of the Y register for this
; purpose, but DEC Zero Page takes five cycles as opposed to DEY's
; two, and LDA Zero Page takes three cycles as opposed to TYA's two.
; I'm using all 191 remaining scanlines after the WSYNC.  If you
; want less, or more, change the LDY line.
; This is a decremental loop, that is, Y starts at 191 and continues
; down to 0, as do all functions of Y such as memory locations, which
; is why the graphics at the end of this file are stored "bottom-up".
; In a way, one could say that's how the screen is drawn.  To turn this
; into an incremental loop, change the number to 255-191 (65) and change
; the DEY at the end ot the scanning loop to INY.
	LDY #191 
; Okay, now THIS is scary.  I decided to put the bulk of my comments
; BEFORE the code, rather than inside it, so that you can look at the
; code all at once.
; Notice the new method of cycle counting I use.  I'll send an update
; to cyccount.txt RSN.
; This routine came out surprisingly clean.  There are no branches,
; and most updates are made even before PF0 becomes visible at cycle 23,
; even though PF1 and PF2 don't become visible until, by my estimate,
; cycles 29 and 40, respectively.  We could use this time to get player
; shape and colors from temp variables and sneak them in, but that's
; another file.  In fact, at the last minute I re-arranged things
; and threw in some color changes.
; The playfield will only be moved up every 4 scanlines, so it doesn't look 
; squished.  I could have updated it every 2 scanlines, and that would have 
; saved two cycles. I could have saved another two cycles by having it 
; change EVERY scanline.  Comment out one or both of the ASL's to see what 
; this would look like.  I realize that it updates the PF registers whether
; it needs it or not, but it would be pointless to branch around these
; updates.  Better to know you're wasting cycles and have them counted
; than to get unlucky and have your code spill into the next scanline
; every time too many things get updated.
; This is going to be a moving playfield.  For a stationary playfield,
; comment out the SEC and SBC lines.  That's probably what most of you all
; are going to want, anyway.  And for a really good moving playfield, 
; like in River Raid or Vanguard, you'll need slightly more interesting 
; code than I'm prepared to provide.
; I also could have made the playfield graphic 16 bytes high, or 32, or 64, 
; by changing only my data and the AND line.  AND can serve as a modulus
; for any power of two (2^n) up to 128, by ANDing a byte with that number
; minus one ( (2^n)-1 ).  8 bytes * every 4 scanlines == 32, which is
; a power of two, which is why this works.  Try commenting out the AND line
; and see how the program interprets it.  Remember that PlayfieldY goes
; up to 255.
; But you won't need to worry about that if you're drawing a stationary 
; playfield where the graphics data is so large, it doesn't need to repeat.
; In that case, you don't need the AND line and you don't need to make sure 
; your graphics are 2^n bytes tall.  Comment out the AND, SEC and SBC lines,
; and add a third LSR to the block of two.  It indexes a bit too far at the 
; top of the screen, which explains the garbage.  You can fix that problem
; either by adding more data to the end of each array, or by decreasing
; the resolution by adding a fourth or fifth LSR.
; And who's to say you'll need all three playfield registers?  Perhaps
; you have a rather narrow playfield, or one that's always clear in the
; middle.  Either choice will save you five cycles per scanline.
; As you can see, it can be trimmed down quite a bit, and I still have
; a lot of cycles left over.  The maximum, if you recall, is 73 if you
; plan to use STA WSYNC, and I pity the fool who doesn't.

; Result of the following math is:
;  X = ( (Y-PlayfieldY) /4 ) mod 7  
	SBC PlayfieldY
	LSR   ;Divide by 4
	AND #7  ;modulo 8
	LDA PFData0,X           ;Load ahead of time.
; WSYNC is placed BEFORE all of this action takes place.
	STA PF0                 ;[0] +3 = *3*   < 23
	LDA PFLColor,X          ;[3] +4
	;In a real game, I wouldn't be this redundant.
	STA COLUP0              ;[7] +3 = *10*  < 23
	STA COLUPF              ;[10]+3 = *13*  < 23
	LDA PFData1,X           ;[13]+4
	STA PF1                 ;[17]+3 = *20*  < 29
	LDA PFRColor,X          ;[20]+4
	STA COLUP1              ;[24]+3 = *27*  < 49
	LDA PFData2,X           ;[27]+4
	STA PF2                 ;[31]+3 = *34*  < 40
	BNE ScanLoop
; Clear all registers here to prevent any possible bleeding.
	LDA #2
	STA WSYNC  ;Finish this scanline.
	STA VBLANK ; Make TIA output invisible,
	; Now we need to worry about it bleeding when we turn
	; the TIA output back on.
	; Y is still zero.

; For the Overscan routine, one might take the time to process such
; things as collisions.  I, however, would rather waste a bunch of
; scanlines, since I haven't drawn any players yet.
OverScan   ;We've got 30 scanlines to kill.
	LDX #30
	 BNE KillLines

; GameInit could conceivably be called when the Select key is pressed,
; or some other event.
	LDA #0
	STA PlayfieldY

; Graphics are placed so that the extra cycle in the PFData,X indexes
; is NEVER taken, by making sure it never has to index across a page
; boundary.  This way our cycle count holds true.

	org $FF00
; This is the tricky part of drawing a playfield: actually
; drawing it.  Well, the display routine and all that binary
; math was a bit tricky, too, but still, listen up.
; Playfield data isn't stored the way most bitmaps are, even
; one-dimensional bitmaps.  We will use the left side of the
; screen only, knowing that the right side is either repeated
; or reflected from it.
; In PF0 and PF2, the most significant bit (bit 7) is on the RIGHT
; side.  In PF1, the most significant bit is on the LEFT side.  This
; means that relative to PF0 and PF2, PF1 has a reversed bit order.
; It's just really weird.
;    PF0  |     PF1       |      PF2
;  4 5 6 7|7 6 5 4 3 2 1 0|0 1 2 3 4 5 6 7
; This is important to remember when doing calculations on bytes intended
; for the PF registers.  Defender gives a good example of this.
; It will become necessary to write a program that makes this easier,
; because it is easy to become confused when dealing with this system.
PFData0  ;H       4 5 6 7
       .byte $00,$f0,$00,$A0,$A0,$E0,$A0,$A0
PFData1  ;EL      7 6 5 4 3 2 1 0
       .byte $00,$FF,$00,$77,$44,$64,$44,$74
PFData2  ;LO      0 1 2 3 4 5 6 7
       .byte $00,$FF,$00,$EE,$A2,$A2,$A2,$E2
PFLColor ; Left side of screen
       .byte $00,$FF,$00,$22,$26,$2A,$2C,$2E
PFRColor ; Right side of screen
       .byte $00,$1F,$00,$6E,$6C,$6A,$66,$62
	org $FFFC
	.word Start
	.word Start

To unsubscribe, send the word UNSUBSCRIBE in the body of a message to

Current Thread