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 |