Jump to content

Search the Community

Showing results for tags 'collect'.



More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • Atari Systems
    • Atari 2600
    • Atari 5200
    • Atari 7800
    • Atari Lynx
    • Atari Jaguar
    • Dedicated Systems
    • Atari 8-Bit Computers
    • Atari ST/TT/Falcon Computers
  • Gaming General
  • Marketplace
  • Community
  • Game Programming
  • Site
  • Classic Gaming News
  • The Club of Clubs's Discussion
  • I Hate Sauron's Topics
  • 1088 XEL/XLD Owners and Builders's Topics
  • Atari BBS Gurus's Community Chat
  • Atari BBS Gurus's BBS Callers
  • Atari BBS Gurus's BBS SysOps
  • Atari BBS Gurus's Resources
  • Atari Lynx Programmer Club's CC65
  • Atari Lynx Programmer Club's ASM
  • Atari Lynx Programmer Club's Lynx Programming
  • Atari Lynx Programmer Club's Music/Sound
  • Atari Lynx Programmer Club's Graphics
  • The Official AtariAge Shitpost Club's Shitty meme repository
  • The Official AtariAge Shitpost Club's Read this before you enter too deep
  • Arcade Gaming's Discussion
  • Tesla's Vehicles
  • Tesla's Solar
  • Tesla's PowerWall
  • Tesla's General
  • Harmony/Melody's CDFJ
  • Harmony/Melody's DPC+
  • Harmony/Melody's BUS
  • Harmony/Melody's General
  • ZeroPage Homebrew's Discussion
  • Furry Club's Chat/RP
  • PSPMinis.com's General PSP Minis Discussion and Questions
  • PSPMinis.com's Reviews
  • Atari Lynx 30th Birthday's 30th Birthday Programming Competition Games
  • 3D Printing Club's Chat
  • Drivers' Club's Members' Vehicles
  • Drivers' Club's Drives & Events
  • Drivers' Club's Wrenching
  • Drivers' Club's Found in the Wild
  • Drivers' Club's General Discussion
  • Dirtarians's General Discussion
  • Dirtarians's Members' Rigs
  • Dirtarians's Trail Runs & Reports
  • Dirtarians's Wrenching
  • The Green Herb's Discussions
  • Robin Gravel's new blog's My blog
  • Atari Video Club's Harmony Games
  • Atari Video Club's The Atari Gamer
  • Atari Video Club's Video Game Summit
  • Atari Video Club's Discsuuions
  • Star Wars - The Original Trilogy's Star Wars Talk
  • DMGD Club's Incoming!
  • DASM's General
  • AtariVox's Topics
  • Gran Turismo's Gran Turismo
  • Gran Turismo's Misc.
  • Gran Turismo's Announcements
  • The Food Club's Food
  • The Food Club's Drinks
  • The Food Club's Read me first!
  • The (Not So) Official Arcade Archives Club's Rules (READ FIRST)
  • The (Not So) Official Arcade Archives Club's Feedback
  • The (Not So) Official Arcade Archives Club's Rumor Mill
  • The (Not So) Official Arcade Archives Club's Coming Soon
  • The (Not So) Official Arcade Archives Club's General Talk
  • The (Not So) Official Arcade Archives Club's High Score Arena
  • Adelaide South Australia Atari Chat's General Chat & Welcome
  • Adelaide South Australia Atari Chat's Meets
  • Adelaide South Australia Atari Chat's Trades & Swaps
  • KC-ACE Reboot's KC-ACE Reboot Forum
  • The Official Lost Gaming Club's Lost Gaming
  • The Official Lost Gaming Club's Undumped Games
  • The Official Lost Gaming Club's Tip Of My Tounge
  • The Official Lost Gaming Club's Lost Gaming Vault
  • The Official Lost Gaming Club's Club Info
  • GIMP Users's Discussion

Blogs

There are no results to display.

There are no results to display.

Calendars

  • AtariAge Calendar
  • The Club of Clubs's Events
  • Atari BBS Gurus's Calendar
  • ZeroPage Homebrew's Schedule

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Website


Facebook


Twitter


Instagram


YouTube


eBay


GitHub


Custom Status


Location


Interests


Currently Playing


Playing Next

Found 19 results

  1. Nintendo Wii ( box / inserts / advertisements / Wii Sports / Sensor bar / 1 controller / 1 nunchuck / Power supply / composite a.v cord / instruction manual / advertising and misc. Metroid Other M (game / case / instructions / advertisement) New Super Mario Bros. Wii (game / case) 1 extra controller with Nunchuck / 2 Steering Wheels / 2 sleeves with Wii Motion Plus adapters HDMI Cable $130 for the lot Open to offers. Sega Sega Genesis (console / controller / a/v cable / power supply) Sega Genesis 6 Button Arcade Pad (Retro-bit) Pound HDMI HD cable (freebie...see picture below) Sonic the Hedgehog (cart / manual / case) Sonic the Hedgehog 2 (cart / manual / case) Sonic the Hedgehog 3 (cart / manual / case) Sonic Spinball (cart / manual / case) Mortal Kombat (cart / manual / case) Mortal Kombat II (cart / manual / case) NBA Jam T.E. (cart / manual / case / insert) Risk (cart / manual / case) World Series Baseball (cart / case) Bulls vs. Blazers (cart / case) International Tour Tennis (cart / case) Virtua Figher 2 (cart) Street Fighter 2 Special Championship Edition (cart) X-men (cart) NBA Jam T.E. (cart) Game Genie (cart) $120 for the lot Open to offers Sega 32x (console / power supply / a/v composite stereo cable / patch cable / box / instructions / brackets / cardboard inserts / outer box) Sega Virtua Racing Deluxe (cart / box / insert / manual) Moto Cross (cart / box / insert / manual) $215 for the lot Open to offers. Sega CD (console / power supply / instructions / extension / metal brace / poster / Sewer Shark / instructions / 2 screws / cardboard inserts / foam padding / outer box) ...video available down below for the Sega CD in action $225 Individually priced now. A Sega Genesis is pictured with the 32x and CD. It isn't included. The Sega Genesis is in its own lot. If you have any questions, please feel free to PM me. If anyone has any offers or suggestions, please PM me. You won't offend me with a low ball offer. If there are any errors that you find that are needed to be corrected please and kindly correct me via PM and not publicly. Thanks!
  2. A quiet game is playable, but it's more fun with sound. TIA produces 2 channel sound. The channels are known as channel 0 and channel 1. There are 3 registers for each channel to control the sound produced: AUDC0, AUDC1 - Control of the channel, specifies the type of sound to generate. Values range from 0 to 15, though some values produce the same type of sound as others. AUDF0, AUDF1 - Frequency of the channel, values range from 0(highest) to 31 (lowest) AUDV0, AUDV1 - Volume of the channel, values range from 0(min) to 15(max) It's common to have a handful of routines that handle sound effects. For Collect, these routines can be found in the new source code file sfx.asm. To add the file to collect.asm we use the include command: include sfx.asm Besides the file, we also need to allocate 2 RAM variables to keep track of the sound effects: ; indexes for sound effect driver SFX_LEFT: ds 1 ; stored in $B1 SFX_RIGHT: ds 1 ; stored in $B2 The routines in sfx.asm are: SFX_UPDATE - Must be called once every frame. This routine updates the 6 TIA registers listed above. SFX_TRIGGER - must be called when there's a sound effect to generate, such as when an object is collected by the player. Use the Y register to denote which effect to trigger. SFX_OFF - routine that silences the sound effects. If not required by your game, it can be commented out to save 15 bytes of ROM sfx.asm also includes a couple data tables that SFX_UPDATE uses to update the 6 TIA registers. Yes, only 2 tables even though there's 3 registers per channel to update. AUDCx and AUDVx both only require a nybbles worth of data, so their information is combined into a single byte to save ROM. The tables are defined like this for Collect: SFX_F: .byte 0, 31 ; collide .byte 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3 ; collect .byte 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 ; ping .byte 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31 ; game over SFX_CV: .byte 0,$8f ; collide sfxCOLLIDE = *-SFX_CV-1 .byte 0,$6f,$6f,$6f,$6f,$6f,$6f,$6f,$6f,$6f,$6f,$6f,$6f ; collect sfxCOLLECT = *-SFX_CV-1 .byte 0,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4a,$4b,$4c,$4d,$4e,$4f ; ping sfxPING = *-SFX_CV-1 .byte 0,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf,$cf ; game over sfxGAMEOVER = *-SFX_CV-1 Each .byte line contains the data for a single sound effect. The two tables are used together, so data in the first .byte line of SFX_F goes along with the data in the first .byte line in SFX_CV. The number of values must be the same in each table and each .byte line. The first value in each .byte line should be 0, it denotes end-of-sfx (though if you have a long-duration sound effect you could span it over multiple .byte lines). Table SFX_CV looks a little complicated because of the extra lines such as sfxPING = *-SFX_CV-1. All those are doing is calculating the value to be used when you trigger a sound effect. You can name your sound effects whatever you'd like, just make sure it's followed by = *-SFX_CV-1 (also make sure you have a space before and after the equal sign). Lets look at a single sound effect (that's been slightly changed from above for clarity) to explain how the data is used: SFX_F: .byte 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ; collect SFX_CV: .byte 0,$64,$65,$66,$67,$68,$69,$6a,$6b,$6c,$6d,$6e,$6f ; collect sfxCOLLECT = *-SFX_CV-1 To trigger the sound effect, you'll use this in your code: ldy #sfxCOLLECT ; select sound effect jsr SFX_TRIGGER ; and trigger it By triggering the sound effect, the value of sfxCOLLECT will be stored in one of the RAM variables. The value of sfxCOLLECT points to the 12 in the SFX_F table and the $6f in the SFX_CF table. When SFX_UPDATE is called (via jsr SFX_UPDATE), the 12 goes into AUDFx while the $6f will be split into two parts with the $6 going into AUDCx and the $f going into AUDFx. Lastly the pointer will be updated so it now points to 11 and $6e. After the next update they'll point to 10 and $6d, and so on until they point to 0 which means the end of the sound effect. The order of the sound effects, as listed in the tables, is used to denote priority. The first sound effect has the lowest priority, so if both channels are busy and you try to trigger sound effect sfxCOLLIDE, nothing will happen. If both are busy and you try to trigger sfxGAMEOVER, the last sound effect, then one of the current sound effects will be aborted so sfxGAMEOVER can be heard. SFX_UPDATE is called during Overscan: OverScan: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda #2 ; LoaD Accumulator with 2 so D1=1 sta VBLANK ; STore Accumulator to VBLANK, D1=1 turns image output off ; set the timer so the total number of scanlines ends up being 262 lda #35 sta TIM64T jsr SFX_UPDATE ; update sound effects While SFX_TRIGGER is called from a number of locations throughout the program. One of them is a tick sound that plays during the last 8 ticks of the timer: DecrementTimer: lsr TimerPF+5 ; PF2 right side, reversed bits so shift right rol TimerPF+4 ; PF1 right side, normal bits so shift left ror TimerPF+3 ; PF0 right side, reversed bits so shift right lda TimerPF+3 ; only upper nybble used, so we need to put bit 3 into C lsr lsr lsr lsr ror TimerPF+2 ; PF2 left side, reversed bits so shift right rol TimerPF+1 ; PF1 left side, normal bits so shift left ror TimerPF ; PF0 left side, reversed bits so shift right lda TimerPF+1 ; PF1 from left side and #%00011111 ; check the lower 5 bits bne NoTickSfx ; branch if there's a value in the lower 5 bits ldy #sfxPING ; else do a sound effect jsr SFX_TRIGGER NoTickSfx: In playing the game I noticed it was harder to locate the box drawn by the ball, because it is the same color as the playfield, so I modified the program so it scores 2 points instead of just 1. TestCollisions: ... bit CXP0FB ; N=player0/playfield, V=player0/ball bvc notP0BL ; if V is off, then player0 did not collide with ball ldy #0 ; which score to update ldx #4 ; which box was collected jsr Collect2ptBox ; update score and reposition box ... bit CXP1FB ; N=player1/playfield, V=player1/ball bvc notP1BL ; if V is off, then player1 did not collide with ball ldy #1 ; which score to update ldx #4 ; which box was collected jsr Collect2ptBox ; update score and reposition box Collect2ptBox is a new routine that falls into CollectBox: Collect2ptBox: lda #2 ; 2 point box .byte $2C ; BIT with absolute addressing, trick that ; causes the lda #1 to be skipped over CollectBox: lda #1 ; 1 point per box sed ; SEt Decimal flag clc ; CLear Carry bit adc Score,y ; add to player's current score ROM collect_20140713.bin Source Collect_20140713.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  3. The 2LK has been revised to display both missile objects. Missile objects do not support Vertical Delay, so there's no need to prime ENAM0 or ENAM1. ArenaLoop: ; 17 - (currently 17 from bpl ArenaLoop) ; continuation of line 2 of the 2LK ; this precalculates data that's used on line 1 of the 2LK tya ; 2 19 - 2LK loop counter in A for testing and #%11 ; 2 21 - test for every 4th time through the loop, bne SkipX ; 2 23 - (3 24) branch if not 4th time inc ArenaIndex ; 5 28 - if 4th time, increase index so new playfield data is used SkipX: ; 28 - use 28 as it's the longest path to here ldx #1 ; 2 30 - D1=0, so missile1 will be off lda #BOX_HEIGHT-1 ; 2 32 - height of box graphic dcp Missile1Draw ; 5 37 - Decrement Missile1Draw and compare with height bcs DoEnam1 ; 2 39 - (3 40) if Carry is Set, then missile1 is on current scanline .byte $24 ; 3 42 - $24 = BIT with zero page addressing, trick that ; causes the inx to be skipped DoEnam1: ; 40 - from bcs DoEnam1 inx ; 2 42 - D1=1, so ball will be ON lda #HUMAN_HEIGHT-1 ; 2 44 - height of the humanoid graphics, subtract 1 due to starting with 0 dcp Player1Draw ; 5 49 - Decrement Player1Draw and compare with height bcs DoDrawGrp1 ; 2 51 - (3 52) if Carry is Set, then player1 is on current scanline lda #0 ; 2 53 - otherwise use 0 to turn off player1 .byte $2C ; 4 57 - $2C = BIT with absolute addressing, trick that ; causes the lda (Player1Ptr),y to be skipped DoDrawGrp1: ; 52 - from bcs DoDrawGrp1 lda (Player1Ptr),y ; 5 57 - load the shape for player1 sta WSYNC ; 3 60 ;--------------------------------------- ; start of line 1 of the 2LK sta GRP1 ; 3 3 - @0-22, update player1 graphics stx ENAM1 ; 3 6 - @0-22, update missile1 graphics ldx ArenaIndex ; 3 9 lda ArenaPF0,x ; 4 13 - get current scanline's playfield pattern sta PF0 ; 3 16 - @0-22 and update it lda ArenaPF1,x ; 4 20 - get current scanline's playfield pattern sta PF1 ; 3 23 - @71-28 and update it lda ArenaPF2,x ; 4 27 - get current scanline's playfield pattern sta PF2 ; 3 30 - @60-39 ; precalculate data that's needed for line 2 of the 2LK ldx #1 ; 2 32 - D1=0, so missile0 will be off lda #BOX_HEIGHT-1 ; 2 34 - height of box graphic dcp Missile0Draw ; 5 39 - Decrement Missile0Draw and compare with height bcs DoEnam0 ; 2 41 - (3 42) if Carry is Set, then missile0 is on current scanline .byte $24 ; 3 44 - $24 = BIT with zero page addressing, trick that ; causes the inx to be skipped DoEnam0: ; 42 - from bcs DoEnam0 inx ; 2 44 - D1=1, so ball will be ON stx Temp ; 3 47 - save for line 2 ldx #1 ; 2 49 - D1=0, so ball will be off lda #BOX_HEIGHT-1 ; 2 51 - height of box graphic dcp BallDraw ; 5 56 - Decrement BallDraw and compare with height bcs DoEnabl ; 2 58 - (3 59) if Carry is Set, then ball is on current scanline .byte $24 ; 3 61 - $24 = BIT with zero page addressing, trick that ; causes the inx to be skipped DoEnabl: ; 59 - from bcs DoEnablPre inx ; 2 61 - D1=1, so ball will be ON lda #HUMAN_HEIGHT-1 ; 2 63 - height of the box graphics, dcp Player0Draw ; 5 68 - Decrement Player0Draw and compare with height bcs DoDrawGrp0 ; 2 70 - (3 71) if Carry is Set then player0 is on current scanline lda #0 ; 2 72 - otherwise use 0 to turn off player0 .byte $2C ; 4 76 - $2C = BIT with absolute addressing, trick that ; causes the lda (Player0Ptr),y to be skipped ; start of line 2 of the 2LK DoDrawGrp0: ; 71 - from bcs DoDrawGRP0 lda (Player0Ptr),y ; 5 76 - load the shape for player0 ;--------------------------------------- ; start of line 2 of the 2LK sta GRP0 ; 3 3 - @0-22, update player0 graphics stx ENABL ; 3 6 - @0-22, update ball graphics lda Temp ; 3 9 - get the precalced data for missile0 sta ENAM0 ; 3 12 - @0-22, update missile0 graphics dey ; 2 14 - decrease the 2LK loop counter bne ArenaLoop ; 2 16 - (3 17) branch if there's more Arena to draw sty PF0 ; 3 19 - @0-22, Y is 0, blank out playfield sty PF1 ; 3 22 - @71-28, Y is 0, blank out playfield sty PF2 ; 3 25 - @60-39, Y is 0, blank out playfield rts ; 6 31 - ReTurn from Subroutine The 2 lines of the 2LK now work like this: updates player1, missile1, playfield, precalcs player0, missile0, ball for line 2 updates player0, missile0, ball, precalcs player1, missile1 and playfield index for line 1 We didn't have enough time to update missile0 and missile1 on both lines of the 2LK. Since TIA doesn't offer a vertical delay feature for the missiles, they can never line up. This is just like the players didn't line up when VDELP0 and VDELP1 were not used. For our game, this isn't a problem. One thing to notice about the revised 2LK is the 1st line uses exactly 76 cycles, so it no longer ends with a sta WSYNC. PositionObjects has been modified to initialize 2 new variables, Missile0Draw and Missile1Draw, that are used by the 2LK to control when the missiles are drawn. PositionObjects: ... ; prep missile0's Y position for 2LK lda ObjectY+2 ; get the missile's Y position lsr ; divide by 2 for 2LK sta Temp ; save for position calculation ; Missile0Draw = ARENA_HEIGHT + BOX_HEIGHT - Y position lda #(ARENA_HEIGHT + BOX_HEIGHT) sec sbc Temp sta Missile0Draw ; prep missile1's Y position for 2LK lda ObjectY+3 ; get the missile's Y position lsr ; divide by 2 for 2LK sta Temp ; save for position calculation ; Missile0Draw = ARENA_HEIGHT + BOX_HEIGHT - Y position lda #(ARENA_HEIGHT + BOX_HEIGHT) sec sbc Temp sta Missile1Draw The routines are a little simpler than the others (for player0, player1 and ball) as the missiles don't have a Vertical Delay to set up. This is how the Arena looks with all the boxes drawn: Not exactly right, is it? One thing we forgot to do was set the width of the missiles! We'd done that for the ball back in step 9 when we set the upper nybble of CTRLPF to 3: TimerBar: ... lda Variation ; 3 20 lsr ; 2 22 - which Arena to show tay ; 2 24 - set for index ldx ArenaOffset,y ; 4 28 - set X for which arena to draw lda ArenaPF0,x ; 4 32 - reflect and priority for playfield and #%00000111 ; 2 34 - get the lower 3 bits for CTRLPF ora #%00110000 ; 2 36 - set ball to display as 8x pixel sta CTRLPF ; 3 39 Which is why the ball was the right width when we added it in step 11. The same setting is needed for the missiles, but it goes into NUSIZ0 and NUSIZ1 instead: VerticalSync: ... lda #$30 ; sta NUSIZ0 ; set missile0 to be 8x sta NUSIZ1 ; set missile1 to be 8x ... And now it looks correct: OverScan was updated to detect collisions with the boxes drawn by the missiles: OverScan: ... notP0BL: bit CXM0P ; V=player0/missile0 bvc notP0M0 ; if V is off then player0 did not collide with missile0 ldx #2 ; which box was collected jsr CollectBox ; update score and reposition box notP0M0: bit CXM1P ; N=player0/missile1 bpl notP0M1 ; if N is off then player0 did not collide with missile1 ldx #3 ; which box was collected jsr CollectBox ; update score and reposition box notP0M1: ... notP1BL: bit CXM0P ; N=player1/missile0 bpl notP1M0 ; if N is off then player1 did not collide with missile0 ldx #2 ; which box was collected jsr CollectBox ; update score and reposition box notP1M0: bit CXM1P ; V=player1/missile1 bvc notP1M1 ; if V is off then player1 did not collide with missile1 ldx #3 ; which box was collected jsr CollectBox ; update score and reposition box notP1M1: A test run of the players collecting the boxes: Lastly, on the off chance that somebody might roll the score, a new Digit graphic has been added: And a minor change was made to CollectBox to display it and end the game if a player collected 100 boxes. CollectBox: sed ; SEt Decimal flag clc ; CLear Carry bit lda #1 ; 1 point per box adc Score,y ; add to player's current score bcc Not100 ; if the Carry is clear, score did not roll sta GameState ; stop the game (A holds 0) lda #$BB ; B image is !! to show that score rolled Not100: sta Score,y ; and save it cld ; CLear Decimal flag jsr RandomLocation ; move box to new location rts To test it I started up a game and used Stella's debugger to change the score to 99: Then collected another box: ROM collect_20140712.bin Source Collect_20140712.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  4. For this update, we're going to modify the Arena Loop to draw the Arena using the playfield. The new Arena loop has these new changes: ArenaLoop: ; 27 - (currently 7 from bpl ArenaLoop) tya ; 2 29 - 2LK loop counter in A for testing and #%11 ; 2 31 - test for every 4th time through the loop, bne SkipX ; 2 33 (3 34) branch if not 4th time inx ; 2 35 - if 4th time, increase X so new playfield data is used SkipX: ; 35 - use 35 as it's the longest path here ... ; start of line 1 of the 2LK sta GRP1 ; 3 3 - @0-22, update player1 graphics lda ArenaPF0,x ; 4 7 - get current scanline's playfield pattern sta PF0 ; 3 10 - @0-22 and update it lda ArenaPF1,x ; 4 14 - get current scanline's playfield pattern sta PF1 ; 3 17 - @71-28 and update it lda ArenaPF2,x ; 4 21 - get current scanline's playfield pattern sta PF2 ; 3 24 - @60-39 ... ; start of line 2 of the 2LK sta GRP0 ; 3 3 - @0-22, update player0 graphics dey ; 2 5 - decrease the 2LK loop counter bne ArenaLoop ; 2 7 - (3 branch if there's more Arena to draw sty PF0 ; 3 10 - Y is 0, blank out playfield sty PF1 ; 3 13 - Y is 0, blank out playfield sty PF2 ; 3 16 - Y is 0, blank out playfield rts ; 6 22 - ReTurn from Subroutine The first change is we're using X as an index into the playfield graphic data. We're changing X every fourth time thru the 2LK, so each byte of playfield data will be used over 8 scanlines. This saves a bit of ROM. Second change is all 3 playfield registers (PF0, PF1 and PF2) are now updated, and they're only updated on line 1 of our 2LK. Third change is on line 2, the bpl ArenaLoop is now a bne ArenaLoop else the bottom row of playfield data was only used for 2 scanlines instead of 8. We also blank out the playfield registers when we are done drawing the playfield. The bne change also impacted Overscan - TIM64T was originally set to 32, it's now set to 35. The playfield data looks like this in jEdit: and this onscreen: Lastly we added some collision detection code. Some space was allocated in RAM: ;save player locations for playfield collision logic SavedX: ds 2 ; stored in $A1-A2 SavedY: ds 2 ; stored in $A3-A4 Then the Process Joystick routines save the current X and Y values before processing the joystick: PJloop: ldy ObjectX,x ; save original X location so the player can be sty SavedX,x ; bounced back upon colliding with the playfield ldy ObjectY,x ; save original Y location so the player can be sty SavedY,x ; bounced back upon colliding with the playfield Finally OverScan was modified to move the players back to their previous X and Y location if a collision was detected: ; Test if player collided with playfield bit CXP0FB ; N = player0/playfield, V=player0/ball bpl notP0PF ; if N is off, then player0 did not collide with playfield lda SavedX ; recall saved X sta ObjectX ; and move player back to it lda SavedY ; recall saved Y sta ObjectY ; and move player back to it notP0PF: bit CXP1FB ; N = player1/playfield, V=player1/ball bpl notP1PF ; if N is off, then player1 did not collide with playfield lda SavedX+1 ; recall saved X sta ObjectX+1 ; and move player back to it lda SavedY+1 ; recall saved Y sta ObjectY+1 ; and move player back to it notP1PF: ROM collect_20140706.bin Source Collect_20140706.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  5. As anybody involved in writing software can tell you, project specifications will often change when new information becomes available. When I started working on Collect, my plan was to use it for my presentation at Classic Game Fest. As I've progressed I've come to the realization that a full blown game is going to be just for a one hour presentation. I decided that I'm going to leave the existing example in place and just add a few slides about Collect with a link to these blog entries for anybody who is interested. Since I'm no longer planning to fit this project into a presentation, I've decided on a few changes: Make Collect a 1-2 player game Add the Ball object to draw an additional box, especially useful for 2 player games Change Timer display to the right player's score Add a Timer Bar to indicate time remaining Mockup of two player game with new timer bar Timer has decreased One player variation will hide right player's score and use player1 as an additional box. The Ball Object has a vertical delay feature. When used, the ball should be udpated on the same scanline as player0. Due to this, I've revised the 2LK to be like this: updates player1, playfield, precalc player0 for line 2 updates player0, playfield, precalc player1 for line 1 This was done to plan ahead for when the playfield is no longer updated on every scanline. Updating the playfield and ball will make the 2LK look something like this: updates player1, playfield, precalcs player0, ball for line 2 updates player0, ball, precalcs player 1, playfield for line 1 I'll also need to add in updates for the missiles. Ideally we want to update them on every scanline like this: updates player1, missile0, missile1, playfield, precalcs player0, missile0, missile1, ball for line 2 updates player0, missile0, missile1, ball, precalcs player 1, missile0, missile1, playfield for line 1 It's probable the timing won't work out for that. If it doesn't, then a change like this should work: updates player1, missile1, playfield, precalcs player0, missile0, ball for line 2 updates player0, missile0, ball, precalcs player1, missile1, playfield for line 1 That would make it so that the missile objects can only start on every-other-scanline, but that's an OK compromise for our game. In this build I've revised the Arena to be a little bit shorter to make room for the new timer display. The timer currently "ticks" once every 64 frames. Whenever it ticks, a bunch of byte rotatations are done to shorten the length of the timer bar. DecrementTimer: lsr TimerPF+5 ; PF2 right side, reversed bits so shift right rol TimerPF+4 ; PF1 right side, normal bits so shift left ror TimerPF+3 ; PF0 right side, reversed bits so shift right lda TimerPF+3 ; only upper nybble used, so we need to put bit 3 into C lsr lsr lsr lsr ror TimerPF+2 ; PF2 left side, reversed bits so shift right rol TimerPF+1 ; PF1 left side, normal bits so shift left ror TimerPF ; PF0 left side, reversed bits so shift right rts Since there are 40 playfield pixels, the total playtime would be 40*64/60 = 42.7 seconds. We might decide that's too short of a play time. If so, we'll just change the tick to occur every 128 frames for 40*128/60 = 85.3 seconds of game time, or maybe even once very 256 frames for 40*256/60 = 170.7 seconds. SetObjectColors has been modified to add a color for the timer bar. The Timer Bar and the Arena are both drawn using the playfield, so to make the Arena a different color than the Timer Bar I store the current Arena color in a RAM location. SetObjectColors: ldx #4 ; we're going to set 5 colors (0-4) ldy #4 ; default to the color entries in the table (0-4) lda SWCHB ; read the state of the console switches and #%00001000 ; test state of D3, the TV Type switch bne SOCloop ; if D3=1 then use color ldy #9 ; else use the b&w entries in the table (5-9) SOCloop: lda Colors,y ; get the color or b&w value sta COLUP0-1,x ; and set it dey ; decrease Y dex ; decrease X bne SOCloop ; Branch Not Equal to Zero lda Colors,y ; get the Arena color sta ArenaColor ; save in RAM for Kernal Usage rts ; ReTurn from Subroutine Colors: .byte $46 ; red - goes into COLUPF, color for Arena (after Timer is drawn) .byte $86 ; blue - goes into COLUP0, color for player0 and missile0 .byte $C6 ; green - goes into COLUP1, color for player1 and missile1 .byte $64 ; purple - goes into COLUPF, color for Timer .byte $00 ; black - goes into COLUBK, color for background .byte $0A ; light grey - goes into COLUPF, color for Arena (after Timer is drawn) .byte $0E ; white - goes into COLUP0, color for player0 and missile0 .byte $06 ; dark grey - goes into COLUP1, color for player1 and missile1 .byte $04 ; dark grey - goes into COLUPF, color for Timer .byte $00 ; black - goes into COLUBK, color for background For testing, I've set it up so the Right Difficulty switch is used to determine if the game is a one or two player game for which graphics to use for player1: ldx #0 bit SWCHB bpl TwoPlayer ldx #1 TwoPlayer: ; Player1Ptr = BoxGfx + HUMAN_HEIGHT - 1 - Y position lda ShapePtrLow,x sec sbc Temp sta Player1Ptr lda ShapePtrHi,x sbc #0 sta Player1Ptr+1 rts ShapePtrLow: .byte <(HumanGfx + HUMAN_HEIGHT - 1) .byte <(BoxGfx + HUMAN_HEIGHT - 1) ShapePtrHi: .byte >(HumanGfx + HUMAN_HEIGHT - 1) .byte >(BoxGfx + HUMAN_HEIGHT - 1) Right Difficulty = B Right Difficulty = A ROM collect_20140704a.bin Source Collect_20140704a.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  6. In Step 1 I used loops of sta WSYNC commands to delay the program so that Vertical Blank and OverScan would last for the proper duration. That method works fine when all we want to do is generate a static display, but as soon as we start to add game logic that won't work out so well. The problem with the game logic is there will be so many different paths the code can take that it is nearly impossible for us to know how long the code ran, and thus we won't know how many scanlines we need to delay before the next section of code can run. As an example, if the player isn't moving the joystick then none of the "move player" logic will run. If the player is moving the joystick left and up then the "move horizontal" and "move vertical" logic will run. If the player is only holding the joystick left then only the "move horizontal" logic will run. Fortunately for us, the Atari 2600 contains a RIOT chip. That acronym stands for RAM, Input/Output and Timer. We're interested in the Timer for this update to Collect, we'll look at RAM and I/O in a later update. First thing I changed was OverScan. The original routine looked like this: OverScan: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda #2 ; LoaD Accumulator with 2 so D1=1 sta VBLANK ; STore Accumulator to VBLANK, D1=1 turns image output off ldx #27 ; LoaD X with 27 osLoop: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) dex ; DEcrement X by 1 bne osLoop ; Branch if Not Equal to 0 rts ; ReTurn from Subroutine So what we want to do is set a timer that will go off after 27 scanlines to pass. There's 76 cycles of time per scanline, so we need the timer to go off after 2052 cycles have passed. When we set the timer, we also select how frequently the timer will decrement in value. RIOT has options to decrement the timer every 1, 8, 64 or 1024 cycles. The timer is set using a single byte, so it can only be set to any value from 0 to 255. As such, we know we can't use decrement every 1 cycle as 2052 is too large. So let's check if decrement every 8 cycles will work: 2052/8 = 256.5 Almost, but 256 won't fit so we're going to have to use the decrement every 64 cycles option. To figure out the initial value to set the timer to, use this equation: (scanlines * 76) / 64 The new OverScan routine that uses the timer looks like this: OverScan: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda #2 ; LoaD Accumulator with 2 so D1=1 sta VBLANK ; STore Accumulator to VBLANK, D1=1 turns image output off lda #32 ; set timer for 27 scanlines, 32 = ((27 * 76) / 64) sta TIM64T ; set timer to go off in 27 scanlines ; game logic will go here OSwait: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda INTIM ; Check the timer bne OSwait ; Branch if its Not Equal to 0 rts ; ReTurn from Subroutine For Vertical Blank we're going to set up the timer a little different. There's time in VerticalSync we can utilize, so we'll set the timer there - look for the code using ldx and stx: VerticalSync: lda #2 ; LoaD Accumulator with 2 so D1=1 ldx #49 ; LoaD X with 49 sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) sta VSYNC ; Accumulator D1=1, turns on Vertical Sync signal stx TIM64T ; set timer to go off in 41 scanlines (49 * 64) / 76 sta WSYNC ; Wait for Sync - halts CPU until end of 1st scanline of VSYNC sta WSYNC ; wait until end of 2nd scanline of VSYNC lda #0 ; LoaD Accumulator with 0 so D1=0 sta WSYNC ; wait until end of 3rd scanline of VSYNC sta VSYNC ; Accumulator D1=0, turns off Vertical Sync signal rts ; ReTurn from Subroutine We're also going to check the timer in the Kernel so we can start drawing the screen as soon as it goes off: Kernel: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda INTIM ; check the timer bne Kernel ; Branch if its Not Equal to 0 ; turn on the display sta VBLANK ; Accumulator D1=0, turns off Vertical Blank signal (image output on) ; draw the screen ldx #192 ; Load X with 192 KernelLoop: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) stx COLUBK ; STore X into TIA's background color register dex ; DEcrement X by 1 bne KernelLoop ; Branch if Not Equal to 0 rts ; ReTurn from Subroutine For the moment, these changes leave VerticalBlank with nothing to do: VerticalBlank: rts ; ReTurn from Subroutine ROM collect_20140625.bin Source Collect_20140625.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  7. For this update we're adding initial support for the Select and Reset buttons. For this we're adding a new RAM variable called GameState to keep track of "Game Active" vs "Game Over". ; D7, 1=Game Active, 0=Game Over GameState: ds 1 ; stored in $A7 We're going to use D7 to denote the state as we can easily test D7 (as well as D6) by using the BIT command. You can see this in the revised Vertical Blank routine were we test GameState to determine if UpdateTimer and ProcessJoystick should be skipped over: VerticalBlank: jsr ProcessSwitches bit GameState bpl NotActive jsr UpdateTimer jsr ProcessJoystick NotActive: jsr PositionObjects jsr SetObjectColors jsr PrepScoreForDisplay rts ; ReTurn from Subroutine ProcessSwitches will check SWCHB to see if RESET is pressed. If so, it'll start up a new game. If not, it'll check if SELECT is pressed, and if so cancel an active game. ProcessSwitches: lda SWCHB ; load in the state of the switches lsr ; D0 is now in C bcs NotReset ; if D0 was on, the RESET switch was not held jsr InitPos ; Prep for new game lda #%10000000 sta GameState ; set D7 on to signify Game Active rts NotReset: lsr ; D1 is now in C bcs NotSelect lda #0 sta GameState ; clear D7 to signify Game Over NotSelect: rts In the next update ProcessSwitches will be expanded upon so that the Select routine will let you select a game variation (and if you check the source you'll see a new Arena layout is already in place for that). In order to visually show you that the game is over, I've revised the Color routines to color cycle if the game is not active. SetObjectColors: lda #$FF sta Temp2 ; default to color mask and ColorCycle ; color cycle bit GameState bpl SOCgameover lda #0 ; if game is active, no color cycle SOCgameover: sta Temp ldx #4 ; we're going to set 5 colors (0-4) ldy #4 ; default to the color entries in the table (0-4) lda SWCHB ; read the state of the console switches and #%00001000 ; test state of D3, the TV Type switch bne SOCloop ; if D3=1 then use color ldy #$0f sty Temp2 ; set B&W mask ldy #9 ; else use the b&w entries in the table (5-9) SOCloop: lda Colors,y ; get the color or b&w value eor Temp ; color cycle and Temp2 ; B&W mask sta COLUP0-1,x ; and set it dey ; decrease Y dex ; decrease X bne SOCloop ; Branch Not Equal to Zero lda Colors,y ; get the Arena color eor Temp ; color cycle and Temp2 ; B&W mask sta ArenaColor ; save in RAM for Kernal Usage rts ; ReTurn from Subroutine Color cycle example B&W Color Cycle example: ROM collect_20140707.bin Source Collect_20140707.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  8. After getting a stable display, I like to implement the routines for displaying the score. You can see that in the first builds of Frantic, Medieval Mayhem, and Space Rocks. Even though we're not ready to show the player's score, the display is very useful for showing diagnostic information - such as this build of Frantic which uses it to show which sprites are colliding with the player. To draw the score we're going to use the playfield graphics. The playfield pattern is comprised of 20 bits stored in 3 bytes of the TIA. PF0 - only the upper 4 bits are used, and they're output in reverse order PF1 - all 8 bits are used, output as you'd expect PF2 - all 8 bits are used, output in reverse order Andrew Davie posted this rather nice diagram in his 2600 Programming For Newbies tutorial which shows how that works: The 20 bits are either repeated or reflected to draw the 20 bits on the right half of the screen. For our game a two digit score and a two digit timer will meet our requirements. We could show a single digit each in PF1 and PF2, but due to the reversed output of PF2 that would mean we'd need to create both normal and mirrored digit graphics. Instead, we're going to create digit graphics that are 3 bits across, which will allow us to show two digits using PF1. You may have seen the graphics in my prior blog entry with the revised mode file for jEdit. Each digit appears twice as we can use a simple mask to get the tens (AND #$F0) and ones(AND #$0F) digits. If we only saved the image as the ones position we'd need to apply 4 shift instructions to create a tens position image. PF1 is displayed twice on the screen and if we time it correctly we can change the contents of PF1 so that both sides of the screen are different. Andrew Davie posted another handy diagram that shows the timing: In looking at the diagram we can figure out the update times required for each instance of PF1 left PF1 - need to update on or after cycle 66 of the prior scanline, or on or before cycle 28 of the current scanline right PF1 - need to update on or after cycle 39 and on or before cycle 54 of the current scanline As mentioned before, the RIOT chip in the Atari 2600 stands for RAM, Input/Output and Timer. For this update we're going to look at the RAM and Input. In order to show the score we need to keep track of a few things, so let's allocate some space in RAM: ORG $80 ; Holds 2 digit score, stored as BCD (Binary Coded Decimal) Score: ds 1 ; stored in $80 ; Holds 2 digit timer, stored as BCD Timer: ds 1 ; stored in $81 ; Offsets into digit graphic data DigitOnes: ds 2 ; stored in $82-$83, DigitOnes = Score, DigitOnes+1 = Timer DigitTens: ds 2 ; stored in $84-$85, DigitTens = Score, DigitTens+1 = Timer ; graphic data ready to put into PF1 ScoreGfx: ds 1 ; stored in $86 TimerGfx: ds 1 ; stored in $87 ; scratch variable Temp: ds 1 ; stored in $88 Vertical Blank is now doing a little bit of work: VerticalBlank: jsr SetObjectColors jsr PrepScoreForDisplay rts ; ReTurn from Subroutine When the macro CLEAN_START initialized the Atari's hardware, it set all the colors to black. So we need to set the object colors if we want to see anything. This routine also reads the state of the console switches (via one of RIOT's Input registers) in order to determine if the player selected Color or Black & White: SetObjectColors: ldx #3 ; we're going to set 4 colors (0-3) ldy #3 ; default to the color entries in the table (0-3) lda SWCHB ; read the state of the console switches and #%00001000 ; test state of D3, the TV Type switch bne SOCloop ; if D3=1 then use color ldy #7 ; else use the b&w entries in the table (4-7) SOCloop: lda Colors,y ; get the color or b&w value sta COLUP0,x ; and set it dey ; decrease Y dex ; decrease X bpl SOCloop ; Branch PLus (positive) rts ; ReTurn from Subroutine Colors: .byte $86 ; blue - goes into COLUP0, color for player0 and missile0 .byte $C6 ; green - goes into COLUP1, color for player1 and missile1 .byte $46 ; red - goes into COLUPF, color for playfield and ball .byte $00 ; black - goes into COLUBK, color for background .byte $0E ; white - goes into COLUP0, B&W for player0 and missile0 .byte $06 ; dark grey - goes into COLUP1, B&W for player1 and missile1 .byte $0A ; light grey - goes into COLUPF, B&W for playfield and ball .byte $00 ; black - goes into COLUBK, B&W for background The score and timer will be stored using BCD (Binary Coded Decimal) but for now we'll treat and display them as hexadecimal values (that's why the digit graphics are 0-9 then A-F). This routine takes the upper and lower nybble (4 bits) of the Score and Timer and multiplies them by 5 in order to get the offset into the digit graphic data. The 6507 does not have a multiply command, though it does have a shift feature which is equivalent to *2. If a nybble is value X then X * 2 * 2 + X is the same as X * 5: PrepScoreForDisplay: ; for testing purposes, change the values in Timer and Score inc Timer ; INCrement Timer by 1 bne PSFDskip ; Branch Not Equal to 0 inc Score ; INCrement Score by 1 if Timer just rolled to 0 PSFDskip: ldx #1 ; use X as the loop counter for PSFDloop PSFDloop: lda Score,x ; LoaD A with Timer(first pass) or Score(second pass) and #$0F ; remove the tens digit sta Temp ; Store A into Temp asl ; Accumulator Shift Left (# * 2) asl ; Accumulator Shift Left (# * 4) adc Temp ; ADd with Carry value in Temp (# * 5) sta DigitOnes,x ; STore A in DigitOnes+1(first pass) or DigitOnes(second pass) lda Score,x ; LoaD A with Timer(first pass) or Score(second pass) and #$F0 ; remove the ones digit lsr ; Logical Shift Right (# / 2) lsr ; Logical Shift Right (# / 4) sta Temp ; Store A into Temp lsr ; Logical Shift Right (# / 8) lsr ; Logical Shift Right (# / 16) adc Temp ; ADd with Carry value in Temp ((# / 16) * 5) sta DigitTens,x ; STore A in DigitTens+1(first pass) or DigitTens(second pass) dex ; DEcrement X by 1 bpl PSFDloop ; Branch PLus (positive) to PSFDloop rts ; ReTurn from Subroutine The Kernel's been modified so it now uses the data in DigitOnes and DigitTens to update PF1. Each line of graphic data is output twice so the digits are drawn over 10 scanlines which gives them a better appearance than if they had been drawn over 5 scanlines. ldx #5 ScoreLoop: ; 43 - cycle after bpl ScoreLoop ldy DigitTens ; 3 46 - get the tens digit offset for the Score lda DigitGfx,y ; 5 51 - use it to load the digit graphics and #$F0 ; 2 53 - remove the graphics for the ones digit sta ScoreGfx ; 3 56 - and save it ldy DigitOnes ; 3 59 - get the ones digit offset for the Score lda DigitGfx,y ; 5 64 - use it to load the digit graphics and #$0F ; 2 66 - remove the graphics for the tens digit ora ScoreGfx ; 3 69 - merge with the tens digit graphics sta ScoreGfx ; 3 72 - and save it sta WSYNC ; 3 75 - wait for end of scanline ;--------------------------------------- sta PF1 ; 3 3 - @66-28, update playfield for Score dislay ldy DigitTens+1 ; 3 6 - get the left digit offset for the Timer lda DigitGfx,y ; 5 11 - use it to load the digit graphics and #$F0 ; 2 13 - remove the graphics for the ones digit sta TimerGfx ; 3 16 - and save it ldy DigitOnes+1 ; 3 19 - get the ones digit offset for the Timer lda DigitGfx,y ; 5 24 - use it to load the digit graphics and #$0F ; 2 26 - remove the graphics for the tens digit ora TimerGfx ; 3 29 - merge with the tens digit graphics sta TimerGfx ; 3 32 - and save it jsr Sleep12 ;12 44 - waste some cycles sta PF1 ; 3 47 - @39-54, update playfield for Timer display ldy ScoreGfx ; 3 50 - preload for next scanline sta WSYNC ; 3 53 - wait for end of scanline ;--------------------------------------- sty PF1 ; 3 3 - @66-28, update playfield for the Score display inc DigitTens ; 5 8 - advance for the next line of graphic data inc DigitTens+1 ; 5 13 - advance for the next line of graphic data inc DigitOnes ; 5 18 - advance for the next line of graphic data inc DigitOnes+1 ; 5 23 - advance for the next line of graphic data jsr Sleep12 ;12 35 - waste some cycles dex ; 2 37 - decrease the loop counter sta PF1 ; 3 40 - @39-54, update playfield for the Timer display bne ScoreLoop ; 2 42 - (3 43) if dex != 0 then branch to ScoreLoop sta WSYNC ; 3 45 - wait for end of scanline ;--------------------------------------- stx PF1 ; 3 3 - x = 0, so this blanks out playfield sta WSYNC ; 3 6 - wait for end of scanline Score and Timer: when TV Type switched to B&W: You'll notice that the score and timer are different colors even though they're both drawn using the playfield. This is because I've modified the Vertical Sync subroutine to turn on SCORE mode. SCORE mode tells TIA to use the color of player0 for the left half of the playfield and the color of player1 for the right half. VerticalSync: lda #2 ; LoaD Accumulator with 2 so D1=1 ldx #49 ; LoaD X with 49 sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) sta VSYNC ; Accumulator D1=1, turns on Vertical Sync signal stx TIM64T ; set timer to go off in 41 scanlines (49 * 64) / 76 sta CTRLPF ; D1=1, playfield now in SCORE mode... rts ; ReTurn from Subroutine ROM collect_20140628.bin Source Collect_20140628.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  9. NOTE: If you're not familiar with 6502 assembly language, take some time to check out the Easy 6502 tutorial. For my Atari 2600 Homebrew presentation I've been giving a rundown of the challenges involved in writing an Atari game, namely the limited resources and capabilities of the hardware, as well as the tools (DASM, bB, Stella, Harmony, etc) and resources(AtariAge, Mini Dig, etc) that are available to help you. I've been updating the presentation for each time I give it. On the most recent update, for the 2013 Houston Arcade Expo, I added code for a very simple 2600 program. You can see it here, starting with slide 51. You can also download the source and ROM from my website - colorful.zip. The code addition went over very well so I've decided to expand upon it for my next presentation, which will be given the weekend of August 16th at the Classic Game Fest in Austin. I decided a very simple game would be the way to go and have worked up a mockup of what it might look like: The game's going to be called Collect. It's a 1 player game and your objective is to collect as many boxes as you can before the timer runs out. My goals for code are to show: How to use TV Type, Select and Reset console switches How to use joystick to move player How to use the hardware collision detection How to use a 2 Line Kernel to draw a reflected playfield with 2 players and 2 missiles Sound Effects (timer tick, collected box, end-of-game) For this tutorial you'll need to have DASM to compile the code as well as Stella and/or a real 2600 with a Harmony cart to run your code. COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  10. This tutorial covers the writing of a 2K game for the Atari 2600. Since it's a tutorial, I put significantly more comments in the source code than I normally would. So be sure to download the source and read it along with each blog entry. The tutorial does assume you have some understanding of 6502 assembly language. If you don't then check out the Easy 6502 tutorial before starting this tutorial - don't worry, it's not a difficult processor to learn* Let's Make a Game! goals for the tutorial Step 1 - Generate a Stable Display On other systems the video chip generates the display, on the 2600 your program generates the display Step 2 - Timers Improve the display generation by using the built in timer Step 3 - Score & Timer display using the playfield to display information Step 4 - 2 Line Kernel draw the player objects onscreen (X & Y location) Note: players are known as sprites in modern nomenclature Step 5 - automate Vertical Delay finish the Y positioning of the player objects step 6 - Spec Change revise our goals step 7 - draw the playfield display an arena (like the mazes in Combat) step 8 - Select and Reset support Using the Game Select and Game Reset console switches step 9 - Game Variations how to implement game variations (number of players, different mazes) step 10 - "Random Numbers" how to randomize your game step 11 - add the ball object draw the ball onscreen (X & Y location) step 12 - add the missile objects draw the missiles onscreen (X & Y location) step 13 - add sound effects let's make some noise! step 14 - add animation make the humans run instead of glide CollectMini simplified version of collect, ROM only CollectMini source code source for the simplified version of Collect * I taught this to myself back in 1982 (I was 15) by reading section 3, Machine Language Programming Guide, of the VIC-20 Programmer's Reference Guide. I didn't have an assembler at the time, so would hand assembly my code, type the results into DATA statements, POKE them into memory, and then finally run them via SYS. COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  11. The 2LK has been revised to display the ball object. Like player0, the ball needs to be primed before the ArenaLoop begins: ; prime ENABL so ball can appear on topmost scanline of Arena ldx #1 ; 2 2 - D1=0, so ball will be off lda #BOX_HEIGHT-1 ; 2 4 - height of box graphic dcp BallDraw ; 5 9 - Decrement BallDraw and compare with height bcs DoEnablPre ; 2 11 - (3 12) if Carry is Set, then ball is on current scanline .byte $24 ; 3 14 - $24 = BIT with zero page addressing, trick that ; causes the inx to be skipped DoEnablPre: ; 12 - from bcs DoEnablPre inx ; 2 14 - D1=1, so ball will be ON stx ENABL ; 3 17 ; prime GRP0 so player0 can appear on topmost scanline of the Arena lda #HUMAN_HEIGHT-1 ; 2 19 - height of player0 graphics, dcp Player0Draw ; 5 24 - Decrement Player0Draw and compare with height bcs DoDrawGrp0pre ; 2 26 - (3 27) if Carry is Set, then player0 is on current scanline lda #0 ; 2 28 - otherwise use 0 to turn off player0 .byte $2C ; 4 32 - $2C = BIT with absolute addressing, trick that ; causes the lda (Player0Ptr),y to be skipped DoDrawGrp0pre: ; 27 - from bcs DoDrawGRP0pre lda (Player0Ptr),y ; 5 32 - load the shape for player0 sta GRP0 ; 3 35 - @0-22, update player0 graphics dey ; 2 37 ArenaLoop: ; 37 - (currently 11 from bpl ArenaLoop) tya ; 2 39 - 2LK loop counter in A for testing and #%11 ; 2 41 - test for every 4th time through the loop, bne SkipX ; 2 43 - (3 44) branch if not 4th time inc ArenaIndex ; 5 48 - if 4th time, increase index so new playfield data is used SkipX: ; 48 - use 48 as it's the longest path here ; continuation of line 2 of the 2LK ; this precalculates data that's used on line 1 of the 2LK lda #HUMAN_HEIGHT-1 ; 2 50 - height of the humanoid graphics, subtract 1 due to starting with 0 dcp Player1Draw ; 5 55 - Decrement Player1Draw and compare with height bcs DoDrawGrp1 ; 2 57 - (3 58) if Carry is Set, then player1 is on current scanline lda #0 ; 2 59 - otherwise use 0 to turn off player1 .byte $2C ; 4 63 - $2C = BIT with absolute addressing, trick that ; causes the lda (Player1Ptr),y to be skipped DoDrawGrp1: ; 58 - from bcs DoDrawGrp1 lda (Player1Ptr),y ; 5 63 - load the shape for player1 sta WSYNC ; 3 66 ;--------------------------------------- ; start of line 1 of the 2LK sta GRP1 ; 3 3 - @0-22, update player1 graphics ldx ArenaIndex ; 3 6 lda ArenaPF0,x ; 4 10 - get current scanline's playfield pattern sta PF0 ; 3 13 - @0-22 and update it lda ArenaPF1,x ; 4 17 - get current scanline's playfield pattern sta PF1 ; 3 20 - @71-28 and update it lda ArenaPF2,x ; 4 24 - get current scanline's playfield pattern sta PF2 ; 3 27 - @60-39 ; precalculate data that's needed for line 2 of the 2LK ldx #1 ; 2 29 - D1=0, so ball will be off lda #BOX_HEIGHT-1 ; 2 31 - height of box graphic dcp BallDraw ; 5 36 - Decrement BallDraw and compare with height bcs DoEnabl ; 2 38 - (3 39) if Carry is Set, then ball is on current scanline .byte $24 ; 3 41 - $24 = BIT with zero page addressing, trick that ; causes the inx to be skipped DoEnabl: ; 39 - from bcs DoEnablPre inx ; 2 41 - D1=1, so ball will be ON lda #HUMAN_HEIGHT-1 ; 2 43 - height of the box graphics, dcp Player0Draw ; 5 48 - Decrement Player0Draw and compare with height bcs DoDrawGrp0 ; 2 50 - (3 51) if Carry is Set then player0 is on current scanline lda #0 ; 2 52 - otherwise use 0 to turn off player0 .byte $2C ; 4 56 - $2C = BIT with absolute addressing, trick that ; causes the lda (Player0Ptr),y to be skipped DoDrawGrp0: ; 51 - from bcs DoDrawGRP0 lda (Player0Ptr),y ; 5 56 - load the shape for player0 sta WSYNC ; 3 59 ;--------------------------------------- ; start of line 2 of the 2LK sta GRP0 ; 3 3 - @0-22, update player0 graphics stx ENABL ; 3 6 - @0-22, update ball graphics dey ; 2 8 - decrease the 2LK loop counter bne ArenaLoop ; 2 10 - (3 11) branch if there's more Arena to draw sty PF0 ; 3 13 - Y is 0, blank out playfield sty PF1 ; 3 16 - Y is 0, blank out playfield sty PF2 ; 3 19 - Y is 0, blank out playfield rts ; 6 25 - ReTurn from Subroutine I then modified RandomLocation to set all objects to the same location for comparision: RandomLocation: ... ; for alignment test, set to (100, 100) lda #100 sta ObjectX,x sta ObjectY,x rts This revealed a minor quirk with TIA - namely that when objects are set to the same X position, missiles and the ball end up 1 pixel to the left of where a player ends up (player1 is the green square and it's directly on top of the red ball). This is a known issue and the solution is to increase the X value by 1 to compensate. I did so by adding this to the end of RandomLocation: RandomLocation: ... cpx #2 bcc RLdone inc ObjectX,x ; missile and ball objects need their X adjusted RLdone: rts And now player1 and the ball line up: The NewGame routine revised last time already sets the ball to a random X-Y location, so all that's left to make it show up is to revise PositionObjects. Two changes are needed, first is to set the X position of the ball object by starting the POloop with X=4 (in the prior build X was initialized to 1), then prep a new variable BallDraw that's used by the 2LK to draw the ball object on the correct scanlines. PositionObjects: ldx #4 ; position all objects POloop lda ObjectX,x ; get the object's X position jsr PosObject ; set coarse X position and fine-tune amount dex ; DEcrement X bpl POloop ; Branch PLus so we position all objects sta WSYNC ; wait for end of scanline sta HMOVE ; use fine-tune values to set final X positions ... ; prep ball's Y position for 2LK ldx #1 ; preload X for setting VDELBL lda ObjectY+4 ; get the balls's Y position clc adc #1 ; add 1 to compensate for priming of ball lsr ; divide by 2 for the 2LK position sta Temp ; save for position calculations bcs NoDelayBL ; if carry is set we don't need Vertical Delay stx VDELBL ; carry was clear, so set Vertical Delay NoDelayBL: ; BallDraw = ARENA_HEIGHT + BOX_HEIGHT - Y position + 1 ; the + 1 compensates for priming of ENABL lda #(ARENA_HEIGHT + BOX_HEIGHT + 1) sec sbc Temp sta BallDraw rts Lastly, Overscan's been updated to process collisions with the ball. OverScan: ... TestCollisions: ; Test left player collisions ... bit CXP0FB ; N = player0/playfield, V=player0/ball ... notP0PF: ; oVerflow flag is not affected by lda or sta bvc notP0BL ; if V is off, then player0 did not collide with ball ldx #4 ; which box was collected jsr CollectBox ; update score and reposition box notP0BL: ... RightPlayer: ; Test right player collisions ... bit CXP1FB ; N = player1/playfield, V=player1/ball ... notP1PF: ; oVerflow flag is not affected by lda or sta bvc notP1BL ; if V is off, then player1 did not collide with ball ldx #4 ; which box was collected jsr CollectBox ; update score and reposition box notP1BL: 2 player games are now playable: and 1 player games have 2 boxes to collect: ROM collect_20140711.bin Source Collect_20140711.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  12. And yes, those are air quotes Inside the Atari (and computers in general), there's no such thing as a Random Number. We can, however, simulate random numbers by using a Linear Feedback Shift Register. The LFSR we're going to use was posted by batari, it's a rather slick bit of code in that it can be either an 8-bit or 16-bit LFSR. Random: lda Rand8 lsr ifconst Rand16 rol Rand16 ; this command is only used if Rand16 has been defined endif bcc noeor eor #$B4 noeor: sta Rand8 ifconst Rand16 eor Rand16 ; this command is only used if Rand16 has been defined endif rts LFSR's create what appear to be a random sequence of numbers, but they're not. A properly constructed 8-bit LFSR will start repeating values after 255 values have been obtained. All you need to do in order to use the above routine as an 8-bit LFSR is allocate a variable called Rand8: ; used by Random for an 8 bit random number Rand8: ds 1 ; stored in $AB A 16-bit LFSR will repeat after 65535 values have been obtained - that's a lot better than 255; however, it requires you to allocate another variable. With the Atari's limited 128 bytes of RAM, games often didn't have RAM to spare so they'd just use an 8-bit LFSR. Collect is a rather simple game though, so we have RAM to spare and will allocate the second variable Rand16. ; optionally define space for Rand16 for 16 bit random number Rand16: ds 1 ; stored in $AC The random number generator does have potential problem to be aware of - if you're using an 8-bit LFSR and Rand8 has the value of 0 then the LFSR will always return 0. Likewise for the 16-bit LFSR if both Rand8 and Rand16 are 0 it will always return 0. So we need to initialize the LFSR as CLEAN_START set all the RAM variables to 0. InitSystem: CLEAN_START ... ; seed the random number generator lda INTIM ; unknown value sta Rand8 ; use as seed eor #$FF ; both seed values cannot be 0, so flip the bits sta Rand16 ; just in case INTIM was 0 One thing that helps when using an LFSR is to keep reading values at a regular rate, even if you don't need the value. What this does is impose an element outside of the Atari's control - namely the time it takes the human to do things: hit the RESET switch to start a game, collect the next box, etc. I've added this to VerticalBlank VerticalBlank: jsr Random ... Now that we have a function for random numbers we need to use them. First up is new routine that will randomly position any object. To use this function we just need to call it with X register holding a value that denotes which object to position. The values are the same ones used for the PosObject function, namely 0=player0, 1=player1, 2=missile0, 3=missile1 and 4=ball. RandomLocation: jsr Random ; get a random value between 0-255 and #127 ; limit range to 0-127 sta Temp ; save it jsr Random ; get a random value between 0-255 and #15 ; limit range to 0-15 clc ; must clear carry for add adc Temp ; add in random # from 0-127 for range of 0-142 adc #5 ; add 5 for range of 5-147 sta ObjectX,x ; save the random X position jsr Random ; get a random value between 0-255 and #127 ; limit range to 0-127 sta Temp ; save it jsr Random ; get a random value between 0-255 and #15 ; limit range to 0-15 clc ; must clear carry for add adc Temp ; add in random # from 0-127 for range of 0-142 adc #26 ; add 26 for range of 26-168 sta ObjectY,x ; save the random Y position rts I've renamed InitPos to NewGame and modified it to call RandomLocation. It runs through a loop setting all the box objects. Starting X value will be 1 or 2 based on the number of players in the selected game variation. NewGame: ... ; Randomly position the boxes for the new game. Set X to 1 for a 1 player ; game or 2 for a 2 player game so that the appropriate objects will be ; randomly placed in the Arena. lda Variation and #1 ; value of 0=1 player game, 1=2 player game tax ; transfer to X inx ; start with 1 for a 1 player game, or 2 for a 2 player game IPloop: jsr RandomLocation ; randomly position object specified by X inx ; increase X for next object cpx #5 ; check if we hit 5 bne IPloop ; branch back if we haven't ... rts I also modified OverScan so that during a 1-player variation a collision between player0 and player1 will be detected. If so, it calls another new function, CollectBox. OverScan: ... bit Players ; test how many players are in this game variation bmi RightPlayer ; test Right Player collisions if its a 2 player game bit CXPPMM ; else see if left player collected box drawn by player1 bpl OSwait ; player0 did not collide wth player1 ldx #1 ; which box was collected jsr CollectBox ; update score and reposition box jmp OSwait ; 1 player game, so skip Right Player test... CollectBox will increase the player's score and call RandomLocation again to move it to a new position. When CollectBox is called, Y must hold which player (0 for left, 1 for right) and X must hold the object that was collected. CollectBox: SED ; SEt Decimal flag clc ; CLear Carry bit lda #1 ; 1 point per box adc Score,y ; add to player's current score sta Score,y ; and save it CLD ; CLear Decimal flag jsr RandomLocation ; move box to new location rts CollectBox means that the game is now playable for 1-player games! I managed to score 26 on game variation 1, how well can you do? One thing you might notice in that screenshot is that the right player's score is not visible. Since we're done using the score for diagnostics, I've made a few changes to the digit graphics: The first change is the left 0 image was blanked out - this provides leading zero suppression The second change is the A image is now blanked out - NewGame uses this to blank out the right player's score in 1 player games. NewGame: ... ; reset scores ldx #0 stx Score bit Players ; check # of players bpl BlankRightScore stx Score+1 rts BlankRightScore: lda #$AA ; AA defines a "space" character sta Score+1 rts Lastly the graphics for B thru F have been removed to save ROM space. ROM collect_20140710.bin Source Collect_20140710.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  13. It's common for Atari games to have a number of game variations. To simplify the logic, the variations are usually driven by individual bits and/or groups of bits within a single byte that holds the game variation. A good example of that would be Space Invaders - check out the game matrix from the manual: A byte is comprised of 8 bits, usually numbered 0-7 where 7 is the leftmost bit as in 76543210. For Space Invaders the bits in the game variation are used in this fashion: 7 - not used 654 - selects 1 of 7 player variations 3 - Invisible Invaders 2 - Fast Bombs 1 - ZigZagging Bombs 0 - Moving Shields Humans start counting at 1, but computers start at 0, so if you select game variation 12, internally it's really 11. 11 in binary is %00001011, which means bits 0, 1 and 3 are all turned on so the game variation has Invisible Invaders, ZigZagging Bombs and Moving Shields. You can confirm that by looking at column 12 of the Game Matrix above. With this update to Collect, we're using bits 1 and 0 to give us 4 game variations: 765432 - not used 1 - Arena 1 or 2 0 - # of players If we have space at the end of the project I plan to add some additional arenas. If we can add 2 more we'd just start using bit 2 and let the game Variation go from 1-8 76543 - not used 21 - Arena 1, 2, 3 or 4 0 - # of players If we can add 6 more we'd add bit 3 and let the game variation go from 1-16 76543 - not used 321 - Arena 1, 2, 3, 4, 5, 6, 7 or 8 0 - # of players The ProcessSwitches routine has been modified so that hitting Select will increment the new variable Variation. It will also limit Variation to only the values 0-3. After changing Variation, the left score will be set to show Variation+1 as Humans prefer to see 1-4 instead of 0-3. The Right Score will be used to show the # of players, either 1 or 2. NotReset: lsr ; D1 is now in C bcs NotSelect ; if D1 was on, the SELECT switch was not held lda #0 sta GameState ; clear D7 to signify Game Over ldx Variation ; Get the Game Variation inx ; and increase it txa ; transfer it to A and #%00000011 ; limit Variation to 0-3 sta Variation ; save it tax ; transfer it to X inx ; and increase it by 1 for the human readable varation 1-4 stx Score ; save in Score so it shows on left side ldy #1 ; default to showing 1 player variation lsr ; D0 of Variation, # of players, now in Carry flag bcc Not2 ; if Carry is clear, then show 1 player iny ; else set Y to 2 to show 2 players Not2: ror Players ; put Carry into D7 for BIT testing of # of players sty Score+1 ; show the human readable # of players on right side NotSelect: rts The routine works, but when Select is pressed the game variation will rapidly change making it difficult to select a specific game variation. You can see that in this build: collect_20140709_nodelay.bin To fix that, we'll add a SelectDelay variable so that holding down SELECT will only result in Variation changing at the rate of once per second. However, if the user rapidly presses/releases SELECT then Variation will also rapidly change. ProcessSwitches: lda SWCHB ; load in the state of the switches lsr ; D0 is now in C bcs NotReset ; if D0 was on, the RESET switch was not held jsr InitPos ; Prep for new game lda #%10000000 sta GameState ; set D7 on to signify Game Active bne NotSelect ; clear SelectDelay NotReset: lsr ; D1 is now in C bcs NotSelect ; if D1 was on, the SELECT switch was not held lda #0 sta GameState ; clear D7 to signify Game Over lda SelectDelay ; do we need to delay the Select switch? beq SelectOK ; if delay is 0 then no dec SelectDelay ; else decrement the delay rts ; and exit the subroutine SelectOK: lda #60 ; Set the Select Delay to 1 second sta SelectDelay ; ldx Variation ; Get the Game Variation inx ; and increase it txa ; transfer it to A and #%00000011 ; limit Variation to 0-3 sta Variation ; save it tax ; transfer it to X inx ; and increase it by 1 for the human readable varation 1-4 stx Score ; save in Score so it shows on left side ldy #1 ; default to showing 1 player variation lsr ; D0 of Variation, # of players, now in Carry flag bcc Not2 ; if Carry is clear, then show 1 player iny ; else set Y to 2 to show 2 players Not2: ror Players ; put Carry into D7 for BIT testing of # of players sty Score+1 ; show the human readable # of players on right side rts NotSelect: lda #0 ; clears SelectDelay if SELECT not held sta SelectDelay rts The routine PositionObjects has been modified to use a Box Graphic for player1 if a 1 player game has been selected: PositionObjects: ... lda Variation ; get the game variation and #1 ; and find out if we're 1 or 2 player tax ; Player1Ptr = BoxGfx + HUMAN_HEIGHT - 1 - Y position lda ShapePtrLow,x sec sbc Temp sta Player1Ptr lda ShapePtrHi,x sbc #0 sta Player1Ptr+1 rts ShapePtrLow: .byte <(BoxGfx + HUMAN_HEIGHT - 1) .byte <(HumanGfx + HUMAN_HEIGHT - 1) ShapePtrHi: .byte >(BoxGfx + HUMAN_HEIGHT - 1) .byte >(HumanGfx + HUMAN_HEIGHT - 1) The Kernel has also been modified so that the correct Arena will be drawn. A little bit after TimerBar: you'll find this: TimerBar: ... lda Variation ; 3 20 lsr ; 2 22 - which Arena to show tay ; 2 24 - set for index ldx ArenaOffset,y ; 4 28 - set X for which arena to draw lda ArenaPF0,x ; 4 32 - reflect and priority for playfield and #%00000111 ; 2 34 - get the lower 3 bits for CTRLPF ora #%00110000 ; 2 36 - set ball to display as 8x pixel sta CTRLPF ; 3 39 ... ArenaOffset: .byte 0 ; Arena 1 .byte 22 ; Arena 2 The lsr command shifts bit 1 down to bit 0 so that we end up with 0 or 1 for the Arena number. That's used to set X to either 0 or 22 via the command ldx ArenaOffset,y. I also added code to update CTRLPF based on the first PF0 data byte for the selected Arena. CTRLPF uses its bits like this: 76 - not used 54 - set width of BALL object 3 - not used 2 - Playfield Priority 1 - Score Mode 0 - Reflected Playfield Since PF0 only uses bits 7654, also known as the upper nybble of the byte, we can use the lower nybble to hold extra information to specify whether or not the selected Arena uses Playfield Priority (as opposed to Player Priority) or has a Reflected Playfield(as opposed to Repeated Playfield). We could even specify Score Mode which would just color the two sides of the playfield to match the colors of the players (like in the score display). ArenaPF0: ; PF0 is drawn in reverse order, and only the upper nybble .byte %11110001 ; Arena 1 lower nybble controls playfield, set for REFLECT .byte %00010000 .byte %00010000 .byte %00010000 ... .byte %11110100 ; Arena 2 - lower nybble controls playfield, set for PLAYFIELD PRIORITY .byte %00010000 .byte %00010000 .byte %00010000 Game Variation 2, Arena 1 with 2 players. Arena 1 features Reflected Playfield and Player Priority Game Variation 3, Arena 2 with 1 player. Arena 2 features Repeated Playfield and Playfield Priority Look at the left Humanoid's head in each screenshot to see the difference that setting Playfield Priority makes. You might remember this being used in some games like Combat where the planes go behind the clouds. Just for fun, here's Arena 2 with SCORE mode set (I've moved the players to the side of the screen they didn't start on): The code change for that is: .byte %11110010 ; Arena 2 - lower nybble controls playfield, set for SCORE ROM collect_20140709.bin Source Collect_20140709.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  14. SpiceWare

    CollectMini

    The Houston Arcade Expo is less than a month away! I'll be giving my presentation Saturday November 14th, at 7pm and, as always, I like to revise the presentation with something new. Last summer I started the series Collect with plans to use it in my presentation at the Classic Game Fest over in Austin. I eventually realized it was too much information for a 1 hour presentation; so instead, I expanded the game's concept and just added a few screenshots to the presentation with the suggestion to check it out on my blog. For this go around I decided to take the original concept for Collect and strip it down even further - just the player and a box to collect: When you collect the box it will move to a new location. Since there's no score display, the background will increment to the next color - of course this doesn't work well for PAL Atari's, 4 of the "scores" will be white. There's no animation in this stripped down version, though I do show how to use the REFP0 register to flip the graphics. The game is finished; though, I still need to make the source "presentation ready", so today I'm only releasing the ROM: CollectMini.bin COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  15. Static images that just slide around the screen work OK, but we can do better - so for this update we'll add a couple new images so we can animate the players as they run around the arena. While you can have as many frames of animation as you'd like, the code is most efficient if the number of frames is a power of 2 (2, 4, 8, 16, etc). The code that cycles through 4 frames is this: Example: inc frame lda frame and #3 ; limits values from 0-3, if A was 4 it becomes 0 after the and sta frame Just change the #3 to #7 (8 frames), #15 (16 frames) and so on. If you wanted to cycle thru a non-power of 2 count, say for example 5 frames, the code would look like this: Example: ldx frame inx cpx #5 bne save ldx #0 save: stx frame For Collect we're going to use 4 frames. You might be wondering why there's only 3 humanoid images - it's because we're going to use 1 of the images twice: HumanGfx HumanRunning0 HumanRunning1 HumanRunning0 In order to animate the players we'll need to keep track of which frame they're showing, so let's add 2 new RAM variables: ; indexes for player animation sequences Animation0: ds 1 ; stored in $B3 Animation1: ds 1 ; stored in $B4 Then modify PositionObjects so it will animate the images when it preps the variables for the 2LK, but only when the player is in motion: PositionObjects: ... ; select image to show for Player0 lda ObjectX ; get current X location for player 0 cmp SavedX ; compare with prior X location bne Animate0 ; if different, animate player 0 lda ObjectY ; otherwise check current Y location cmp SavedY ; against prior Y location bne Animate0 ; and animate player 0 if they're different lda #0 ; if X and Y didn't change then select 0, the beq SaveFrame0 ; stationary image, and save it Animate0: inc Animation0 ; increment to select the next frame lda Animation0 ; load it and #3 ; limit to 0-3 (if it was 4, it's now 0) SaveFrame0: sta Animation0 ; save it tax ; Transfer A to X ; Player0Ptr = HumanGfx + HUMAN_HEIGHT - 1 - Y position lda ShapePtrLow,x ; select image as specified in X sec sbc Temp sta Player0Ptr lda ShapePtrHi,x ; select image as specified in X sbc #0 sta Player0Ptr+1 ... ShapePtrLow: .byte <(HumanGfx + HUMAN_HEIGHT - 1) .byte <(HumanRunning0 + HUMAN_HEIGHT - 1) .byte <(HumanRunning1 + HUMAN_HEIGHT - 1) .byte <(HumanRunning0 + HUMAN_HEIGHT - 1) .byte <(BoxGfx + HUMAN_HEIGHT - 1) ShapePtrHi: .byte >(HumanGfx + HUMAN_HEIGHT - 1) .byte >(HumanRunning0 + HUMAN_HEIGHT - 1) .byte >(HumanRunning1 + HUMAN_HEIGHT - 1) .byte >(HumanRunning0 + HUMAN_HEIGHT - 1) .byte >(BoxGfx + HUMAN_HEIGHT - 1) The code for player 1 is almost the same, though it adds a test so the box image will be displayed for one player game variations: ; select image to show for Player1 bit Players bpl UseBoxImage ; if 1 player game then draw the box lda ObjectX+1 ; get current X location for player 1 cmp SavedX+1 ; compare with prior X location bne Animate1 ; if different, animate player 1 lda ObjectY+1 ; otherwise check current Y location cmp SavedY+1 ; against prior Y location bne Animate1 ; and animate player 1 if they're different lda #0 ; if X and Y didn't change then select 0, the beq SaveFrame1 ; stationary image, and save it Animate1: inc Animation1 ; increment to select the next frame lda Animation1 ; load it and #3 ; limit to 0-3 (if it was 4, it's now 0) SaveFrame1: sta Animation1 ; save it tax ; Transfer A to X .byte $2C ; $2C = BIT with absolute addressing, trick that ; causes the ldx #4 to be skipped over UseBoxImage: ldx #4 ; select the Box Image ; Player1Ptr = BoxGfx + HUMAN_HEIGHT - 1 - Y position lda ShapePtrLow,x ; select image as specified in X sec sbc Temp sta Player1Ptr lda ShapePtrHi,x ; select image as specified in X sbc #0 sta Player1Ptr+1 It works, but the players move so fast they look spastic. collect_20140714_toofast.bin To fix that, well revise it to use an image over multiple frames. For testing, we'll make the left player use each image for 2 frames while the right uses each image for 4: Animate0: inc Animation0 ; increment to select the next frame lda Animation0 ; load it and #7 ; limit to 0-7 (if it was 8, it's now 0) SaveFrame0: sta Animation0 ; save it lsr ; divide by 2 for 0-3 - this means we show the same ; image twice in succession tax ; Transfer A to X ... Animate1: inc Animation1 ; increment to select the next frame lda Animation1 ; load it and #15 ; limit to 0-15 (if it was 16, it's now 0) SaveFrame1: sta Animation1 ; save it lsr ; divide by 4 for 0-3 - this means we show the same lsr ; image four times in succession tax ; Transfer A to X collect_20140714_speedtest.bin Both look OK, though I think the left player looks a little better so the final version will use each image twice. One minor thing happens when the game is over - if the players were in motion, the animation keeps on going even though the players are no longer in motion. To fix that, well add a Game Over check (same logic was added for Player1) that will select the stationary image: ; select image to show for Player0 bit GameState bpl StopAnimation0 ; if game is inactive, stop animation lda ObjectX ; get current X location for player 0 cmp SavedX ; compare with prior X location bne Animate0 ; if different, animate player 0 lda ObjectY ; otherwise check current Y location cmp SavedY ; against prior Y location bne Animate0 ; and animate player 0 if they're different StopAnimation0: lda #0 ; if X and Y didn't change then select 0, the beq SaveFrame0 ; stationary image, and save it ROM collect_20140714.bin Source: Collect_20140714.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  16. First things first - head over to MiniDig - best of stella and download the Stella Programmer's Guide from the docs page. I've also attached it to this blog entry, but you should still check out what's available over at MiniDig. Also, for this tutorial you'll need to have DASM to compile the code as well as Stella and/or a real 2600 with a Harmony cart to run your code. The heart of the Atari is TIA, the Television Interface Adaptor. It's the video chip, sound generator, and also handles some of the controller input. As a video chip, TIA is very unusual. Most video game systems have memory that holds the current state of the display. Their video chip reads that memory and uses that information to generate the display. But not TIA - memory was very expensive at the time, so TIA was designed with a handful of registers that contain just the information needed to draw a single scanline. It's up to our program to change those registers in realtime so that each scanline shows what its supposed to. It's also up to our program to generate a "sync signal" that tells the TV when its time to start generating a new image. Turn to page 2 of the programmer's guide. You'll find the following diagram, which I've slightly modified: The Horizontal Blank is part of each scanline, so we don't need to worry about generating it. Everything else though is up to us! We need to generate a sync signal over 3 scanlines, after which we need to wait 37 scanlines before we tell TIA to "turn on" the image output. After that we need to update TIA so each of the 192 scanlines that comprise visible portion of the display show what they're supposed to. Once that's done, we "turn off" the image output and wait 30 scanlines before we start all over again. In the source code, available below, you can see the Main loop which follows the diagram: Main: jsr VerticalSync ; Jump to SubRoutine VerticalSync jsr VerticalBlank ; Jump to SubRoutine VerticalBlank jsr Kernel ; Jump to SubRoutine Kernel jsr OverScan ; Jump to SubRoutine OverScan jmp Main ; JuMP to Main Each of the subroutines handles what's needed, such as this section which generates the sync signal: VerticalSync: lda #2 ; LoaD Accumulator with 2 so D1=1 sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) sta VSYNC ; Accumulator D1=1, turns on Vertical Sync signal sta WSYNC ; Wait for Sync - halts CPU until end of 1st scanline of VSYNC sta WSYNC ; wait until end of 2nd scanline of VSYNC lda #0 ; LoaD Accumulator with 0 so D1=0 sta WSYNC ; wait until end of 3rd scanline of VSYNC sta VSYNC ; Accumulator D1=0, turns off Vertical Sync signal rts ; ReTurn from Subroutine Currently there's no game logic, so the VerticalBlank just waits for the 37 scanlines to pass: VerticalBlank: ldx #37 ; LoaD X with 37 vbLoop: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) dex ; DEcrement X by 1 bne vbLoop ; Branch if Not Equal to 0 rts ; ReTurn from Subroutine The Kernel is the section of code that draws the screen. Kernel: ; turn on the display sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda #0 ; LoaD Accumulator with 0 so D1=0 sta VBLANK ; Accumulator D1=1, turns off Vertical Blank signal (image output on) ; draw the screen ldx #192 ; Load X with 192 KernelLoop: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) stx COLUBK ; STore X into TIA's background color register dex ; DEcrement X by 1 bne KernelLoop ; Branch if Not Equal to 0 rts ; ReTurn from Subroutine For this initial build it just changes the background color so we can see that we're generating a stable picture: Like Vertical Blank, OverScan doesn't have anything to do besides turning off the image output, so it just waits for enough scanlines to pass so that the total scanline count is 262. OverScan: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) lda #2 ; LoaD Accumulator with 2 so D1=1 sta VBLANK ; STore Accumulator to VBLANK, D1=1 turns image output off ldx #27 ; LoaD X with 27 osLoop: sta WSYNC ; Wait for SYNC (halts CPU until end of scanline) dex ; DEcrement X by 1 bne osLoop ; Branch if Not Equal to 0 rts ; ReTurn from Subroutine Anyway, download the source and take a look - there's comments galore. ROM collect_20140624.bin Source Collect_20140624.zip Stella Programmer's Guide Stella Programmers Guide.pdf Addendum on Keynote - what the audience sees: What I see on the iPad: COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  17. Here's the source code for CollectMini. CollectMini.zip It's a little different from the source available on my site as I've made some minor changes to the comments. The presentation was received well; though going over the code, even simple as it is, caused me to run short on time. So I'll need to come up with a new plan of action for next time. COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  18. For this update, we're going to double the Y range of the player objects. To use the new Y value for the 2LK we just need to divide it in half using the LSR command. The remainder of the divide, which ends up in the Carry flag, will conveniently tell us if we need to turn on Vertical Delay. This routine preps the 2LK data for player0 and turns on VDELP0 if required (if you're wondering, VDELP0 is turned off in VerticalSync): ; prep Humanoid's Y position for 2LK ldx #1 ; preload X for setting VDELPx lda ObjectY ; get the human's Y position lsr ; divide by 2 for the 2LK position sta Temp ; save for position calculations bcs NoDelay0 ; if carry is set we don't need Vertical Delay stx VDELP0 ; carry was clear, so set Vertical Delay NoDelay0: ; HumanDraw = ARENA_HEIGHT + HUMAN_HEIGHT - Y position lda #(ARENA_HEIGHT + HUMAN_HEIGHT) sec sbc Temp sta HumanDraw ; HumanPtr = HumanGfx + HUMAN_HEIGHT - 1 - Y position lda #<(HumanGfx + HUMAN_HEIGHT - 1) sec sbc Temp sta HumanPtr lda #>(HumanGfx + HUMAN_HEIGHT - 1) sbc #0 sta HumanPtr+1 One minor problem with the prior 2LK was that player1 could not show up on the topmost scanline of the Arena: Closeup: To fix this, we'll modify the kernel to prime GRP1 before it enters the loop that draws the Arena: ldy #ARENA_HEIGHT+1 ; 2 7 - the arena will be 180 scanlines (from 0-89)*2 ; prime GRP1 so player1 can appear on topmost scanline of the Arena lda #BOX_HEIGHT-1 ; 2 9 - height of the box graphics, dcp BoxDraw ; 5 14 - Decrement BoxDraw and compare with height bcs DoDrawGrp1pre ; 2 16 - (3 17) if Carry is Set, then box is on current scanline lda #0 ; 2 18 - otherwise use 0 to turn off player1 .byte $2C ; 4 22 - $2C = BIT with absolute addressing, trick that ; causes the lda (BoxPtr),y to be skipped DoDrawGrp1pre: ; 17 - from bcs DoDrawGRP1pre lda (BoxPtr),y ; 5 22 - load the shape for the box sta GRP1 ; 3 25 - @0-22, update player1 to draw box dey ; 2 27 ArenaLoop: ; 13 - from bpl ArenaLoop The 2LK calculations for player1 used to be the same as for player0, but now must be modified to compensate for the priming of GRP1: ; prep box's Y position for 2LK lda ObjectY+1 ; get the box's Y position clc adc #1 ; add 1 to compensate for priming of GRP1 lsr ; divide by 2 for the 2LK position sta Temp ; save for position calculations bcs NoDelay1 ; if carry is set we don't need Vertical Delay stx VDELP1 ; carry was clear, so set Vertical Delay NoDelay1: ; BoxDraw = ARENA_HEIGHT + BOX_HEIGHT - Y position + 1 ; the + 1 compensates for priming of GRP1 lda #(ARENA_HEIGHT + BOX_HEIGHT +1) sec sbc Temp sta BoxDraw ; BoxPtr = BoxGfx + BOX_HEIGHT - 1 - Y position lda #<(BoxGfx + BOX_HEIGHT - 1) sec sbc Temp sta BoxPtr lda #>(BoxGfx + BOX_HEIGHT - 1) sbc #0 sta BoxPtr+1 Added GRP1 priming which allows player1 to cover full Arena: Closeup: Lastly, I added a new Box graphic for player1 ROM collect_20140704.bin Source Collect_20140704.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
  19. Let's review the TIA Timing diagram from last time: We used that to determine when we could safely update the playfield data in order to draw the score and timer. For moveable objects(player0, player1, missile0, missile1 and ball) if you update their graphics during the Visible Screen (cycles 23-76) you run the risk of shearing. For something that's moving fast, like the snowball in Stay Frosty 2, shearing may be an acceptable design compromise: That snowball should be square, but the left edge has sheared due to the ball object being updated mid-scanline. To prevent shearing we need to update the objects on cycles 0-22. There's a lot of calculations to be done in the kernel to draw just one player. For Collect I'm using DoDraw, which looks like this for drawing player0: DoDraw0: lda #HUMAN_HEIGHT-1 ; 2 2 - height of the humanoid graphics, subtract 1 due to starting with 0 dcp HumanDraw ; 5 7 - Decrement HumanDraw and compare with height bcs DoDrawGrp0 ; 2 9 - (3 10) if Carry is Set, then humanoid is on current scanline lda #0 ; 2 11 - otherwise use 0 to turn off player0 .byte $2C ; 4 15 - $2C = BIT with absolute addressing, trick that ; causes the lda (HumanPtr),y to be skipped DoDrawGrp0: ; 10 - from bcs DoDrawGrp0 lda (HumanPtr),y ; 5 15 - load the shape for player0 sta GRP0 ; 3 18 - update player0 to draw Human That's 18 cycles to draw a single player. One way to make it easier to fit all the code in is to use a 2 Line Kernel (2LK). In a 2LK we update TIA's registers over 2 scanlines in order to build the display. For Collect, the current routines are updating them like this: player0, playfield player1, playfield The actual code looks like this: ldy #ARENA_HEIGHT ; 2 7 - the arena will be 180 scanlines (from 0-89)*2 ArenaLoop: ; 13 - from bpl ArenaLoop ; continuation of line 2 of the 2LK ; this precalculates data that's used on line 1 of the 2LK lda #HUMAN_HEIGHT-1 ; 2 15 - height of the humanoid graphics, subtract 1 due to starting with 0 dcp HumanDraw ; 5 20 - Decrement HumanDraw and compare with height bcs DoDrawGrp0 ; 2 22 - (3 23) if Carry is Set, then humanoid is on current scanline lda #0 ; 2 24 - otherwise use 0 to turn off player0 .byte $2C ; 4 28 - $2C = BIT with absolute addressing, trick that ; causes the lda (HumanPtr),y to be skipped DoDrawGrp0: ; 23 - from bcs DoDrawGrp0 lda (HumanPtr),y ; 5 28 - load the shape for player0 sta WSYNC ; 3 31 ;--------------------------------------- ; start of line 1 of the 2LK sta GRP0 ; 3 3 - @ 0-22, update player0 to draw Human ldx #%11111111 ; 2 5 - playfield pattern for vertical alignment testing stx PF0 ; 3 8 - @ 0-22 ; precalculate data that's needed for line 2 of the 2LK lda #HUMAN_HEIGHT-1 ; 2 10 - height of the humanoid graphics, dcp BoxDraw ; 5 15 - Decrement BoxDraw and compare with height bcs DoDrawGrp1 ; 2 17 - (3 18) if Carry is Set, then box is on current scanline lda #0 ; 2 19 - otherwise use 0 to turn off player1 .byte $2C ; 4 23 - $2C = BIT with absolute addressing, trick that ; causes the lda (BoxPtr),y to be skipped DoDrawGrp1: ; 18 - from bcs DoDrawGRP1 lda (BoxPtr),y ; 5 23 - load the shape for the box sta WSYNC ; 3 26 ;--------------------------------------- ; start of line 2 of the 2LK sta GRP1 ; 3 3 - @0-22, update player1 to draw box ldx #0 ; 2 5 - PF pattern for alignment testing stx PF0 ; 3 8 - @0-22 dey ; 2 10 - decrease the 2LK loop counter bpl ArenaLoop ; 2 12 - (3 13) branch if there's more Arena to draw If you look at that closely, you'll see I'm splitting DoDraw a bit so that this is how the 2LK works: updates player0, playfield, precalc player1 for line 2 updates player1, playfield, precalc player0 for line 1 By pre-calculating data during the visible portion of the scanline, we'll have more time during the critical 0-22 cycles for when we add the other objects. Since we're updating the players on every other scanline, each byte of graphic data is displayed twice (compare the thickness of the humanoid pixels with the red lines drawn with the playfield). Also, the players never line up as they're never updated on the same scanlines: closeup: The designers of TIA planned for this by adding a Vertical Delay feature to the players and ball (though sadly not the missiles). The TIA registers for this are VDELP0, VDELP1 and VDELBL. For this update to Collect, I've tied the Vertical Delay to the difficulty switches, putting the switch in position A will turn on the delay for that player so we can experiment with how that works. For the next update I'll set the Vertical Delay based on the Y position of the player (this also means the maximum Y value will be double that of this build). Left Difficulty A, Right Difficulty B so VDELP0 = 1 and VDELP1 = 0. Sprites line up with the same Y closeup: Left Difficulty B, Right Difficulty A so VDELP0 = 0 and VDELP1 = 1. Sprites line up when player1's Y = player0's Y + 1 Closeup: The code that preps the data used by DoDraw looks like this: ; HumanDraw = ARENA_HEIGHT + HUMAN_HEIGHT - Y position lda #(ARENA_HEIGHT + HUMAN_HEIGHT) sec sbc ObjectY sta HumanDraw ; HumanPtr = HumanGfx + HUMAN_HEIGHT - 1 - Y position lda #<(HumanGfx + HUMAN_HEIGHT - 1) sec sbc ObjectY sta HumanPtr lda #>(HumanGfx + HUMAN_HEIGHT - 1) sbc #0 sta HumanPtr+1 ; BoxDraw = ARENA_HEIGHT + HUMAN_HEIGHT - Y position lda #(ARENA_HEIGHT + HUMAN_HEIGHT) sec sbc ObjectY+1 sta BoxDraw ; BoxPtr = HumanGfx + HUMAN_HEIGHT - 1 - Y position lda #<(HumanGfx + HUMAN_HEIGHT - 1) sec sbc ObjectY+1 sta BoxPtr lda #>(HumanGfx + HUMAN_HEIGHT - 1) sbc #0 sta BoxPtr+1 ... HumanGfx: .byte %00011100 .byte %00011000 .byte %00011000 .byte %00011000 .byte %01011010 .byte %01011010 .byte %00111100 .byte %00000000 .byte %00011000 .byte %00011000 HUMAN_HEIGHT = * - HumanGfx The graphics are much easier to see using my mode file for jEdit: I'm sure some of you are wondering why the human graphics are upside down. If you wanted to loop thru something 10 times, you'd normally think to write the code like this: ldy #0 Loop: ; do some work iny cpy #10 bne Loop But the 6507 does an automatic check for 0 (as well as positive/negative) which lets you save 2 cycles of processing time by eliminating the CPY command: ldy #10 Loop: ; do some work dey bne Loop Alternatively, if your initial value is less than 128, you can use this: ldy #(10-1) Loop: ; do some work dey bpl Loop Making the loop count down instead of up saves 2 cycles, but doing so requires the graphics to be upside down. 2 cycles doesn't sound like much, but in a scanline that's 2.6% of your processing time and saving it might be what allows you to update everything you want. In Kernels I've written, I often use every cycle - and that includes eliminating the sta WSYNC to buy back 3 cycles of processing time. See the reposition kernels in this post about Draconian. I've also added joystick support that will let you move around the players. Pressing FIRE will slow down the movement, making it easier to line things up. The score (on the left) is used to display player0's Y position, and the timer is used for player1. As an added bonus, I'm showing how you can save ROM space by creating graphics that only face in one direction by using REFP0 and REFP1 (REFlect Player) to make the graphics face the other way. The routine's fairly sizable, so I'm not posting it here so download the source code and check it out! ROM collect_20140703.bin Source Collect_20140703.zip COLLECT TUTORIAL NAVIGATION <PREVIOUS> <INDEX> <NEXT>
×
×
  • Create New...