I am a humble sheep working on my first NES project. I am starting with the Nerdy Nights pong tutorial, which I am sure some of you are familiar with. In taking my pong game a few steps beyond the tutorial, I have run into a PPU-garbage-graphics issue where some of my background tiles are getting overwritten with garbage. I cannot seem to properly solve after a few days effort, so I'm politely requesting some help.
When I want to draw some background tiles in my game logic, I set a flag for the set of tiles I want to draw (ex. line 420). Then, at the beginning of every NMI interrupt (line 161), I call a subroutine called DrawTiles (line 1240). In the subroutine, I check the drawing flags, and depending on which flags are set, I pull specific arrays of BG tile data from ROM and write them to $2007. I do this for menu text, instructive text, and floor tiles used to clear said text. At the start of this routine I disable rendering, and at the end of it, I re-enable rendering, and write $00 twice to $2005 so I don't get unwanted scrolling.
*Most* of the time, this works great. But every so often, when a player scores a point in 2-player mode, and the round resets (which sets a flag to begin blinking instructive text on the screen), my BG has some garbage written to it. It also happens sometimes when I start up the game (using Mesen) but very rarely.
I have tried debugging through Mesen, setting breakpoints, and using the Event viewer, but I cannot quite seem to capture when and why this is happening. I measures the CPU cycles from the start of the NMI to the end of DrawTiles during the heaviest tile drawing it will do, and came out to 1579 cyles, so I feel like I should still be within VBlank, yet I'm almost certain my issues is that I'm sending data to the PPU after rendering has started. I just don't know when or why!
All in all, a minor bug that only occasionally inconveniences the game, but before I can make something bigger and better, I really want to completely understand graphics timing. Thanks for taking the time to read. Any help in solving this issue is greatly appreciated!
PS Please don't bully me for my bad code, I'm just a dumb sheep and can't be held accountable. That said, I'm open to any advice on good coding practices for 6502 ASM, and games in general. Thanks again!
Code: Select all
;MyPong
.inesprg 1 ; 1x 16KB PRG code. Bank 0 and 1: CPU $8000-$BFFF
.ineschr 1 ; 1x 8KB CHR data. Bank 2: PPU $0000
.inesmap 0 ; mapper 0 = NROM, no bank swapping
.inesmir 1 ; background mirroring
;;;;Game Constants;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
SCOREDIGIT1WIN2P = $05 ;In 2P or VSAI, First player to 15 wins
SCOREDIGIT2WIN2P = $01
SCOREDIGIT1WIN1P = $09 ;In 1P, let player play to 99 Points
SCOREDIGIT2WIN1P = $09
LEFTGOAL = $08 ;Screen boundary for ball
RIGHTGOAL = $F6
TOPWALL = $0C
BOTTOMWALL = $D8
BOTTOMWALL_PLAYER = $C8 ;Screen boundary for Player
TOPWALL_PLAYER = $08
BALL_X_DIR_LEFT = $01
BALL_X_DIR_RIGHT = $00
BALL_Y_DIR_DOWN = $00
BALL_Y_DIR_UP = $01
BALL_INITSPEED_X = $02
BALL_INITSPEED_Y = $00
P1_X = $10
P2_X = $E8
LASTSCORED_P1 = $00
LASTSCORED_P2 = $01
GAMEMODE_2Player = $01
GAMEMODE_1Player = $00
GAMEMODE_VSAI = $02
PLAYERSPEED_SLOW = $03
PLAYERSPEED_FAST = $05
GAMESTATE_TITLE = $00
GAMESTATE_PLAYING = $01
GAMESTATE_GAMEOVER = $03
GAMESTATE_PAUSED = $04
PLAYSTATE_SERVING = $00
PLAYSTATE_ROUNDINPROGRESS = $01
TEXTTIMER = $44
TEXTTIMERHALF = $22
INPUTDELAYTIMER = $1E
BEEPLENGTH = $14
MAXSPEEDLEVEL = $03
;;;;Variables set in ram;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.rsset $0000 ;Start variables here
pointerLo .rs 1
pointerHi .rs 1
counterLo .rs 1
counterHi .rs 1
debug .rs 1 ;0 = off, 1 = on
gamestate .rs 1 ;Major states get their own INIT sub
gamesubstate .rs 1 ;minor states do not
changeStateRequest .rs 1
gamemode .rs 1 ;1 player, 2 player, or VS
buttons1 .rs 1 ;Reserve 1 byte for controller 1 buttons
buttons2 .rs 1
buttons1OldState .rs 1
buttons2OldState .rs 1
delayInputTimer .rs 1
player1_y .rs 1
player2_y .rs 1
player1_speed .rs 1
player2_speed .rs 1
ball_x_subPx .rs 1
ball_x .rs 1
ball_y_subPx .rs 1
ball_y .rs 1
ball_x_speedSubPx .rs 1
ball_x_speed .rs 1
ball_y_speedSubPx .rs 1
ball_y_speed .rs 1
ball_x_dir .rs 1
ball_y_dir .rs 1
scoreP1Digit1 .rs 1 ;Player1 score
scoreP1Digit2 .rs 1
scoreP2Digit1 .rs 1 ;Player2 score
scoreP2Digit2 .rs 1
lastScored .rs 1 ;Set depending on who scores last, which determines who serves.
textBlinkTimer .rs 1
arg0 .rs 1 ;Generic variables to use as pointers, parameters, and scratch
arg1 .rs 1 ;TODO: Replace pointerLo/Hi and counters
arg2 .rs 1
arg3 .rs 1
arg4 .rs 1
arg5 .rs 1
arg6 .rs 1
arg7 .rs 1
arg8 .rs 1
arg9 .rs 1
scrollLow .rs 1
scrollHigh .rs 1
winScoreD1 .rs 1
winScoreD2 .rs 1
inputDelayTimer .rs 1
beepTimer .rs 1
beepTone .rs 1
beepToneIndex .rs 1
coverMenuTilesFlag .rs 1
blinkTextFlag .rs 1
drawMenuTileFlag .rs 1
hitCount .rs 1 ;Count of how many times ball was hit for X speed index
speedLevel .rs 1
serveSpeed .rs 1
;.rsset $0400 ;Array Buffers
;;;;Initialization;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.bank 0
.org $9000 ;$8000-$FFFF Program ROM
RESET:
SEI ; disable IRQs. all interrupts except the NMI are inhibited.
CLD ; disable decimal mode
LDX #$40
STX $4017 ; disable APU frame IRQ
LDX #$FF
TXS ; Set up stack
INX ; now X = 0
STX $2000 ; disable NMI
STX $2001 ; disable rendering
STX $4010 ; disable DMC IRQs
JSR vblankwait ; First wait for vblank to make sure PPU is ready
clrmem: ;Zero out variable and sprite RAM space
LDA #$00
STA $0000, x
STA $0100, x
STA $0300, x
STA $0400, x
STA $0500, x
STA $0600, x
STA $0700, x
LDA #$FE
STA $0200, x
INX
BNE clrmem
JSR vblankwait ; Second wait for vblank, PPU is ready after this
JSR LoadAssets ; Load palettes, sprites, tile data, etc from ROM into RAM
LDA #$00 ;Set debug here to 1 to turn on some features
STA debug
LDA #$01
STA changeStateRequest ;We're going to call a state change immediately to initialize the first state, the title menu. Set this flag to 1
JSR InitSound
LDA #%10000000 ; enable NMI, sprites from Pattern Table 0
STA $2000
LDA #%10011000 ; enable sprites + Background
STA $2001
Forever:
JMP Forever ;Loop forever. Game logic happens on NMI interrupt
vblankwait: ;port $2002 tells us if PPU is in vblankstate, aka not drawing anything, and ready for data transfer
BIT $2002
BPL vblankwait
RTS
;;;;Handle Interrupts;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
NMI: ;NMI is triggered by the PPU when it enters VBlankstate, ie a period of time between drawing each frame. All Graphical Updates, then Game Logic, happen in here.
LDA #$00 ;Send the address of our sprite data, $0200, to PPU via 2 ports: 2003 and 4014. This begins transfer from RAM to PPU
STA $2003 ;set the low byte (00) of the RAM address. See OAM register
LDA #$02
STA $4014 ;set the high byte (02) of the RAM address to start transfer See OAMDMA register
JSR DrawTiles
JSR ReadController
JSR GameEngine
RTI
;;;;Main Routine;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;**GameEngine**
;Main driver of game
;Called from NMI Interrupt
;**
GameEngine:
JSR HandleBeep ;Handle sound here so we always end a note in time, regardless of our state
JSR ChangeState_Sub
LDA gamestate
CMP #GAMESTATE_TITLE
BNE .CheckPlayState
JSR State_Title_Sub
JMP .Exit_GameEngine
.CheckPlayState:
CMP #GAMESTATE_PLAYING
BNE .CheckGameOverState
JSR State_MatchInProgress_Sub
JMP .Exit_GameEngine
.CheckGameOverState:
CMP #GAMESTATE_GAMEOVER
BNE .CheckPausedState
JSR StateGameOver_Sub
JMP .Exit_GameEngine
.CheckPausedState
CMP #GAMESTATE_PAUSED
BNE .Exit_GameEngine
JSR CheckStart
.Exit_GameEngine:
RTS
ChangeState_Sub:
LDA changeStateRequest
CMP #$00
BEQ .Exit_Sub
LDA gamestate
CMP #GAMESTATE_TITLE
BNE .CheckPlayState
JSR ResetTitle_Sub
JMP .Exit_Sub
.CheckPlayState
CMP #GAMESTATE_PLAYING
BNE .CheckGameOverState
JSR InitMatch_Sub
JSR ResetRound_Sub
JMP .Exit_Sub
.CheckGameOverState
CMP #GAMESTATE_GAMEOVER
BNE .CheckPausedState
JSR InitGameOver_Sub
JMP .Exit_Sub
.CheckPausedState
CMP #GAMESTATE_PAUSED
BNE .Exit_Sub
JSR InitPause_Sub
.Exit_Sub:
LDA #$00
STA changeStateRequest
RTS
;;;;Game Logic;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ResetTitle_Sub:
LDA #$50
STA ball_x
LDA #$50
STA ball_y
LDA #INPUTDELAYTIMER
STA inputDelayTimer
LDA #TEXTTIMER
STA textBlinkTimer
LDA #$01
STA drawMenuTileFlag ;Draw tiles
RTS
State_Title_Sub:
LDA inputDelayTimer ;Small delay to reading input on title screen
CMP #$00
BEQ .CheckInput
SEC
SBC #$01
STA inputDelayTimer
JMP .Exit_Sub
.CheckInput:
LDA buttons1 ;Check button-down on up and down keys to switch game mode
AND #%00000100 ;Check 'Up'
BEQ .CheckDown
LDA buttons1OldState ;Check that old state was a non-press. Only want to cycle one option per press
AND #%00000100
BNE .CheckDown
LDA gamemode ;Up pressed, mode++
CLC
ADC #$01
CMP #$03 ;There are three game modes: 0, 1, and 2. Reach 3 -> roll over
BEQ .RollModeOver
STA gamemode
JMP .CheckStartGame
.RollModeOver:
LDA #$00
STA gamemode
JMP .CheckStartGame
.CheckDown:
LDA buttons1
AND #%00001000 ;Check 'Down'
BEQ .CheckStartGame
LDA buttons1OldState
AND #%00001000
BNE .CheckStartGame
LDA gamemode ;Down pressed, mode--
SEC
SBC #$01
CMP #$FF
BEQ .RollModeUnder
STA gamemode
JMP .CheckStartGame
.RollModeUnder:
LDA #$02
STA gamemode
.CheckStartGame:
LDA buttons1 ;If start pressed, go to play state
AND #%00010000
BEQ .Exit_Sub
LDA #GAMESTATE_PLAYING ;Set our new gamestate, and set the flag changeStateRequest
STA gamestate
LDA #$01
STA changeStateRequest
.Exit_Sub:
LDX gamemode
LDA #$50
CLC
ADC menuSelect_position, x
STA ball_y
JSR SetBallSprite_Sub
LDA #$01
STA blinkTextFlag
RTS
InitMatch_Sub:
LDA #$80 ;Set Ball x and y to 80 (center of screen)
STA player1_y
STA player2_y
LDA #LASTSCORED_P1
STA lastScored
LDA #$30 ;Reset Score sprites, hide 'W' sprite
STA $0221
LDA #$30
STA $021D
LDA #$59
STA $022D
LDA #$30
STA $0229
LDA #$30
STA $0225
LDA #$00
STA scoreP1Digit1 ;Reset player score
STA scoreP1Digit2
STA scoreP2Digit1
STA scoreP2Digit2
LDA #$01
STA coverMenuTilesFlag ;Draw over menu tiles at beginning of next NMI
LDA gamemode
CMP #GAMEMODE_1Player
BNE .Set2P
LDA #SCOREDIGIT1WIN1P ;For 1 player mode, set winning score to 99
STA winScoreD1
LDA #SCOREDIGIT2WIN1P
STA winScoreD2
LDA #$59 ;Hide P2 with a blank tile
STA $0211
STA $0215
STA $0219
JMP .Exit_Sub
.Set2P
LDA #SCOREDIGIT1WIN2P ;For 2 player mode, set winning score to 15
STA winScoreD1
LDA #SCOREDIGIT2WIN2P
STA winScoreD2
LDA #$00 ;Make sure player 2 has visible tiles
STA $0211
LDA #$10
STA $0215
LDA #$20
STA $0219
.Exit_Sub
RTS
ResetRound_Sub:
LDA #$00 ;Set initial ball speed
STA ball_x_speed
STA ball_y_speed
STA ball_x_subPx
STA ball_y_subPx
STA ball_x_speedSubPx
STA ball_y_speedSubPx
STA speedLevel
STA hitCount
LDA #PLAYSTATE_SERVING
STA gamesubstate
JSR StickBallToPlayer_Sub
LDA lastScored
CMP #LASTSCORED_P1 ;This player-serving/ball direction set might be un-necessary
BEQ .SetLeft
LDA #BALL_X_DIR_RIGHT
STA ball_x_dir
RTS
.SetLeft:
LDA #BALL_X_DIR_LEFT
STA ball_x_dir
RTS
State_MatchInProgress_Sub:
JSR CheckStart
JSR HandlePlayer1_Sub ;Player Movement Logic for both substates
LDA gamemode
CMP #GAMEMODE_1Player
BEQ .CheckGameSubstate ;If we are in 1-Player Mode, skip logic for second player
JSR HandlePlayer2_Sub
.CheckGameSubstate:
LDA gamesubstate
CMP #PLAYSTATE_SERVING
BNE .RoundInProgress
JSR Serving_Sub ;Play substate serving
JMP .Exit_Sub
.RoundInProgress: ;Play substate RoundInProgress
JSR LevelCheck
JSR HandleBall_Sub ;Ball movement logic
JSR HandleCollisions_Sub ;Handle Collision
JSR CheckWinCondition_Sub ;Transition to GameOver state if a player wins
.Exit_Sub:
JSR SetBallSprite_Sub ;Place sprites
JSR SetPlayerSprite_Sub
RTS
Serving_Sub:
JSR StickBallToPlayer_Sub ;If serving player presses A, serve ball
LDA lastScored
CMP #LASTSCORED_P1
BNE .LoadP2
LDA buttons1
JMP .CheckA
.LoadP2
LDA buttons2
.CheckA
STA arg0 ;Store button state in scratch
AND #%10000000
BNE .CheckUp ;If A pressed, check for direction, the serve ball
LDA #$01
STA blinkTextFlag ;Blink serve text
RTS
.CheckUp
LDA arg0
AND #%00001000 ;Check up
BEQ .CheckDown
LDA #BALL_Y_DIR_UP
STA ball_y_dir
LDA #$01
STA serveSpeed
JMP .CheckB
.CheckDown
LDA arg0
AND #%00000100 ;Check down
BEQ .Zero
LDA #BALL_Y_DIR_DOWN
STA ball_y_dir
LDA #$02
STA serveSpeed
.CheckB
LDA arg0
AND #%01000000 ;If B is pressed while serving, ball will be extra fast!
LDA #$03
JMP .ServeBall
.Zero
LDA #$00
.ServeBall ;Serve the ball! Start by setting initial values, and let next game substate take care of the rest
STA serveSpeed
LDA #BALL_INITSPEED_X
STA ball_x_speed
LDA #BALL_INITSPEED_Y
CLC
ADC serveSpeed
STA ball_y_speed
LDA #PLAYSTATE_ROUNDINPROGRESS
STA gamesubstate ;Set game substate to game in progress
JSR CoverServeText ;Hide serve text
LDA #$00 ;reset PPU Scroll
STA $2005
STA $2005
RTS
;**Check how many hits we have, and INC speed level at thresholds
LevelCheck:
LDA speedLevel
CMP #MAXSPEEDLEVEL
BEQ .Exit_Sub ;We've reached max level, so skip
TAX
LDA hitCountToLevel,x
CMP hitCount
BCS .Exit_Sub
.IncLevel
LDA levelToXSpeed,x
STA ball_x_speed
INC speedLevel
.Exit_Sub
RTS
;**HandleBall_Sub
; Ball Movement behavior.
; 1 byte for sub pixels, 1 byte for actual pixel.
HandleBall_Sub:
LDA ball_x_dir ;Handle X movement
CMP #BALL_X_DIR_RIGHT
BNE .MoveBallLeft
.MoveBallRight
LDA ball_x_subPx
CLC
ADC ball_x_speedSubPx
STA ball_x_subPx
LDA ball_x
ADC ball_x_speed
JMP .SetBallX
.MoveBallLeft:
LDA ball_x_subPx
SEC
SBC ball_x_speedSubPx
STA ball_x_subPx
LDA ball_x
SBC ball_x_speed
.SetBallX:
STA ball_x
.HandleY
LDA ball_y_dir ;Handle Y Movement
CMP #BALL_Y_DIR_DOWN
BNE .MoveBallUp
.MoveBallDown
LDA ball_y_subPx
CLC
ADC ball_y_speedSubPx
STA ball_y_subPx
LDA ball_y
ADC ball_y_speed
STA ball_y
RTS
.MoveBallUp:
LDA ball_y_subPx
SEC
SBC ball_y_speedSubPx
STA ball_y_subPx
LDA ball_y
SBC ball_y_speed
STA ball_y
.Exit_Sub:
RTS
;**StickBallToPlayer_Sub
;Set ball to center-front of paddle. Offset as necessary
;**
StickBallToPlayer_Sub:
LDA lastScored
CMP #LASTSCORED_P1
BEQ .SetP1
LDA player2_y
CLC
ADC #$08
STA ball_y
LDA #P2_X
SEC
SBC #$08
JMP .Exit_Sub
.SetP1:
LDA player1_y
CLC
ADC #$08
STA ball_y
LDA #P1_X
CLC
ADC #$04
.Exit_Sub
STA ball_x
RTS
HandlePlayer1_Sub:
.CheckB: ;Hold B to go faster
LDA buttons1
AND #%01000000
BNE .SetBSpeed
LDA #PLAYERSPEED_SLOW
STA player1_speed
JMP .ReadPlayer1Y
.SetBSpeed:
LDA #PLAYERSPEED_FAST
STA player1_speed
.ReadPlayer1Y: ;Use up and down to move
LDA buttons1
AND #%00001000
BNE .MoveP1Up
LDA buttons1
AND #%00000100
BNE .MoveP1Down
JMP .Exit_Sub
.MoveP1Down: ;Move player 1 down, check against bottom bounds and kick out if necessary
LDA player1_y
CLC
ADC player1_speed
STA player1_y
CMP #BOTTOMWALL_PLAYER
BCC .Exit_Sub
LDA #BOTTOMWALL_PLAYER
STA player1_y
JMP .Exit_Sub
.MoveP1Up: ;Move player 1 up, check against top bounds and kick out if necessary
LDA player1_y
SEC
SBC player1_speed
STA player1_y
SEC
CMP #TOPWALL_PLAYER
BCS .Exit_Sub
LDA #TOPWALL_PLAYER
STA player1_y
.Exit_Sub:
RTS
HandlePlayer2_Sub:
.P2CheckB: ;Hold B to go faster
LDA buttons2
AND #%01000000
BNE .P2SetBSpeed
LDA #PLAYERSPEED_SLOW
STA player2_speed
JMP .ReadPlayer2Y
.P2SetBSpeed:
LDA #PLAYERSPEED_FAST
STA player2_speed
.ReadPlayer2Y: ;Use up and down to move
LDA buttons2
AND #%00001000
BNE .MoveP2Up
LDA buttons2
AND #%00000100
BNE .MoveP2Down
JMP .Exit_Sub
.MoveP2Down: ;Move player 2 down, check against bottom bounds and kick out if necessary
LDA player2_y
CLC
ADC player2_speed
STA player2_y
CMP #BOTTOMWALL_PLAYER
BCC .Exit_Sub
LDA #BOTTOMWALL_PLAYER
STA player2_y
JMP .Exit_Sub
.MoveP2Up: ;Move player 2 up, check against top bounds and kick out if necessary
LDA player2_y
SEC
SBC player2_speed
STA player2_y
SEC
CMP #TOPWALL_PLAYER
BCS .Exit_Sub
LDA #TOPWALL_PLAYER
STA player2_y
.Exit_Sub:
RTS
HandleCollisions_Sub:
LDA ball_y
CLC
ADC #$04 ;Test against center of ball by adding 4 to ball x and y coords
STA ball_y
LDA ball_x
CLC
ADC #$04
STA ball_x
JSR Collisions_BallV_Sub
JSR Collisions_BallH_Sub
LDA ball_x ;remove 4 from ball x and y so that drawing coordinates are correct
SEC
SBC #$04
STA ball_x
LDA ball_y
SEC
SBC #$04
STA ball_y
RTS
Collisions_BallV_Sub: ;Check against top and bottom walls
LDA ball_y
CMP #TOPWALL
BCS .CheckBottomWall
JSR BounceY_Sub ;Collide with Top wall
RTS
.CheckBottomWall:
LDA ball_y
CMP #BOTTOMWALL
BCC .Exit_Sub
JSR BounceY_Sub ;Collide with bottom wall
.Exit_Sub:
RTS
Collisions_BallH_Sub: ;Check against left and right boundaries and player paddles
LDA ball_x_dir
CMP #BALL_X_DIR_RIGHT
BNE .CheckLeftSide
JSR CheckCollisions_Right
RTS
.CheckLeftSide
JSR CheckCollisions_Left
.Exit_Sub:
RTS
;**CheckCollisions_Left
;Test ball against left wall and P1
;**
CheckCollisions_Left:
LDA ball_x
CMP #LEFTGOAL
BCS .CheckPlayer1 ;BCS: true if A > M
.CheckLeftGoal
LDA #LASTSCORED_P2 ;If Ball x < left wall, ball has collided with goal
STA lastScored
JSR ToggleBallXDirection
JSR HandleGoalCollision
RTS
.CheckPlayer1:
LDA player1_y ;Test againt four sides of P1 paddle.
SEC ;y-4 to account for diameter of ball
SBC #$04
CMP ball_y
BCS .Exit_Sub
LDA player1_y
CLC
ADC #$1B ;$18+$04-1=$1B to compensate for diameter of ball, Would need extra powerIndex
CMP ball_y
BCC .Exit_Sub
LDA #P1_X
CMP ball_x
BCS .Exit_Sub
CLC
ADC #$07
CMP ball_x
BCC .Exit_Sub
JSR Calculate_BallYSpeed
JSR ToggleBallXDirection ;Ball has hit paddle
LDA #$40 ;Load player 1 palette
STA $020E ;Set ball palette
INC hitCount
.Exit_Sub
RTS
;**CheckCollisions_Right
;Test ball against Right wall and P2
;**
CheckCollisions_Right:
LDA ball_x ;Ball moving Right, check against right goal/P2
CMP #RIGHTGOAL
BCC .CheckPlayer2
.CheckRightGoal
LDA #LASTSCORED_P1 ;Ball x > right goal = collision. Award point to P1.
STA lastScored
JSR ToggleBallXDirection
JSR HandleGoalCollision
RTS
.CheckPlayer2:
LDA gamemode
CMP #GAMEMODE_1Player ;If we are in 1 player mode, skip player 2 check
BEQ .Exit_Sub
LDA player2_y ;Test againt four sides of P2 paddle. BCS = A > X. BCC = A < X
SEC
SBC #$04
CMP ball_y
BCS .Exit_Sub
LDA player2_y
CLC
ADC #$1B ;$18+$04-1=$1B
CMP ball_y
BCC .Exit_Sub
LDA #P2_X
CLC
ADC #$01
CMP ball_x
BCS .Exit_Sub
CLC
ADC #$07
CMP ball_x
BCC .Exit_Sub
JSR Calculate_BallYSpeed
JSR ToggleBallXDirection
LDA #$42 ;Load player 2 palette
STA $020E ;Set ball palette
.Exit_Sub
RTS
ToggleBallXDirection:
JSR StartBeep
LDA ball_x_dir
EOR #$01
STA ball_x_dir
RTS
BounceY_Sub:
LDA ball_y_dir
CMP #BALL_Y_DIR_DOWN
BNE .Top
.Bottom
JSR KickoutBall_Bottom
JMP .bounce
.Top
JSR KickoutBall_Top
.bounce
JSR StartBeep
JSR ToggleBallYDirection
RTS
;**KickoutBall_Bottom
;TODO: make this more advanced.
;
KickoutBall_Bottom:
LDA #BOTTOMWALL
STA ball_y
RTS
KickoutBall_Top:
LDA #TOPWALL
STA ball_y
RTS
ToggleBallYDirection:
LDA ball_y_dir
EOR #$01
STA ball_y_dir
RTS
;**Calculate_BallYSpeed
;Calculate new ball speed and direction of ball, dependant on the position it hits a paddle
;BUG WITH PADDLE COLLISION: When d = 1A, X = 6 (oob)
;Use arg0,1,2 for scratch
Calculate_BallYSpeed:
LDA ball_x_dir ;We have just hit the ball with the paddle, but not yet changed ball x direction. Use direction to determine which paddle it hit
CMP #BALL_X_DIR_RIGHT
BNE .LeftPaddle
.RightPaddle
LDA ball_y ;We also know ball_y > player_y
SEC
SBC player2_y
JMP .GetSpeedFromTable
.LeftPaddle
LDA ball_y
SEC
SBC player1_y
.GetSpeedFromTable
CLC
ADC #$04 ;Add 4 to account for collision
STA arg0 ;Store distance in arg0
LSR A ;A=distance. A/4 = index. Used two LSRs and ignore remainder
LSR A
TAX
LDA distanceToPower_3_Lo,x ;Store px and sub pix speed changes based on index
STA arg1
LDA distanceToPower_3_Hi,x
STA arg2
.AddOrSubtract
LDA ball_y_dir
CMP #BALL_Y_DIR_UP
BNE .Down
.Up ;When ball is moving up, top half of paddle increases y speed
LDA arg0 ;Check distance. if d < 12, we have hit top half (24+4+4)
CMP #$0C
BCS .Sub
JMP .Add
.Down ;When ball is moving down, bottom half of paddile increases y speed
LDA arg0
CMP #$0C
BCS .Add
JMP .Sub
.Add
LDA ball_y_speedSubPx
CLC
ADC arg1
STA ball_y_speedSubPx
LDA ball_y_speed
ADC arg2
STA ball_y_speed
JMP .Max
.Sub
LDA ball_y_speedSubPx
SEC
SBC arg1
STA ball_y_speedSubPx
LDA ball_y_speed
SBC arg2
STA ball_y_speed
BCS .Max
JSR TwosCompliment ;If BCC, top bit had to borrow, so we are negative, and have to make our speed positive
JSR ToggleBallYDirection ;Since we have flipped signs, flip ball direction
.Max
RTS
;**Performs 2's compliment on a Negative 16 bit number, giving us its absolute value
TwosCompliment:
;If top bit is subtracted and overflows, get absolute value of speed
LDA ball_y_speed
EOR #$FF ;N xor FF is same as performing 1's Compliment operation
STA ball_y_speed
LDA ball_y_speedSubPx
EOR #$FF
CLC
ADC #$01
STA ball_y_speedSubPx
RTS
StartBeep:
LDA #BEEPLENGTH
STA beepTimer ;Start beep timer
.SetNextNote
LDA beepToneIndex ;Load current index
asl a ;multiply index by two for word array
tay
LDA beepToneSeq, y ;Store low byte of period
STA $4002
LDA beepToneSeq+1, y ;Store high byte of period
STA $4003
LDA beepToneIndex
CLC
ADC #$01
CMP #$10 ;16 note sequence. Roll over index
BNE .Exit_Sub
LDA #$00
.Exit_Sub
STA beepToneIndex
RTS
HandleBeep:
LDA beepTimer
CMP #$00
BEQ .Exit_Sub
SEC
SBC #$01
STA beepTimer ;Subtract 1 from beepTimer every frame
LDA #$00 ;Turn off beep 000 turns off square?
STA $4002
LDA #$00
STA $4003
.Exit_Sub
RTS
HandleGoalCollision: ;Award point, and either reset board to serving mode, or reflect ball, depending on GAMEMODE
LDA lastScored
CMP #$00
BNE .CheckP2
JSR IncP1ScoreOnes
LDA gamemode ;Game mode = 1 player, reflect ball off right wall rather than reseting board
CMP #GAMEMODE_1Player
BNE .Exit_Sub
RTS
.CheckP2:
LDA gamemode ;Game mode = 1 player, reflect ball off right wall rather than reseting board
CMP #GAMEMODE_1Player
BNE .AwardPointP2
LDA #$00
STA gamesubstate
LDA #GAMESTATE_GAMEOVER
STA gamestate
LDA #$01
STA changeStateRequest
.AwardPointP2
JSR IncP2ScoreOnes
.Exit_Sub:
JSR ResetRound_Sub
SKIP_STATECHANGE
RTS
IncP1ScoreOnes:
LDA scoreP1Digit1 ; load the lowest digit of the number
CLC
ADC #$01 ; add one
STA scoreP1Digit1
CMP #$0A ; check if it overflowed, now equals 10
BNE IncP1ScoreDone ; if there was no overflow, all done
IncP1ScoreTens:
LDA #$00
STA scoreP1Digit1 ; wrap digit to 0
LDA scoreP1Digit2 ; load the next digit
CLC
ADC #$01 ; add one, the carry from previous digit
STA scoreP1Digit2
CMP #$0A ; check if it overflowed, now equals 10
BNE IncP1ScoreDone ; if there was no overflow, all done
LDA #$00
STA scoreP1Digit2 ; wrap digit to 0
IncP1ScoreDone:
LDA scoreP1Digit1 ; Set score sprite tile numbers
CLC
ADC #$30 ;Offset tile number
STA $021D ;Store sprite number
LDA scoreP1Digit2
CLC
ADC #$30 ;Offset tile number
STA $0221 ;Store sprite number
RTS
IncP2ScoreOnes:
LDA scoreP2Digit1
CLC
ADC #$01
STA scoreP2Digit1
CMP #$0A
BNE IncP2ScoreDone
IncP2ScoreTens:
LDA #$00
STA scoreP2Digit1
LDA scoreP2Digit2
CLC
ADC #$01
STA scoreP2Digit2
CMP #$0A
BNE IncP2ScoreDone
LDA #$00
STA scoreP2Digit2
IncP2ScoreDone:
LDA scoreP2Digit1
CLC
ADC #$30 ;Offset tile number
STA $0225 ;Store sprite number
LDA scoreP2Digit2
CLC
ADC #$30 ;Offset tile number
STA $0229 ;Store sprite number
RTS
CheckWinCondition_Sub: ;****TODO**** For one player, we'll want to allow player as many points as they can get until they are out
LDA scoreP1Digit1
CMP winScoreD1
BNE CheckPlayer2Score
LDA scoreP1Digit2
CMP winScoreD2
BNE CheckPlayer2Score
JMP SetGameOverState
CheckPlayer2Score:
LDA scoreP2Digit1
CMP winScoreD1
BNE End_CheckWinCondition_Sub
LDA scoreP2Digit2
CMP winScoreD2
BNE End_CheckWinCondition_Sub
SetGameOverState: ;If either player reaches 15 (0E), move to game over state. 'lastScored' will determine what displays.
LDA #$00
STA gamesubstate
LDA #GAMESTATE_GAMEOVER
STA gamestate
LDA #$01
STA changeStateRequest
End_CheckWinCondition_Sub:
RTS
StateGameOver_Sub:
LDA buttons1 ;If start pressed, go to play state
AND #%00010000
BEQ .Exit_Sub
STA gamesubstate ;Set Substate PLAYSTATE_SERVING
LDA #GAMESTATE_TITLE ;Set game state to Playing
STA gamestate
LDA #$01
STA changeStateRequest
.Exit_Sub:
RTS
InitGameOver_Sub:
LDA #$80 ;Set Ball x and y to 80 (center of screen)
STA ball_x ;Variable values
STA ball_y
STA player1_y
STA player2_y
LDA #$00 ;Set initial ball speed
STA ball_x_speed
STA ball_y_speed
STA player1_speed
STA player2_speed
LDA gamemode
CMP #GAMEMODE_1Player
BNE .Set2P
LDA $0221 ;For 1P mode, transfer player score to right side, and set left side to 'P1'
STA $0229
LDA $021D
STA $0225
LDA #$31
STA $021D
JMP .Exit_Sub
.Set2P:
LDA #$31 ;Display '1' or '2' depending on who won. Use score sprites as part of message. For 1Player mode, always display P1
CLC
ADC lastScored
STA $021D
LDA #$50 ;W
STA $022D
LDA #$42 ;I
STA $0229
LDA #$47 ;N
STA $0225
.Exit_Sub:
LDA #$49 ;Set P Sprite
STA $0221
RTS
;**TODO... merge this with PauseState behavior
CheckStart:
LDA buttons1
AND #%00010000
BEQ .CheckP2
LDA buttons1OldState
AND #%00010000
BNE .CheckP2
JMP .Pause
.CheckP2
LDA buttons2
AND #%00010000
BEQ .Exit_Sub
LDA buttons2OldState
AND #%00010000
BNE .Exit_Sub
.Pause:
LDA gamestate
CMP #GAMESTATE_PAUSED
BEQ .Unpause
LDA #GAMESTATE_PAUSED
STA gamestate
LDA #$01
STA changeStateRequest
JMP .Exit_Sub
.Unpause
LDA #GAMESTATE_PLAYING
STA gamestate ;Change game state back to playing without setting stateChangeRequest, so we don't reinitialize values that are mid match
.Exit_Sub
RTS
InitPause_Sub:
;Start a jingle here
;Start tile text drawing here
RTS
;**BlinkPressStart_Sub
;;Subtract 1 from timer each from. At half time, we hide text. At 0, we show text again
;**
BlinkPressStart_Sub:
LDA textBlinkTimer
SEC
SBC #$01
STA textBlinkTimer
CMP #TEXTTIMERHALF
BNE .CheckBlink
LDA #$0F
STA arg2
LDA #$22
STA arg0
LDA #$48
STA arg1
JSR WriteFloorTiles_Upper
JMP .Exit_Sub
.CheckBlink
LDA textBlinkTimer
CMP #$00
BNE .Exit_Sub
LDA #LOW(textHeader_pressStart)
STA arg0
LDA #HIGH(textHeader_pressStart)
STA arg1
JSR WriteBGTiles
LDA #TEXTTIMER
STA textBlinkTimer
.Exit_Sub
LDA #$00
STA $2005
STA $2005
RTS
BlinkPressA_Sub:
LDA textBlinkTimer
SEC
SBC #$01
STA textBlinkTimer
CMP #TEXTTIMERHALF ;Timer has reached half time. Overwrite text with floor tiles
BNE .CheckBlink
JSR CoverServeText
JMP .Exit_Sub
.CheckBlink
LDA textBlinkTimer
CMP #$00
BNE .Exit_Sub ;Timer has reached 0.
LDA #TEXTTIMER ;Reset Timer
STA textBlinkTimer
LDA #LOW(textHeader_playerServe) ;Draw text
STA arg0
LDA #HIGH(textHeader_playerServe)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_pressA)
STA arg0
LDA #HIGH(textHeader_pressA)
STA arg1
JSR WriteBGTiles
LDA lastScored
CMP #LASTSCORED_P1
BEQ .SetP1
LDA #LOW(textHeader_player2)
STA arg0
LDA #HIGH(textHeader_player2)
STA arg1
JSR WriteBGTiles
JMP .Exit_Sub
.SetP1
LDA #LOW(textHeader_player1)
STA arg0
LDA #HIGH(textHeader_player1)
STA arg1
JSR WriteBGTiles
.Exit_Sub
LDA #$00
STA $2005
STA $2005
RTS
CoverServeText:
LDA #$0E
STA arg2
LDA #$22
STA arg0
LDA #$48
STA arg1
JSR WriteFloorTiles_Upper
LDA #$68
STA arg1
JSR WriteFloorTiles_Lower
RTS
SetBallSprite_Sub:
LDA ball_x
STA $020F
LDA ball_y
STA $020C
.Exit_Sub:
RTS
SetPlayerSprite_Sub:
LDA player1_y
STA $0200
CLC
ADC #$08
STA $0204
ADC #$08
STA $0208
LDA gamemode ;If game mode is 1 player, skip player 2 sprites POS logic
CMP #GAMEMODE_1Player
BEQ .Exit_Sub
LDA player2_y
STA $0210
CLC
ADC #$08
STA $0214
ADC #$08
STA $0218
.Exit_Sub:
RTS
ToggleGoalLineTiles_Sub:
RTS
;;;;;Tiles Setting Stuff;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrawTiles:
LDA #$00
STA $2001 ;Start by disabling rendering to avoid VRAM corruption. This is a lot of tiles to draw
LDA drawMenuTileFlag
CMP #$00
BEQ .CoverMenu
JSR DrawMenuTiles
LDA #$00
STA drawMenuTileFlag
JMP .Exit_Sub ;Don't do
.CoverMenu
LDA coverMenuTilesFlag
CMP #$00
BEQ .BlinkTextFlag
JSR CoverTitleMenuText_Sub
LDA #$00
STA coverMenuTilesFlag
JMP .Exit_Sub ;Might remove this if I think I can do them on the same frame
.BlinkTextFlag
LDA blinkTextFlag
CMP #$00
BEQ .Exit_Sub
LDA #$00
STA blinkTextFlag
LDA gamestate
CMP #GAMESTATE_PLAYING
BEQ .BlinkA
.BlinkStart
JSR BlinkPressStart_Sub
JMP .Exit_Sub
.BlinkA
JSR BlinkPressA_Sub
.Exit_Sub
LDA #%10011000 ; enable sprites + Background
STA $2001
LDA #$00 ;Reset PPU Scroll
STA $2005
STA $2005
RTS
DrawMenuTiles:
LDA #LOW(textHeader_solo)
STA arg0
LDA #HIGH(textHeader_solo)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_2Player)
STA arg0
LDA #HIGH(textHeader_2Player)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_VSAI)
STA arg0
LDA #HIGH(textHeader_VSAI)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_pressStart)
STA arg0
LDA #HIGH(textHeader_pressStart)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_title1)
STA arg0
LDA #HIGH(textHeader_title1)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_title2)
STA arg0
LDA #HIGH(textHeader_title2)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_title3)
STA arg0
LDA #HIGH(textHeader_title3)
STA arg1
JSR WriteBGTiles
LDA #LOW(textHeader_title4)
STA arg0
LDA #HIGH(textHeader_title4)
STA arg1
JSR WriteBGTiles
RTS
;**CoverTitleMenuText_Sub:
; Draw floor tiles to over write menu text
;**
CoverTitleMenuText_Sub:
LDA #$08 ;Each draw will be 8 tiles long
STA arg2
LDA #$21
STA arg0
LDA #$4C
STA arg1
JSR WriteFloorTiles_Upper
LDA #$21
STA arg0
LDA #$8C
STA arg1
JSR WriteFloorTiles_Upper
LDA #$21
STA arg0
LDA #$CC
STA arg1
JSR WriteFloorTiles_Upper
LDA #$0F ;Last draw is 14 tiles long
STA arg2
LDA #$22
STA arg0
LDA #$48
STA arg1
JSR WriteFloorTiles_Upper
RTS
;**WriteBGTiles
; arg0 Text pos object low byte
; arg1 text pos object high byte
; arg2 used for scratch
;**
WriteBGTiles:
LDA $2002
LDY #$00
LDA [arg0],y
STA $2006
INY
LDA [arg0],y
STA $2006
INY
LDA [arg0],y
CLC
ADC #$03 ;Offset by size of header
STA arg2
INY
.LoadTilesLoop
LDA [arg0], y
STA $2007
INY
CPY arg2
BNE .LoadTilesLoop
RTS
;**WriteFloorTiles_Upper
; arg0 start address HIGH
; arg1 start address LOW
; arg2 length of bytes/number of tiles to draw
;**
WriteFloorTiles_Upper:
LDA $2002
LDA arg0
STA $2006
LDA arg1
STA $2006
LDX #$00
.LoadTilesLoop
LDA text_coverUpper, x
STA $2007
INX
CPX arg2
BNE .LoadTilesLoop
RTS
WriteFloorTiles_Lower:
LDA $2002
LDA arg0
STA $2006
LDA arg1
STA $2006
LDX #$00
.LoadTilesLoop
LDA text_coverLower, x
STA $2007
INX
CPX arg2
BNE .LoadTilesLoop
RTS
;**FillTileColumn
;arg0: Low byte of top tile in column. High byte always starts at $20
;arg1: tile ID
;arg0,2 used as scratch
;**FillTileColumn
;arg0: Low byte of top tile in column. High byte always starts at $20
;arg1: tile ID
;arg0,2 used as scratch
;Currently not functional. It seems this method, called from NMI, can fill about 24 tiles before corruption
FillTileColumn:
LDA #$01
STA arg0
LDA #$20
STA arg1
LDX #$00
.loop
LDA $2002
LDA arg1
STA $2006
LDA arg0
STA $2006
LDA #$6A
STA $2007
LDA arg0
CLC
ADC #$20
STA arg0
LDA arg1
ADC #$00
STA arg1
INX
CPX #$02 ;Do this for 30 tiles in column
BNE .loop
.Exit_Sub
RTS
;;;;Audio;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
InitSound:
LDA #%00000001 ;Enable square 1 of audio
STA $4015 ;Write to Audio control
LDA #%10111111; Duty 10 (50%), volume F (max!)
STA $4000
RTS
;;;;Asset Loading For Initialization Subroutines;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LoadAssets:
LoadPalettes: ; Send palette data from PRG ROM to store @ PPU RAM $3F00
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$3F
STA $2006 ;write high, then low byte of PPU address to PPU port $2006
LDA #$00
STA $2006
LDX #$00
LoadPalettesLoop:
LDA palette, x ;load palette byte
STA $2007 ;write to PPU port $2007
INX ;set index to next byte
CPX #$20
BNE LoadPalettesLoop ;if x = $20, 32 bytes copied, all done
LoadSprites:
LDX #$00 ; start at 0
LoadSpritesLoop:
LDA sprites, x ;Load data from PRG-ROM (address sprites + x)
STA $0200, x ;store sprite data in CPU RAM ( address $0200 + x)
INX
CPX #$30 ;Compare X to # of bytes of sprite data
BNE LoadSpritesLoop ;Loop if we haven't loaded all bytes yet
LoadBackground:
LDA $2002 ; read PPU status to reset the high/low latch
LDA #$20
STA $2006 ; write the high byte of $2000 address
LDA #$00
STA $2006 ; write the low byte of $2000 address
LDX #$00 ; start out at 0
LDA #LOW(background)
STA pointerLo ; put the low byte of the address of background into pointer
LDA #HIGH(background)
STA pointerHi ; put the high byte of the address into pointer
LDA #$00
sta counterLo ; put the loop counter into 16 bit variable
LDA #$04
sta counterHi ; count = $0400 =1KB, the whole screen at once including attributes
LDY #$00 ; put y to 0 and don't change it
.LoadBackgroundLoop:
LDA [pointerLo], y
STA $2007 ; copy one background byte
LDA pointerLo
CLC
ADC #$01
STA pointerLo
LDA pointerHi
ADC #$00
STA pointerHi ; increment the pointer to the next byte
LDA counterLo
SEC
SBC #$01
STA counterLo
LDA counterHi
SBC #$00
STA counterHi ; decrement the loop counter
LDA counterLo
CMP #$00
BNE .LoadBackgroundLoop
LDA counterHi
CMP #$00
BNE .LoadBackgroundLoop ; if the loop counter isn't 0000, keep copying
LDA #$00
;STA $2005
;STA $2005
RTS
;;;;Handle Input;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;**ReadController: Sets buttons1 and buttons2
;Byte order: A, B, Select, Start, Up, Down, Left, Right
;**
ReadController:
LDA buttons1 ;Save our input from to detect button presses and releases
STA buttons1OldState
LDA buttons2
STA buttons2OldState
LDA #$01 ;Initialize controller reading at port $4016
STA $4016
LDA #$00
STA $4016 ; tell both the controllers to latch buttons
LDX #$08 ;counter to read 8 buttons from shift register @ $4016
.ReadControllerLoop:
LDA $4016
LSR A ;Left Shift, bit0 -> Carry
ROL buttons1 ; Right Shift, bit0 <- Carry
DEX
BNE .ReadControllerLoop
.ReadController2: ;Initialize controller reading at port $4016
LDA #$01
STA $4017
LDA #$00
STA $4017 ; tell both the controllers to latch buttons
LDX #$08 ;counter to read 8 buttons from shift register @ $4016
.ReadController2Loop:
LDA $4017
LSR A ;Left Shift, bit0 -> Carry
ROL buttons2 ; Right Shift, bit0 <- Carry
DEX
BNE .ReadController2Loop
RTS
;;;;Resource bank;;;;
.bank 1
.org $E000
palette:
.db $0F,$1C,$2D,$0C, $0F,$20,$2D,$0C, $0F,$14,$03,$24, $00,$00,$00,$00 ;First 16 bytes are for BG. 4 sets of 4 colors $E000-$E010
.db $0F,$21,$39,$01, $0F,$13,$20,$1C, $0F,$2B,$39,$13, $0F,$10,$01,$00 ;Second 16 bytes are for sprites. $E011-$E020
sprites: ;Y, tile#, Attr, X
.db $80,$00,$00,$10 ;player 1 paddle top -> 0200-0203
.db $88,$10,$00,$10 ;player 1 paddle middle 0204-0207
.db $90,$20,$00,$10 ;player 1 padle bottom -> 0208-020B
.db $10,$01,$00,$80 ;ball -> 020C-020F
.db $80,$00,$42,$E8 ;player 2 paddle top -> 0210-0213
.db $88,$10,$42,$E8 ;player 2 paddle middle -> 0214-0217
.db $90,$20,$42,$E8 ;player 2 paddle bottom -> 0218-021B
.db $40,$30,$00,$38 ;Player 1 score low digit -> 021C-021F
.db $40,$30,$00,$30 ;Player 1 score high digit -> 0220-0223
.db $40,$30,$00,$C8 ;Player 2 score low digit -> 0224-0227
.db $40,$30,$00,$C0 ;Player 2 score high digit -> 0228-022B
.db $40,$59,$00,$B8 ;"W" sprite on Win Screen -> 022C-022F
background:
.db $59,$5A,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
.db $59,$5A,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5A,$59
.db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
attribute:
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
.db %00000000, %00000000, %01010000, %01010000, %01010000, %01010000, %00000000, %00000000
.db %00000000, %00000000, %10101010, %10101010, %10101010, %10101010, %00000000, %00000000
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
.db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
textHeader_solo:
.db $21, $4C, $04 ;address to start writng tiles (HIGH, LOW), character count. The text data should always be the next in memory, so that it is three bytes from this position
text_solo:
.db $4C,$48,$45,$48
textHeader_2Player:
.db $21,$8C,$08
text_2Player:
.db $32,$59,$49,$45,$3A,$52,$3E,$4B
textHeader_VSAI:
.db $21,$CC,$05
text_VSAI:
.db $4F,$4C,$59,$3A,$42
textHeader_pressStart:
.db $22,$49,$0E
text_pressStart:
.db $94,$89,$8B,$7E,$8C,$8C,$59,$8C,$8D,$7A,$8B,$8D,$95,$94
textHeader_playerServe:
.db $22,$49,$08
text_playerServe:
.db $94,$89,$85,$7A,$92,$7E,$8B,$59
textHeader_pressA:
.db $22,$6B,$08
text_pressA:
.db $89,$8B,$7E,$8C,$8C,$59,$7A,$94
textHeader_player1:
.db $22,$51,$01
text_Player1:
.db $71
textHeader_player2:
.db $22,$51,$01
text_Player2:
.db $72
text_coverUpper:
.db $5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C
text_coverLower:
.db $6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C
textHeader_title1:
.db $22,$88,$10
text_title1;
.db $B0,$B1,$B2,$B3,$B4,$B5,$B6,$B7,$B8,$B9,$59,$59,$59,$59,$59,$59
textHeader_title2:
.db $22,$A8,$10
text_title2;
.db $C0,$C1,$C2,$C3,$C4,$C5,$C6,$C7,$C8,$C9,$59,$59,$59,$59,$59,$59
textHeader_title3:
.db $22,$C8,$10
text_title3;
.db $59,$59,$59,$59,$59,$59,$D0,$D1,$D2,$D3,$D4,$D5,$D6,$D7,$D8,$D9
textHeader_title4:
.db $22,$E8,$10
text_title4;
.db $59,$59,$59,$59,$59,$59,$E0,$E1,$E2,$E3,$E4,$E5,$E6,$E7,$E8,$E9
beepToneSeq: ;C4, G4, A#4(Bb?), C5, C4, G4, A#4, C5, A#3, F4, G#4(Ab?), A#4, A#3, F4, G#4, A#4
.word $00d2, $008E, $00EF, $006A, $00d2, $008E, $00EF, $006A, $01DF, $009F, $0096, $00EF, $01DF, $009F, $0096, $00EF ;16
menuSelect_position:
.db $00,$10,$20
distanceToPower_3_Hi: ;Player is 3 tiles long, 24 px, + 4px pffset for top and bottom due to ball radius
.db $01,$01,$00,$00,$00,$00,$01,$01 ;Pixel/Frame
distanceToPower_3_Lo:
.db $7F,$00,$7F,$30,$30,$7F,$00,$7F ;SubPixel/Frame
hitCountToLevel:
.db $05,$19,$28 ;5 hits, 25 hits, 50 hits
levelToXSpeed:
.db $03,$04,$05 ;Probably want one level that can't be achieved except temporarily after a power shot, which then drops back down into normal max level
;;;;Interrupt vars;;;;
.org $FFFA ;first of the three vectors starts here
.dw NMI ;when an NMI happens (once per frame if enabled) the
;processor will jump to the label NMI:
.dw RESET ;when the processor first turns on or is reset, it will jump
;to the label RESET:
.dw 0 ;external interrupt IRQ is not used in this tutorial
;;;;CHR Bank;;;;
.bank 2
.org $0000 ;CHR ROM, will show up in PPU memory
.incbin "D:\NESDev\Projects\CyberPong2023\CyberPong.chr" ;includes 8KB graphics file