Lab 03 - Breakout Game
This lab is quite special compared to previous ones, as it is more subjective and taps into our creativity. We were tasked with creating any program we want, as long as it meets these criteria:
- The program must work in the 6502 Emulator
- It must output to the character screen as well as the graphics (bitmapped) screen.
- It must accept user input from the keyboard in some form.
- It must use some arithmetic/math instructions (to add, subtract, do bitwise operations, or rotate/shift)
Some suggestions given by our professor were to make either a simple game or a simple calculator/converter. With that in mind, I decided to remake the 1976 arcade game Breakout.
What is Breakout?
Based on its Wikipedia description, Breakout is a 1967 arcade game developed and published by Atari, Inc. It involves some bricks, a paddle, and a bouncing ball. The goal is to destroy all bricks with a bouncing ball, using the paddle as a sort of tool to keep the ball in play. You lose when the ball misses the paddle and escapes.
In the original version of game, it uses multiple rows of bricks with each brick corresponding to a certain amount of points when broken. The deeper the brick, the more points you get when you break it. The original game also gives you three chances to keep the ball in play. When the ball escapes the third time, you lose.
For my version, I'm deciding to keep things really simple by using only one row of bricks and giving the player only one chance to break them all. I will also remove the concept of points so that you only win by either breaking all the bricks or lose by allowing the ball to escape. This allows us to minimize complexity yet check all the criteria required by this lab.
My First Steps in Development
I started my development by doing some research about the game and looking up on tutorials on how to make the game in other programming languages. I needed a sort of guideline to follow so that I know the major concepts and building blocks that make up the game.
Some useful resources I found:
- 2D breakout game using pure JavaScript [MDN Tutorial]
- I made the same game in Assembly, C and C++ [YouTube Video]
- Alternative 6502 assembler and emulator [The brickout.asm project]
- Place a Message on the Character Display
Through my research, I have come up with the following list of steps in order to create this breakout game:
- Develop keyboard controllable paddle
- Draw bricks onto screen
- Develop bouncing ball
- Making the ball break bricks
- Determining game end state
- Adding messages regarding game state
Develop keyboard controllable paddle
In order to create a keyboard controllable paddle, we need to be able to draw a paddle and update the its location depending on whether the user presses the left or right arrow keys.
Here is the pseudocode in order to do this:
Get base row address in screen Add paddle position offset to base address Draw paddle starting from (base address + offset) Read one key input If key input is left arrow key: Check if current paddle position is max left If it is max left: Do nothing and return If it is not max left: Clear the paddle (for next re-render) Decrement paddle position (for next re-render) If key input is right arrow key: Check if current paddle position is max right If it is max right: Do nothing and return If it is not max right: Clear the paddle (for next re-render) Increment paddle position (for next re-render) If key input is neither left nor right: Do nothing and return
Here is the pseudocode implemented in 6502 Assembly:
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard ; Constant variables define PADCOLOR $03 ; Cyan ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) INITIALIZE: ; Runs once every new game LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS GAME_LOOP: JSR UPDATE_PADDLE JMP GAME_LOOP ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS
And here is the program output:
Draw bricks onto screen
For the bricks, all we'll do is render a single row with each pixel in the row representing a single brick. We'll have 26 bricks in total to leave some gaps on the left and right side.
For this, we need to create new variables for the brick information and add a new subroutine to draw the bricks:
; .. OTHER CODE .. ; Constant variables define BRICK_COLOR $07 ; Yellow ; .. OTHER CODE .. ; Zero-page variables for bricks define BRICKNUM $40 ; .. OTHER CODE .. INITIALIZE: LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen ; .. OTHER CODE .. ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICK_COLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; .. OTHER CODE ..
Here it is implemented into our overall codebase:
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard ; Constant variables define PADCOLOR $03 ; Cyan define BRICKCOLOR $07 ; Yellow ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) ; Zero-page variables for bricks define BRICKNUM $40 INITIALIZE: ; Runs once every new game LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen GAME_LOOP: JSR UPDATE_PADDLE JMP GAME_LOOP ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICKCOLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS
And here is the program output:
Develop bouncing ball
This where things get a bit complicated. We need to be able to animate a moving ball as well as detect collision with other objects in order to readjust its trajectory accordingly. With that said, here is the pseudocode that allows us to do just that.
Calculate ball's next x position based on current x direction Check new x if it is border (-1 or 32) If border: Bad! we have reached the border Switch x direction Keep current position, and go back to loop If not border: Get address of adjacent pixel in x direction (new x, current y) Check value in address If address empty: Good! nothing is blocking us in x direction Calculate and check y If address not empty: Bad! something is blocking us in x direction Switch x direction Keep current position, and go back to loop Calculate ball's next y position based on current y direction Check new y if it is border (-1 or 32) If border: Bad! we have reached the border Switch y direction Keep current position, and go back to loop If not border: Get address of adjacent pixel in y direction (current x, new y) Check value in address If address empty: Good! nothing is blocking us in y direction Get address using new x and y and check if its valid If address not empty: Bad! something is blocking us in y direction Switch y direction Keep current position, and go back to loop Get address using new x and y Check value in address: If address is empty: Great! we are safe to move in xy direction Fill the address with ball color Set new postion as the current postion Ball is now in new location, go back to loop If address is not empty: Bad! something is blocking us in xy direction (diagonal) Switch x direction Switch y direction Keep current position, and go back to loop
To implement this code, we need to declare and initialize these new variables
; Constant variables define BALLCOLOR $08 ; Orange ; Zero-page variables for ball define BALLX $00 ; Current ball x position define BALLY $01 ; Current ball y position define NEWBALLX $02 ; New ball x position define NEWBALLY $03 ; New ball y position define BALLXMOV $04 ; How to change x position (+1 or -1) define BALLYMOV $05 ; How to change y position (+1 or -1) define BALLPTR $06 ; Pointer to ball address in screen (low byte) define BALLPTRH $07 ; Pointer to ball address in screen (high byte) ; ... OTHER CODE ... INITIALIZE: ; Runs once every new game LDA #16 ; Set ball to start at x=16,y=28 STA BALLX LDA #28 STA BALLY LDA #$01 ; Set ball to move right (+1) STA BALLXMOV LDA #$ff ; Set ball to move up (-1) STA BALLYMOV LDX BALLX ; Get ball address from xy-pos LDY BALLY JSR GET_ADDRESS_FROM_XY LDA ADDRESSPTR STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Draw ball at ball address LDA #BALLCOLOR STA (BALLPTR),Y ; ... OTHER CODE ...
Additionally, it would be useful to create a subroutine that gives us the memory address of a pixel on a the screen using xy coordinates.
; =============================================== ; GET_ADDRESS_FROM_XY: ; General subroutine used by UPDATE_BALL to ; get an address in the bitmap screen based on ; xy coordinates ; ; Entry conditions: ; Xreg - horizontal location in bitmap screen (0-31) ; Yreg - vertical location in bitmap screen (0-31) ; ; Returns: ; Zero-page pointer to address in bitmap ; $00A0 - low byte of address in bitmap ; $00A1 - high byte of address in bitmap ; =============================================== ; Zero-page variables used by this subroutine define ADDRESSPTR $A0 define ADDRESSPTRH $A1 GET_ADDRESS_FROM_XY: STY ADDRESSPTR ; Add y-pos LDA #$00 STA ADDRESSPTRH LDY #$05 ; Do 5 left shifts to multiply y-pos by 32 MULT_ADDR: ASL ADDRESSPTR ROL ADDRESSPTRH DEY BNE MULT_ADDR CLC ; Add x-pos TXA ADC ADDRESSPTR STA ADDRESSPTR LDA #$00 ADC ADDRESSPTRH STA ADDRESSPTRH INC ADDRESSPTRH ; Add screen base Address of $0200 INC ADDRESSPTRH RTS
With all these in place, we can begin to implement the bouncing ball pseudocode into a subroutine called UPDATE_BALL
.
; =============================================== ; UPDATE_BALL: ; Subroutine to update ball movement and ; location based on the pixels ahead of the ; direction its moving in. ; ; This subroutine also updates the bricks and ; brick count when the ball hits a brick ; =============================================== UPDATE_BALL: LDA BALLX ; Calculate next x-pos CLC ADC BALLXMOV STA NEWBALLX LDA NEWBALLX ; Check next x-pos if border (-1 or 32) BMI SWITCH_X ; If next x-pos is a border (-1), switch x-direction CMP #32 BEQ SWITCH_X ; If next x-pos is a border (32), switch x-direction JMP CHECK_X_ADJ ; If next x-pos is not a border, check the adjacent address in x-direction SWITCH_X: ; Switch x-direction, then we are done updating LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV RTS ; Done updating CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y JMP SWITCH_X ; If address is not empty, Switch x-direction CHECK_Y_BORDER: ; Check next y-pos if border (-1 or 32) LDA BALLY ; Calculate next y-pos CLC ADC BALLYMOV STA NEWBALLY LDA NEWBALLY BMI SWITCH_Y ; If next y-pos is a border (-1), switch y-direction CMP #32 BEQ SWITCH_Y ; If next y-pos is a border (32), switch y-direction JMP CHECK_Y_ADJ ; If next y-pos is not a border, check the adjacent address in y-direction SWITCH_Y: ; Switch y-direction, then we are done updating LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid JMP SWITCH_Y ; If address is not empty, Switch y-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos JMP SWITCH_X_Y ; If address is not empty, switch both x and y direction SET_NEW_POS: ; Set new-pos as new ball position LDY #$00 ; Clear old ball LDA #$00 STA (BALLPTR),Y LDA NEWBALLX ; Copy new xy-pos to current xy-pos STA BALLX LDA NEWBALLY STA BALLY LDA ADDRESSPTR ; Copy new pointer to current pointer STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Color ball at new pointer LDA #BALLCOLOR STA (BALLPTR),Y RTS ; Done updating SWITCH_X_Y: ; Switch both x and y direction LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating
We can now update our game loop to update the ball after updating our paddle, like so.
GAME_LOOP: JSR UPDATE_PADDLE JSR UPDATE_BALL JMP GAME_LOOP
Here is our code after all these changes.
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard ; Constant variables define BALLCOLOR $08 ; Orange define PADCOLOR $03 ; Cyan define BRICKCOLOR $07 ; Yellow ; Zero-page variables for ball define BALLX $00 ; Current ball x position define BALLY $01 ; Current ball y position define NEWBALLX $02 ; New ball x position define NEWBALLY $03 ; New ball y position define BALLXMOV $04 ; How to change x position (+1 or -1) define BALLYMOV $05 ; How to change y position (+1 or -1) define BALLPTR $06 ; Pointer to ball address in screen (low byte) define BALLPTRH $07 ; Pointer to ball address in screen (high byte) ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) ; Zero-page variables for bricks define BRICKNUM $40 INITIALIZE: ; Runs once every new game LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS JSR UPDATE_PADDLE ; Draw paddle LDA #16 ; Set ball to start at x=16,y=28 STA BALLX LDA #28 STA BALLY LDA #$01 ; +1 ; Set ball to move right-up STA BALLXMOV LDA #$ff ; -1 STA BALLYMOV LDX BALLX ; Get ball address from xy-pos LDY BALLY JSR GET_ADDRESS_FROM_XY LDA ADDRESSPTR STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Draw ball at ball address LDA #BALLCOLOR STA (BALLPTR),Y GAME_LOOP: JSR UPDATE_PADDLE JSR UPDATE_BALL JMP GAME_LOOP ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICKCOLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS ; =============================================== ; UPDATE_BALL: ; Subroutine to update ball movement and ; location based on the pixels ahead of the ; direction its moving in. ; ; This subroutine also updates the bricks and ; brick count when the ball hits a brick ; =============================================== UPDATE_BALL: LDA BALLX ; Calculate next x-pos CLC ADC BALLXMOV STA NEWBALLX LDA NEWBALLX ; Check next x-pos if border (-1 or 32) BMI SWITCH_X ; If next x-pos is a border (-1), switch x-direction CMP #32 BEQ SWITCH_X ; If next x-pos is a border (32), switch x-direction JMP CHECK_X_ADJ ; If next x-pos is not a border, check the adjacent address in x-direction SWITCH_X: ; Switch x-direction, then we are done updating LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV RTS ; Done updating CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y JMP SWITCH_X ; If address is not empty, Switch x-direction CHECK_Y_BORDER: ; Check next y-pos if border (-1 or 32) LDA BALLY ; Calculate next y-pos CLC ADC BALLYMOV STA NEWBALLY LDA NEWBALLY BMI SWITCH_Y ; If next y-pos is a border (-1), switch y-direction CMP #32 BEQ SWITCH_Y ; If next y-pos is a border (32), switch y-direction JMP CHECK_Y_ADJ ; If next y-pos is not a border, check the adjacent address in y-direction SWITCH_Y: ; Switch y-direction, then we are done updating LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid JMP SWITCH_Y ; If address is not empty, Switch y-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos JMP SWITCH_X_Y ; If address is not empty, switch both x and y direction SET_NEW_POS: ; Set new-pos as new ball position LDY #$00 ; Clear old ball LDA #$00 STA (BALLPTR),Y LDA NEWBALLX ; Copy new xy-pos to current xy-pos STA BALLX LDA NEWBALLY STA BALLY LDA ADDRESSPTR ; Copy new pointer to current pointer STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Color ball at new pointer LDA #BALLCOLOR STA (BALLPTR),Y RTS ; Done updating SWITCH_X_Y: ; Switch both x and y direction LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating ; =============================================== ; GET_ADDRESS_FROM_XY: ; General subroutine used by UPDATE_BALL to ; get an address in the bitmap screen based on ; xy coordinates ; ; Entry conditions: ; Xreg - horizontal location in bitmap screen (0-31) ; Yreg - vertical location in bitmap screen (0-31) ; ; Returns: ; Zero-page pointer to address in bitmap ; $00A0 - low byte of address in bitmap ; $00A1 - high byte of address in bitmap ; =============================================== ; Zero-page variables used by this subroutine define ADDRESSPTR $A0 define ADDRESSPTRH $A1 GET_ADDRESS_FROM_XY: STY ADDRESSPTR ; Add y-pos LDA #$00 STA ADDRESSPTRH LDY #$05 ; Do 5 left shifts to multiply y-pos by 32 MULT_ADDR: ASL ADDRESSPTR ROL ADDRESSPTRH DEY BNE MULT_ADDR CLC ; Add x-pos TXA ADC ADDRESSPTR STA ADDRESSPTR LDA #$00 ADC ADDRESSPTRH STA ADDRESSPTRH INC ADDRESSPTRH ; Add screen base Address of $0200 INC ADDRESSPTRH RTS
And here is the output of the program:
Making the animation run smoother
We currently have a couple issues. When running the program at max speed, the ball moves too fast causing our paddle to miss way more than it can deflect. If we run our program at a slower speed the ball becomes easier to track, but our paddle becomes very flickery when it moves.
In order to solve this issue, we can try to update our paddle more times than our ball. That way, when we set the emulator to max speed, the paddle will be able to run smoother and the ball a lot slower.
To do this we need to declare a few looping variables.
; Zero-page variables for loops define REPEAT $60 define REPEAT2 $61
And update the game loop like so.
GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively, this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC DELAY_VAR BNE PADDLE_LOOP DEC DELAY_VAR2 BNE PADDLE_LOOP JSR UPDATE_BALL JMP GAME_LOOP
Here is our code with these new changes.
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard ; Constant variables define BALLCOLOR $08 ; Orange define PADCOLOR $03 ; Cyan define BRICKCOLOR $07 ; Yellow ; Zero-page variables for ball define BALLX $00 ; Current ball x position define BALLY $01 ; Current ball y position define NEWBALLX $02 ; New ball x position define NEWBALLY $03 ; New ball y position define BALLXMOV $04 ; How to change x position (+1 or -1) define BALLYMOV $05 ; How to change y position (+1 or -1) define BALLPTR $06 ; Pointer to ball address in screen (low byte) define BALLPTRH $07 ; Pointer to ball address in screen (high byte) ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) ; Zero-page variables for bricks define BRICKNUM $40 ; Zero-page variables for loops define REPEAT $60 define REPEAT2 $61 INITIALIZE: ; Runs once every new game LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS JSR UPDATE_PADDLE ; Draw paddle LDA #16 ; Set ball to start at x=16,y=28 STA BALLX LDA #28 STA BALLY LDA #$01 ; +1 ; Set ball to move right-up STA BALLXMOV LDA #$ff ; -1 STA BALLYMOV LDX BALLX ; Get ball address from xy-pos LDY BALLY JSR GET_ADDRESS_FROM_XY LDA ADDRESSPTR STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Draw ball at ball address LDA #BALLCOLOR STA (BALLPTR),Y GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC REPEAT BNE PADDLE_LOOP DEC REPEAT2 BNE PADDLE_LOOP JSR UPDATE_BALL JMP GAME_LOOP ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICKCOLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS ; =============================================== ; UPDATE_BALL: ; Subroutine to update ball movement and ; location based on the pixels ahead of the ; direction its moving in. ; ; This subroutine also updates the bricks and ; brick count when the ball hits a brick ; =============================================== UPDATE_BALL: LDA BALLX ; Calculate next x-pos CLC ADC BALLXMOV STA NEWBALLX LDA NEWBALLX ; Check next x-pos if border (-1 or 32) BMI SWITCH_X ; If next x-pos is a border (-1), switch x-direction CMP #32 BEQ SWITCH_X ; If next x-pos is a border (32), switch x-direction JMP CHECK_X_ADJ ; If next x-pos is not a border, check the adjacent address in x-direction SWITCH_X: ; Switch x-direction, then we are done updating LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV RTS ; Done updating CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y JMP SWITCH_X ; If address is not empty, Switch x-direction CHECK_Y_BORDER: ; Check next y-pos if border (-1 or 32) LDA BALLY ; Calculate next y-pos CLC ADC BALLYMOV STA NEWBALLY LDA NEWBALLY BMI SWITCH_Y ; If next y-pos is a border (-1), switch y-direction CMP #32 BEQ SWITCH_Y ; If next y-pos is a border (32), switch y-direction JMP CHECK_Y_ADJ ; If next y-pos is not a border, check the adjacent address in y-direction SWITCH_Y: ; Switch y-direction, then we are done updating LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid JMP SWITCH_Y ; If address is not empty, Switch y-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos JMP SWITCH_X_Y ; If address is not empty, switch both x and y direction SET_NEW_POS: ; Set new-pos as new ball position LDY #$00 ; Clear old ball LDA #$00 STA (BALLPTR),Y LDA NEWBALLX ; Copy new xy-pos to current xy-pos STA BALLX LDA NEWBALLY STA BALLY LDA ADDRESSPTR ; Copy new pointer to current pointer STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Color ball at new pointer LDA #BALLCOLOR STA (BALLPTR),Y RTS ; Done updating SWITCH_X_Y: ; Switch both x and y direction LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating ; =============================================== ; GET_ADDRESS_FROM_XY: ; General subroutine used by UPDATE_BALL to ; get an address in the bitmap screen based on ; xy coordinates ; ; Entry conditions: ; Xreg - horizontal location in bitmap screen (0-31) ; Yreg - vertical location in bitmap screen (0-31) ; ; Returns: ; Zero-page pointer to address in bitmap ; $00A0 - low byte of address in bitmap ; $00A1 - high byte of address in bitmap ; =============================================== ; Zero-page variables used by this subroutine define ADDRESSPTR $A0 define ADDRESSPTRH $A1 GET_ADDRESS_FROM_XY: STY ADDRESSPTR ; Add y-pos LDA #$00 STA ADDRESSPTRH LDY #$05 ; Do 5 left shifts to multiply y-pos by 32 MULT_ADDR: ASL ADDRESSPTR ROL ADDRESSPTRH DEY BNE MULT_ADDR CLC ; Add x-pos TXA ADC ADDRESSPTR STA ADDRESSPTR LDA #$00 ADC ADDRESSPTRH STA ADDRESSPTRH INC ADDRESSPTRH ; Add screen base Address of $0200 INC ADDRESSPTRH RTS
And here is the output of the program now:
Making the ball break bricks
To make the ball erase or "break" a brick whenever it hits one, we must update our UPDATE_BALL
code such that when it detects a brick color in its immediate surrounding pixels, it erases the brick from that address and decrements the number of bricks before switching the xy directions.
We need to update the code at the CHECK_X_ADJ
, CHECK_Y_ADJ
, and CHECK_NEW_POS
labels to do just that.
Here are the changes we would make to the code.
; ... OTHER CODE ... CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_X ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X ; Switch x-direction ; ... OTHER CODE ... CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_Y ; Switch x-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X_Y ; Switch both x and y direction
Here is our code incorporating these new changes.
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard ; Constant variables define BALLCOLOR $08 ; Orange define PADCOLOR $03 ; Cyan define BRICKCOLOR $07 ; Yellow ; Zero-page variables for ball define BALLX $00 ; Current ball x position define BALLY $01 ; Current ball y position define NEWBALLX $02 ; New ball x position define NEWBALLY $03 ; New ball y position define BALLXMOV $04 ; How to change x position (+1 or -1) define BALLYMOV $05 ; How to change y position (+1 or -1) define BALLPTR $06 ; Pointer to ball address in screen (low byte) define BALLPTRH $07 ; Pointer to ball address in screen (high byte) ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) ; Zero-page variables for bricks define BRICKNUM $40 ; Zero-page variables for loops define REPEAT $60 define REPEAT2 $61 INITIALIZE: ; Runs once every new game LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS JSR UPDATE_PADDLE ; Draw paddle LDA #16 ; Set ball to start at x=16,y=28 STA BALLX LDA #28 STA BALLY LDA #$01 ; +1 ; Set ball to move right-up STA BALLXMOV LDA #$ff ; -1 STA BALLYMOV LDX BALLX ; Get ball address from xy-pos LDY BALLY JSR GET_ADDRESS_FROM_XY LDA ADDRESSPTR STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Draw ball at ball address LDA #BALLCOLOR STA (BALLPTR),Y GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC REPEAT BNE PADDLE_LOOP DEC REPEAT2 BNE PADDLE_LOOP JSR UPDATE_BALL JMP GAME_LOOP ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICKCOLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS ; =============================================== ; UPDATE_BALL: ; Subroutine to update ball movement and ; location based on the pixels ahead of the ; direction its moving in. ; ; This subroutine also updates the bricks and ; brick count when the ball hits a brick ; =============================================== UPDATE_BALL: LDA BALLX ; Calculate next x-pos CLC ADC BALLXMOV STA NEWBALLX LDA NEWBALLX ; Check next x-pos if border (-1 or 32) BMI SWITCH_X ; If next x-pos is a border (-1), switch x-direction CMP #32 BEQ SWITCH_X ; If next x-pos is a border (32), switch x-direction JMP CHECK_X_ADJ ; If next x-pos is not a border, check the adjacent address in x-direction SWITCH_X: ; Switch x-direction, then we are done updating LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV RTS ; Done updating CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_X ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X ; Switch x-direction CHECK_Y_BORDER: ; Check next y-pos if border (-1 or 32) LDA BALLY ; Calculate next y-pos CLC ADC BALLYMOV STA NEWBALLY LDA NEWBALLY BMI SWITCH_Y ; If next y-pos is a border (-1), switch y-direction CMP #32 BEQ SWITCH_Y ; If next y-pos is a border (32), switch y-direction JMP CHECK_Y_ADJ ; If next y-pos is not a border, check the adjacent address in y-direction SWITCH_Y: ; Switch y-direction, then we are done updating LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_Y ; Switch x-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X_Y ; Switch both x and y direction SET_NEW_POS: ; Set new-pos as new ball position LDY #$00 ; Clear old ball LDA #$00 STA (BALLPTR),Y LDA NEWBALLX ; Copy new xy-pos to current xy-pos STA BALLX LDA NEWBALLY STA BALLY LDA ADDRESSPTR ; Copy new pointer to current pointer STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Color ball at new pointer LDA #BALLCOLOR STA (BALLPTR),Y RTS ; Done updating SWITCH_X_Y: ; Switch both x and y direction LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating ; =============================================== ; GET_ADDRESS_FROM_XY: ; General subroutine used by UPDATE_BALL to ; get an address in the bitmap screen based on ; xy coordinates ; ; Entry conditions: ; Xreg - horizontal location in bitmap screen (0-31) ; Yreg - vertical location in bitmap screen (0-31) ; ; Returns: ; Zero-page pointer to address in bitmap ; $00A0 - low byte of address in bitmap ; $00A1 - high byte of address in bitmap ; =============================================== ; Zero-page variables used by this subroutine define ADDRESSPTR $A0 define ADDRESSPTRH $A1 GET_ADDRESS_FROM_XY: STY ADDRESSPTR ; Add y-pos LDA #$00 STA ADDRESSPTRH LDY #$05 ; Do 5 left shifts to multiply y-pos by 32 MULT_ADDR: ASL ADDRESSPTR ROL ADDRESSPTRH DEY BNE MULT_ADDR CLC ; Add x-pos TXA ADC ADDRESSPTR STA ADDRESSPTR LDA #$00 ADC ADDRESSPTRH STA ADDRESSPTRH INC ADDRESSPTRH ; Add screen base Address of $0200 INC ADDRESSPTRH RTS
And here is the output of the program now:
Determining game end state
After updating our ball, we need to check the current state of the game and decide whether we win or we lose. To check if we win, we need to check the current brick count. If the brick count is zero, then we win. To check if we lose, we need to check the y position of the ball. If the ball is at y=31 then the ball has missed the paddle and escaped, so we lose.
We need to make these checks after updating our ball, like so.
GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC REPEAT BNE PADDLE_LOOP DEC REPEAT2 BNE PADDLE_LOOP JSR UPDATE_BALL LDA #31 CMP BALLY ; Check ball position BEQ GAME_END ; If ball hits the bottom edge, you lose LDA BRICKNUM ; Check status of bricks BEQ GAME_END ; If there are no more bricks, you win JMP GAME_LOOP GAME_END: BRK
Here is the program output with these changes:
Pressing ENTER to START the game
Right now our game immediately starts when we run program; however, it would be better if we give the player a chance to prepare themselves first before starting the game. Let's make it so that we wait for the user to press "enter" before starting the game.
To do that we just need to implement another loop in between INITIALIZE
and GAME_LOOP
that constantly waits for the user to press "enter".
; ... AFTER INITIALIZE ... INITIAL_LOOP: ; Loop to wait for user input before proceeding to game loop JSR CHRIN ; Read key input CMP #$0d ; ascii for "carriage return" BNE INITIAL_LOOP ; ... BEFORE GAME_LOOP ...
Here is the program with these changes:
Pressing ENTER to RESTART the game
It would also be good if we allow the user to easily restart the game after it has ended. Let's make it so that after the game ends we wait for the user to press "enter" before restarting the game.
To do this, we need to implement a loop after the GAME_LOOP
that constantly waits for the user to press "enter". When a user presses "enter", we should clear the bitmap screen and jump back to the INITIALIZE
label.
GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC REPEAT BNE PADDLE_LOOP DEC REPEAT2 BNE PADDLE_LOOP JSR UPDATE_BALL LDA #31 CMP BALLY ; Check ball position BEQ POST_GAME_LOOP ; If ball hits the bottom edge, you lose LDA BRICKNUM ; Check status of bricks BEQ POST_GAME_LOOP ; If there are no more bricks, you win JMP GAME_LOOP POST_GAME_LOOP: ; Wait for user to press enter before restarting game JSR CHRIN ; Read key input CMP #$0d ; ascii for "carriage return" BNE POST_GAME_LOOP JMP INITIALIZE
We also need to make sure that we clear the bitmap screen when we restart the game. Let's implement this at the very start of INITIALIZE
.
INITIALIZE: ; Runs once every new game LDA #$00 ; Clear bitmap screen LDY #$00 CLEAR_BITMAP: STA $0200,y STA $0300,y STA $0400,y STA $0500,y INY BNE CLEAR_BITMAP ; ... OTHER CODE ...
Here is our program with these changes:
Adding messages regarding game state
Our game is now practically complete, however a player with no context would be lost and have no idea how to play the game. Let's make use of the text screen in order to inform the player about how to play the game and whether or not they have won.
To print a message to the text screen, we can create a subroutine called PRINT_MSG
and provide to it the start address of a contiguous block of text and continuously prints a character from that text to the screen until it hits the null character.
; =============================================== ; PRINT_MSG: ; General subroutine to print a message to ; the text screen ; ; Entry conditions: ; $00B0 - low byte pointer to message ; $00B1 - high byte pointer to message ; =============================================== PRINT_MSG: JSR SCINIT ; Clear screen LDY #$00 PRINT_LOOP: LDA ($b0),y BEQ DONE_PRINT JSR CHROUT ; put a character on the screen INY BNE PRINT_LOOP DONE_PRINT: RTS
We can define our messages at the very bottom of our program instructions like so.
; =============================================== ; OTHER DATA ; =============================================== WELCOME_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"U","s","e",32,"t","h","e",32,"l","e","f","t",32,"a","n","d",32,"r","i","g","h","t",32,"a","r","r","o","w",32,"k","e","y","s",32,"t","o",13 DCB 32,32,"m","o","v","e",32,"t","h","e",32,"p","a","d","d","l","e",".",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"s","t","a","r","t",".",00 PLAY_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"G","a","m","e",32,"s","t","a","r","t","e","d",".",00 WIN_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"Y","o","u",32,"W","i","n","!",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"p","l","a","y",32,"a","g","a","i","n",".",00 LOSE_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"Y","o","u",32,"L","o","s","e",".",".",".",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"p","l","a","y",32,"a","g","a","i","n",".",00
And use the subroutine like so.
; Print welcome message LDA #<WELCOME_MSG STA $b0 LDA #>WELCOME_MSG STA $b1 JSR PRINT_MSG
Let's print the WELCOME_MSG
after initializing the variables and before the INITIAL_LOOP
; print the PLAY_MSG
after the INITIAL_LOOP
and before the GAME_LOOP
; print the LOSE_MSG
when a player loses; and print the WIN_MSG
when a player wins the game.
Here is our final program code with all the messages added in.
; ROM ROUTINES define CHRIN $ffcf ; input character from keyboard define SCINIT $ff81 ; initialize/clear screen define CHROUT $ffd2 ; output character to screen ; Constant variables define BALLCOLOR $08 ; Orange define PADCOLOR $03 ; Cyan define BRICKCOLOR $07 ; Yellow ; Zero-page variables for ball define BALLX $00 ; Current ball x position define BALLY $01 ; Current ball y position define NEWBALLX $02 ; New ball x position define NEWBALLY $03 ; New ball y position define BALLXMOV $04 ; How to change x position (+1 or -1) define BALLYMOV $05 ; How to change y position (+1 or -1) define BALLPTR $06 ; Pointer to ball address in screen (low byte) define BALLPTRH $07 ; Pointer to ball address in screen (high byte) ; Zero-page variables for paddle define PADWIDTH $20 ; How long is the paddle define PADPOS $21 ; How many pixels from the left to start rendering paddle define PADMAXPOS $22 ; Maximum value for PADPOS define PADPTR $23 ; Screen address (low byte) define PADPTRH $24 ; Screen address (high byte) ; Zero-page variables for bricks define BRICKNUM $40 ; Zero-page variables for loops define REPEAT $60 define REPEAT2 $61 INITIALIZE: ; Runs once every new game LDA #$00 ; Clear bitmap screen LDY #$00 CLEAR_BITMAP: STA $0200,y STA $0300,y STA $0400,y STA $0500,y INY BNE CLEAR_BITMAP LDA #26 ; Set num bricks to 26 STA BRICKNUM JSR DRAW_BRICKS ; Draw bricks onto screen LDA #8 ; Set paddle width to 8 pixels STA PADWIDTH LDA #24 ; Set paddle max pos to x=24 STA PADMAXPOS LDA #12 ; Set paddle start pos at x=12 STA PADPOS JSR UPDATE_PADDLE ; Draw paddle LDA #16 ; Set ball to start at x=16,y=28 STA BALLX LDA #28 STA BALLY LDA #$01 ; +1 ; Set ball to move right-up STA BALLXMOV LDA #$ff ; -1 STA BALLYMOV LDX BALLX ; Get ball address from xy-pos LDY BALLY JSR GET_ADDRESS_FROM_XY LDA ADDRESSPTR STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Draw ball at ball address LDA #BALLCOLOR STA (BALLPTR),Y ; Print welcome message LDA #<WELCOME_MSG STA $b0 LDA #>WELCOME_MSG STA $b1 JSR PRINT_MSG INITIAL_LOOP: ; Loop to wait for user input before proceeding to game loop JSR CHRIN ; Read key input CMP #$0d ; ascii for "carriage return" BNE INITIAL_LOOP ; Print play message LDA #<PLAY_MSG STA $b0 LDA #>PLAY_MSG STA $b1 JSR PRINT_MSG GAME_LOOP: LDA #$00 ; Update the paddle more times than the ball so that the ball moves more slowly STA REPEAT LDA #$03 STA REPEAT2 PADDLE_LOOP: ; Effectively this updates the paddle 259 times before updating the ball JSR UPDATE_PADDLE DEC REPEAT BNE PADDLE_LOOP DEC REPEAT2 BNE PADDLE_LOOP JSR UPDATE_BALL LDA #31 CMP BALLY ; Check ball position BEQ GAME_LOSE ; If ball hits the bottom edge, you lose LDA BRICKNUM ; Check status of bricks BEQ GAME_WIN ; If there are no more bricks, you win JMP GAME_LOOP GAME_LOSE: ; Print lose message LDA #<LOSE_MSG STA $b0 LDA #>LOSE_MSG STA $b1 JSR PRINT_MSG JMP POST_GAME_LOOP GAME_WIN: ; Print win message LDA #<WIN_MSG STA $b0 LDA #>WIN_MSG STA $b1 JSR PRINT_MSG JMP POST_GAME_LOOP POST_GAME_LOOP: ; Wait for user to press enter before restarting game JSR CHRIN ; Read key input CMP #$0d ; ascii for "carriage return" BNE POST_GAME_LOOP JMP INITIALIZE ; =============================================== ; DRAW_BRICKS: ; Subroutine to draw the bricks on the screen ; =============================================== DRAW_BRICKS: LDY #0 LDA #BRICKCOLOR BRICK_LOOP: STA $0263,Y INY CPY BRICKNUM BNE BRICK_LOOP RTS ; =============================================== ; UPDATE_PADDLE: ; Subroutine for updating paddle location ; based on user input ; =============================================== UPDATE_PADDLE: LDA #$A0 ; Setup paddle base-row address ($05C0) STA PADPTR LDA #$05 STA PADPTRH CLC ; Add x-pos to base-row to get our start address for drawing LDA PADPOS ADC PADPTR STA PADPTR LDA #$00 ADC PADPTRH STA PADPTRH LDY #$00 ; Draw paddle from start address LDA #PADCOLOR DRAW_PADDLE: STA (PADPTR),Y INY CPY PADWIDTH BNE DRAW_PADDLE JSR CHRIN ; Read key input CMP #$83 ; ascii for "left arrow key" BEQ MOVE_PADDLE_LEFT CMP #$81 ; ascii for "right" BEQ MOVE_PADDLE_RIGHT RTS ; If neither "left" nor "right, do nothing (return) MOVE_PADDLE_LEFT: LDA #0 ; MAX left-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX left, don't move left (return) JSR CLEAR_PADDLE ; clear paddle for re-render DEC PADPOS ; decrement paddle position RTS MOVE_PADDLE_RIGHT: LDA PADMAXPOS ; MAX right-pos CMP PADPOS BEQ END_UPDATE_PADDLE ; if PADPOS is MAX right, don't move right (return) JSR CLEAR_PADDLE ; clear paddle for re-render INC PADPOS ; increment paddle position END_UPDATE_PADDLE: RTS ; =============================================== ; CLEAR_PADDLE: ; Subroutine used by UPDATE_PADDLE to clear ; the old paddle before rendering a new one ; =============================================== CLEAR_PADDLE: LDY #$00 LDA #$00 CLEAR_LOOP: STA (PADPTR),Y INY CPY PADWIDTH BNE CLEAR_LOOP RTS ; =============================================== ; UPDATE_BALL: ; Subroutine to update ball movement and ; location based on the pixels ahead of the ; direction its moving in. ; ; This subroutine also updates the bricks and ; brick count when the ball hits a brick ; =============================================== UPDATE_BALL: LDA BALLX ; Calculate next x-pos CLC ADC BALLXMOV STA NEWBALLX LDA NEWBALLX ; Check next x-pos if border (-1 or 32) BMI SWITCH_X ; If next x-pos is a border (-1), switch x-direction CMP #32 BEQ SWITCH_X ; If next x-pos is a border (32), switch x-direction JMP CHECK_X_ADJ ; If next x-pos is not a border, check the adjacent address in x-direction SWITCH_X: ; Switch x-direction, then we are done updating LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV RTS ; Done updating CHECK_X_ADJ: ; Check adjacent address in x-direction LDX NEWBALLX ; Get adjacent address in x-direction of ball (new-x, cur-y) LDY BALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_Y_BORDER ; If address is empty, we are good to move in this direction. Go check y CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_X ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X ; Switch x-direction CHECK_Y_BORDER: ; Check next y-pos if border (-1 or 32) LDA BALLY ; Calculate next y-pos CLC ADC BALLYMOV STA NEWBALLY LDA NEWBALLY BMI SWITCH_Y ; If next y-pos is a border (-1), switch y-direction CMP #32 BEQ SWITCH_Y ; If next y-pos is a border (32), switch y-direction JMP CHECK_Y_ADJ ; If next y-pos is not a border, check the adjacent address in y-direction SWITCH_Y: ; Switch y-direction, then we are done updating LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating CHECK_Y_ADJ: ; Check adjacent address in x-direction LDX BALLX ; Get adjacent address in y-direction of ball (cur-x, new-y) LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check the value in address LDA (ADDRESSPTR),Y BEQ CHECK_NEW_POS ; If address is empty, we are good to move in this direction. Go check if new xy-pos is valid CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_Y ; Switch x-direction CHECK_NEW_POS: ; Check address in new-pos LDX NEWBALLX ; Get new-pos address based on new-x and new-y LDY NEWBALLY JSR GET_ADDRESS_FROM_XY ; Calculate address from xy and store it to ADDRESSPTR LDY #$00 ; Check value in new-pos LDA (ADDRESSPTR),Y BEQ SET_NEW_POS ; If address is empty, we are good to move in this direction. Set and color this new-pos CMP #BRICKCOLOR ; If address is not empty, check if this address contains a brick BNE SWITCH_Y ; If it is not a brick, go ahead to switch x-direction LDA #$00 STA (ADDRESSPTR),Y ; If it is a brick, erase the brick DEC BRICKNUM ; Decrement brick count JMP SWITCH_X_Y ; Switch both x and y direction SET_NEW_POS: ; Set new-pos as new ball position LDY #$00 ; Clear old ball LDA #$00 STA (BALLPTR),Y LDA NEWBALLX ; Copy new xy-pos to current xy-pos STA BALLX LDA NEWBALLY STA BALLY LDA ADDRESSPTR ; Copy new pointer to current pointer STA BALLPTR LDA ADDRESSPTRH STA BALLPTRH LDY #$00 ; Color ball at new pointer LDA #BALLCOLOR STA (BALLPTR),Y RTS ; Done updating SWITCH_X_Y: ; Switch both x and y direction LDA BALLXMOV EOR #$fe ; This toggles x-direction between 1 and -1 STA BALLXMOV ; Store result of toggle back to BALLXMOV LDA BALLYMOV EOR #$fe ; This toggles y-direction between 1 and -1 STA BALLYMOV ; Store result of toggle back to BALLYMOV RTS ; Done updating ; =============================================== ; GET_ADDRESS_FROM_XY: ; General subroutine used by UPDATE_BALL to ; get an address in the bitmap screen based on ; xy coordinates ; ; Entry conditions: ; Xreg - horizontal location in bitmap screen (0-31) ; Yreg - vertical location in bitmap screen (0-31) ; ; Returns: ; Zero-page pointer to address in bitmap ; $00A0 - low byte of address in bitmap ; $00A1 - high byte of address in bitmap ; =============================================== ; Zero-page variables used by this subroutine define ADDRESSPTR $A0 define ADDRESSPTRH $A1 GET_ADDRESS_FROM_XY: STY ADDRESSPTR ; Add y-pos LDA #$00 STA ADDRESSPTRH LDY #$05 ; Do 5 left shifts to multiply y-pos by 32 MULT_ADDR: ASL ADDRESSPTR ROL ADDRESSPTRH DEY BNE MULT_ADDR CLC ; Add x-pos TXA ADC ADDRESSPTR STA ADDRESSPTR LDA #$00 ADC ADDRESSPTRH STA ADDRESSPTRH INC ADDRESSPTRH ; Add screen base Address of $0200 INC ADDRESSPTRH RTS ; =============================================== ; PRINT_MSG: ; General subroutine to print a message to ; the text screen ; ; Entry conditions: ; $00B0 - low byte pointer to message ; $00B1 - high byte pointer to message ; =============================================== PRINT_MSG: JSR SCINIT ; Clear screen LDY #$00 PRINT_LOOP: LDA ($b0),y BEQ DONE_PRINT JSR CHROUT ; put a character on the screen INY BNE PRINT_LOOP DONE_PRINT: RTS ; =============================================== ; OTHER DATA ; =============================================== WELCOME_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"U","s","e",32,"t","h","e",32,"l","e","f","t",32,"a","n","d",32,"r","i","g","h","t",32,"a","r","r","o","w",32,"k","e","y","s",32,"t","o",13 DCB 32,32,"m","o","v","e",32,"t","h","e",32,"p","a","d","d","l","e",".",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"s","t","a","r","t",".",00 PLAY_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"G","a","m","e",32,"s","t","a","r","t","e","d",".",00 WIN_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"Y","o","u",32,"W","i","n","!",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"p","l","a","y",32,"a","g","a","i","n",".",00 LOSE_MSG: DCB 13 DCB 32,32,"A","S","S","E","M","B","L","Y",32,"B","R","E","A","K","O","U","T",13 DCB 13 DCB 32,32,"Y","o","u",32,"L","o","s","e",".",".",".",13 DCB 13 DCB 32,32,"P","r","e","s","s",32,"e","n","t","e","r",32,"t","o",32,"p","l","a","y",32,"a","g","a","i","n",".",00
And here is our final game!
Thoughts
This has definitely been the most challenging lab out of three labs we've done so far but also the most rewarding. I knew I've always wanted to learn assembly but never really knew that I'd end up coding a game in it! Though the game can still be improved, like giving the player more chances to break all bricks or creating more brick rows, I believe what I have created is already enough and is pretty much a full functioning breakout game. Going back to the lab criteria, this program works in the 6502 Emulator; It outputs to the character screen by informing the user about the game states; It outputs to the bitmap screen by running the actual game; It accepts user input from the keyboard as it uses the "enter" key to start/restart the game and the "left" and "right" arrow keys to move the paddle; and it uses a lot of arithmetic functions like EOR to toggle ball direction, AOL and ROR for multiplying, DEC for subtracting the brick counts, etc.
Overall, I definitely learned a lot in the lab. I started from scratch and got to learn how to create my own subroutines, work with loops and conditions, and overall developed a method for structuring my data and naming my variables. This lab really drove me to learn how to think in assembly which I believe will be really useful as I progress through my career as a software engineer.
Comments
Post a Comment