|
Subject: [stella] Death Derby: First code submission From: Glenn Saunders <cybpunks2@xxxxxxxxxxxxx> Date: Mon, 10 Sep 2001 10:12:00 -0700 |
Glenn Saunders - Producer - Cyberpunks Entertainment Personal homepage: http://www.geocities.com/Hollywood/1698 Cyberpunks Entertainment: http://cyberpunks.uni.cc
;Death Derby 2001
;version .0001 alpha
;By Glenn Saunders
;Built opon the foundation of:
; 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.
;
processor 6502
include vcs.h
; CUSTOM EQUATES
;FINE CONTROL FOR MISSILES
Right_8 = %10000000
Right_7 = %10010000
Right_6 = %10100000
Right_5 = %10110000
Right_4 = %11000000
Right_3 = %11010000
Right_2 = %11100000
Right_1 = %11110000
Right_0 = %00000000
; CONSTANTS
ScoreHeight = #32
ScreenHeight = #160
; RAM POINTERS
SEG.U vars
org $80
;PLAYFIELD LAYOUT:
;32 tombstone bits per row (X resolution)
;20 tombstone bits per column (Y resolution)
; each tombstone is 8 scanlines high
;640 bits used for the entire bitmap
; 80 bytes of RAM TOTAL
LeftPF1DataRAM ds 20
LeftPF2DataRAM ds 20
RightPF1DataRAM ds 20
RightPF2DataRAM ds 20
PlayfieldY ds 1 ; scanline counter
RowCounter ds 1
; THESE ARE TEMP VARIABLES AND SHOULD BE MERGED LATER
; USING OVERLAYS
FoundPFRam ds 1
NewTombstone ds 1
M0_CollisionRow ds 1
M1_CollisionRow ds 1
; /TEMPS
P0Score ds 2
P1Score ds 2
;76543210
;xxx SPEED (7 speeds plus stop)
; x Gear 1 = forward, 0 = reverse
; xxxx 16 degrees rotation
P0State ds 1
P1State ds 1
P0_X ds 1
P1_X ds 1
P0_Y ds 1
P1_Y ds 1
;76543210
;xx SPEED (3 speeds plus stop)
; xx Current Direction (UP DOWN LEFT RIGHT)
; xx Frame of animation (0-3)
; xx Type (0-3)
M0State ds 1
M1State ds 1
;MISSILE X is a base value. All subsequent horizontal positioning
;adds variable positive (+1-7 pixels to the right) fine control adjustments
M0_X ds 1
M1_X ds 1
M0_Y ds 1
M1_Y ds 1
CURRENT_VARIATION ds 1; encode all game variations bits here
GAME_TIMER ds 1;BCD timer for traditional arcade mode
;ROM AREA
SEG code
org $F000
Start
;
; 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
DEX
BNE B1
;
; 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
; This should only need to be called once when the game first boots up
; so it is no longer a subroutine
;GameInit
;0 is already in accumulator
STA PlayfieldY ; initialize scanline counter
LDA PF_Reflect
STA CTRLPF; set playfield to reflect
;initialize this test variable
;LDA #159
;STA TestMult8
; load initial tombstone graphics from ROM to RAM
; this won't be necessary in the final game
LDX #20 ; start at the end and cycle back to zero
LoadLeftPF1
LDA LeftPF1DataROM,X
STA LeftPF1DataRAM,X
DEX
BNE LoadLeftPF1
LDX #20 ; start at the end and cycle back to zero
LoadLeftPF2
LDA LeftPF2DataROM,X
STA LeftPF2DataRAM,X
DEX
BNE LoadLeftPF2
LDX #20
LoadRightPF1
LDA RightPF1DataROM,X
STA RightPF1DataRAM,X
DEX
BNE LoadRightPF1
LDX #20 ; start at the end and cycle back to zero
LoadRightPF2
LDA RightPF2DataROM,X
STA RightPF2DataRAM,X
DEX
BNE LoadRightPF2
JSR SetColors
;
; Here is a representation of our program flow.
;
MainLoop
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.
;
VerticalBlank
LDX #0
LDA #2
STA WSYNC
STA WSYNC
STA WSYNC
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
STA TIM64T
;
; And now's as good a time as any to clear the collision latches.
;
LDA #0
STA CXCLR
;
; 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.
;
RTS
; this was broken off in order to allow for color cycling later on
; in which case we'll need to run a function which just restores the color
; registers when the game resets
SetColors
LDA #$00
STA COLUBK ; Background will be black.
LDA #$FF
STA COLUPF ; set playfield to white for now
LDA #%10010000 ; border graphics
STA PF0 ; store border graphics for whole screen for now
RTS
;
; 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!
;
CheckSwitches
RTS
;
; Minimal game calculations, just to get the ball rolling.
;
GameCalc
RTS
;
; This is the scariest thing I've done all month.
;
DrawScreen
LDA INTIM
BNE DrawScreen ; Whew!
STA WSYNC
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.
;
;
; 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.
;
LDA #20
; initialize (or reinitialize) rowcounter
STA RowCounter
;
;reserve 32 scanlines for the score and border graphic
LDY #ScoreHeight
DrawScore
STA WSYNC
STY PF1; for debugging purposes
STY PF2
DEY
BNE DrawScore
; border line
DrawTopBorder
STA WSYNC
LDA #%00000000
STA PF0
LDA #$FF
STA PF1
STA PF2
LDA #ScreenHeight
ScanLoop
; Result of the following math is:
; Y starts out holding the current scanline
TYA ; transfer the scanline counter to A
SBC #ScoreHeight
LSR
LSR
LSR ; divide current scanline by 8
TAX ; X is going to be used to index playfield RAM
; LDA Sidewalk ; bit flip the sidewalk
; EOR #128
; STA Sidewalk
; WSYNC is placed BEFORE all of this action takes place.
STA WSYNC ; start counting below
NOP
LDA LeftPF1DataRAM,X
STA PF1
LDA LeftPF2DataRAM,X
STA PF2
; this timing is imprecise. I need help.
NOP ;2
NOP ;2
NOP ;2
NOP ;2
NOP ;2
NOP ;2
CMP $80 ;3
LDA RightPF2DataRAM,X
TAX
NOP ;2
CMP $80 ;3
LDA RightPF1DataRAM,X
STA PF2
STX PF1 ;[17]+3 = *20* < 29
DEY ; Y = Y - 1
BNE ScanLoop; brance until Y = 0
;
; 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.
;STY PF0
STY PF1
STY PF2
STY GRP0
STY GRP1
STY ENAM0
STY ENAM1
STY ENABL
RTS
;
; 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
;Y is now at zero because it is a decremental scanline counter
; insure that Sidewalk starts out as zero in the next frame
;STY Sidewalk
;We've got 30 scanlines to kill.
LDX #30
KillLines
STA WSYNC
DEX
BNE KillLines
RTS
ReverseBit
NOP ; insert code here
RTS
Add_Tombstone
; CALCULATE THE POINTER TO TOMBSTONE RAM ON COLLISION
; take the X position of the collision, divide by 4
; let's say X = 55.
; INT(55/4) = 13
; subtract by 4 because we will only be seeking beyond the safe-zone
; which is not part of the pseudobitmap
; we must turn on the 11th bit of the playfield
; divide by 8
; 11/8 = 1 remainder 3
;LDX 1
;LDA FindPF_RAM,X
;STA FoundPF_RAM
; we now have a ram pointer to the proper ram table
;NOW WE NEED TO POTENTIALLY FLIP THE BITS before writing to RAM
;LDA FindPF_RAM_BitOrder,X; load reversed bit order data
;BPL ReverseBit ; reverse bits if = $FF
; use the remainder
; turn on the 3rd bit of the first playfield ram table
; LDA 3
; STA NewTombstone ; temporarily save
; now calculate offset (row) to set
;subtract M0_Y by unused scanlines
;divide M0_Y by 8
; LDX (Y of collision) ;
; this combines the bit in the accumulator
; with the memory location starting at the contents of FindPF_RAM
; offset by the contents of X, the row counter
; ORA (FoundPF_RAM,X)
RTS
;
; 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
;
; DEATH RACE PLAYFIELD LAYOUT
; PF0| PF1 | PF2 | PF2m | PF1m |PF0m
; 4567|76543210|01234567|76543210|01234567|7654
; these tables describe the playfield layout so that we can convert
; an X/Y collision into a bit to turn on in Playfield RAM
FindPF_RAM .byte #<LeftPF1DataRAM,#<LeftPF2DataRAM,#<RightPF2DataRAM,#<LeftPF1DataRAM
FindPF_RAM_BitOrder .byte #$FF,#$00,#$FF,#$00
;THIS ROM WON'T BE NECESSARY WHEN THE GAME BECOMES INTERACTIVE
;REVERSED BIT ORDER
;76543210
LeftPF1DataROM
REPEAT 10
.byte %11111000
.byte %01010101
REPEND
LeftPF2DataROM
REPEAT 10
.byte %01010101
.byte %10101010
REPEND
;(REVERSE ALL BIT ORDERING)
;76543210
RightPF2DataROM
REPEAT 10
.byte %11111111
.byte %00000000
REPEND
;REVERSED BIT ORDER
;01234567
RightPF1DataROM
REPEAT 10
.byte %11111111
.byte %00000000
REPEND
;THE GRAPHICS
CarRotation0
.byte %00000000; ........
.byte %11101110; XXX.XXX.
.byte %01100100; .XX..X..
.byte %11011111; XX.XXXXX
.byte %11011111; XX.XXXXX
.byte %01100100; .XX..X..
.byte %11101110; XXX.XXX.
.byte %00000000; ........
CarRotation1
.byte %00000100; .....X..
.byte %00011100; ...XXX..
.byte %11000111; XX...XXX
.byte %11111111; XXXXXXXX
.byte %01011110; .X.XXXX.
.byte %01100011; .XX...XX
.byte %00110010; ..XX.X..
.byte %00110000; ..XX....
CarRotation2
.byte %00010000; ...X....
.byte %00110110; ..XX.XX.
.byte %00011110; ...XXXX.
.byte %11011100; XX.XXX..
.byte %11011011; XX.XX.XX
.byte %01000010; .X....X.
.byte %01111000; .XXXX...
.byte %00011000; ...XX...
CarRotation3
.byte %00000000; ..X.XX..
.byte %00000000; ..XXXX..
.byte %00000000; .X.XXXXX
.byte %00000000; ...XX.X.
.byte %00000000; XX.XX.X.
.byte %00000000; XXX.X...
.byte %00000000; ..XXXX..
.byte %00000000; ....XX..
CarRotation4
.byte %00011000; ...XX...
.byte %01011010; .X.XX.X.
.byte %01111110; .XXXXXX.
.byte %01011010; .X.XX.X.
.byte %00011000; ...XX...
.byte %01100110; .XX..XX.
.byte %01111110; .XXXXXX.
.byte %01011010; .X.XX.X.
;missiles are used as pseudoplayers
;data is encoded as width and pixel offsets.
;76543210
;XXXX <- OFFSET (typically 0 through +8 pixels)
; XX <- NUSIZ data
;EXAMPLE:
;LDA (M0_ShapeFrame,X)
;STA HMM0
;LSL
;LSL
;STA NUSIZ0
Ped1_1; OOOOWW__
.byte %11110100 ; .XX.....
.byte %11100000 ; ..X.....
.byte %11011000 ; ...XXXX.
.byte %11010100 ; ...XX...
.byte %11111000 ; .XXXX...
.byte %00000000 ; ........
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
Ped1_2; OOOOWW__
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
.byte %00000000 ; ........
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
Ped1_3; OOOOWW__
.byte %11110100 ; .XX.....
.byte %00000000 ; ..X.....
.byte %11011100 ; ...XXXX.
.byte %11100100 ; ..XX....
.byte %11010100 ; ...XX...
.byte %00000000 ; ........
.byte %11010100 ; ...XX...
.byte %11010100 ; ...XX...
Ped1_4; OOOOWW__
.byte %11011100 ; ...XXXX.
.byte %11010000 ; ...X....
.byte %11011100 ; ...XXXX.
.byte %11010100 ; ...XX...
.byte %00001100 ; XXXX....
.byte %11101100 ; ..XXXX..
.byte %11110100 ; .XX.....
.byte %11110100 ; .XX.....
;add score graphics later
; this makes sure the program counter jumps to the program start
org $FFFC
.word Start
.word Start
| Current Thread |
|---|
|
| <- Previous | Index | Next -> |
|---|---|---|
| Re: [stella] First steps, Chris Wilkson | Thread | Re: [stella] Death Derby: First cod, Thomas Jentzsch |
| Re: [stella] baubles.bin v0.001, wa, Chris Wilkson | Date | [stella] Polynomial Counter Flash, Glenn Saunders |
| Month |