7. subroutines, game layout, starting Pong¶
This Week
Most of this lesson is about how to organize and structure your game. Subroutines and game states help arrange the code for easier reading and reuse of code.
7.1. Variables¶
As covered in week 1, variables are data stored in RAM that you can change any time. The sprite data in RAM is all variables. You will need more variables for keeping track of things like the score in the game. To do that you first need to tell NESASM where in RAM to put the variable. This is done using the .rsset and .rs directives. First .rsset is used to set the starting address of the variable. Then .rs is used to reserve space. Usually just 1 byte is reserved, but you can have as much as you want. Each time you do a .rs the address gets incremented so you don’t need to do .rsset again:
.rsset $0000 ;put variables starting at 0
score1 .rs 1 ;put score for player 1 at $0000
score2 .rs 1 ;put score for player 2 at $0001
buttons1 .rs 1 ;put controller data for player 1 at $0002
buttons2 .rs 1 ;put controller data for player 2 at $0003
Once you set the address for the variable, you do not need to know the address anymore. You can just reference it using the variable name you created. You can insert more variables above the current ones and the assembler will automatically recalculate the addresses.
7.2. Constants¶
Constants are numbers that you do not change. They are just used to make your code easier to read. In Pong an example of a constant would be the position of the outer walls. You will need to compare the ball position to the walls to make the ball bounce, but the walls do not change so they are good constants. Doing a compare to LEFTWALL is easier to read and understand than a comparison to $F6.
To declare constants you use the = sign:
RIGHTWALL = $02 ; when ball reaches one of these, do something
TOPWALL = $20
BOTTOMWALL = $D8
LEFTWALL = $F6
The assembler will then do a find/replace when building your code.
7.3. Subroutines¶
As your program gets larger, you will want subroutines for organization and to reuse code. Instead of progressing linearly down your code, a subroutine is a block of code located somewhere else that you jump to, then return from. The subroutine can be called at any time, and used as many times as you want. Here is what some code looks like without subroutines:
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
vblankwait1: ; First wait for vblank to make sure PPU is ready
BIT $2002
BPL vblankwait1
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
vblankwait2: ; Second wait for vblank, PPU is ready after this
BIT $2002
BPL vblankwait2
Notice that the vblankwait is done twice, so it is a good choice to turn into a subroutine. First the vblankwait code is moved outside the normal linear flow:
vblankwait: ; wait for vblank
BIT $2002
BPL vblankwait
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
Then that code needs to be called, so the JSR (Jump to SubRoutine) instruction is where the vblankwait code used to be:
RESET:
SEI ; disable IRQs
CLD ; disable decimal mode
JSR vblankwait ;;jump to vblank wait #1
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
JSR vblankwait ;; jump to vblank wait again
And then when the subroutine has finished, it needs to return back to the spot it was called from. This is done with the RTS (ReTurn from Subroutine) instruction. The RTS will jump back to the next instruction after the JSR:
vblankwait: ; wait for vblank <--------
BIT $2002 \
BPL vblankwait |
----- RTS |
/ |
| RESET: |
| SEI ; disable IRQs |
| CLD ; disable decimal mode |
| |
| JSR vblankwait ;;jump to vblank wait #1 --/
|
\--> clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
JSR vblankwait ;; jump to vblank wait again, returns here
7.4. Better Controller Reading¶
Now that you can set up subroutines, you can do much better controller reading. Previously the controller was read as it was processed. With multiple game states, that would mean many copies of the same controller reading code. This is replaced with one controller reading subroutine that saves the button data into a variable. That variable can then be checked in many places without having to read the whole controller again:
ReadController:
LDA #$01
STA $4016
LDA #$00
STA $4016
LDX #$08
ReadControllerLoop:
LDA $4016
LSR A ; bit0 -> Carry
ROL buttons ; bit0 <- Carry
DEX
BNE ReadControllerLoop
RTS
This code uses two new instructions. The first is LSR (Logical Shift Right). This takes each bit in A and shifts them over 1 position to the right. Bit 7 is filled with a 0, and bit 0 is shifted into the Carry flag:
bit number 7 6 5 4 3 2 1 0 carry
original data 1 0 0 1 1 0 1 1 0
\ \ \ \ \ \ \ \
\ \ \ \ \ \ \ \
shifted data 0 1 0 0 1 1 0 1 1
Each bit position is a power of 2, so LSR is the same thing as divide by 2.
The next new instruction is ROL (ROtate Left) which is the opposite of LSR. Each bit is shifted to the left by one position. The Carry flag is put into bit 0. This is the same as a multiply by 2.
These instructions are used together in a clever way for controller reading. When each button is read, the button data is in bit 0. Doing the LSR puts the button data into Carry. Then the ROL shifts the previous button data over and puts Carry back to bit 0. The following diagram shows the values of Accumulator and buttons data at each step of reading the controller:
Accumulator | buttons data | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Carry | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Carry | |
read button A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
LSR A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | |
ROL buttons | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | |
read button B | 0 | 0 | 0 | 0 | 0 | 0 | 0 | B | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | |
LSR A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | B | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | B | |
ROL buttons | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | B | 0 | |
read button SEL | 0 | 0 | 0 | 0 | 0 | 0 | 0 | SEL | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | |
LSR A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | SEL | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | SEL | |
ROL buttons | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | B | SEL | 0 | |
read button STA | 0 | 0 | 0 | 0 | 0 | 0 | 0 | STA | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | 0 | |
LSR A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | STA | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | STA | |
ROL buttons | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | A | B | SEL | STA | 0 |
The loop continues for a total of 8 times to read all buttons. When it is done there is one button in each bit:
bit: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
button: | A | B | select | start | up | down | left | right |
If the bit is 1, that button is pressed.
7.5. Game Layout¶
The Pong game engine will use the typical simple NES game layout. First all the initialization is done. This includes clearing out RAM, setting up the PPU, and loading in the title screen graphics. Then it enters an infinite loop, waiting for the NMI to happen. When the NMI hits the PPU is ready to accept all graphics updates. There is a short time to do these so code like sprite DMA is done first. When all graphics are done the actual game engine starts. The controllers are read, then game processing is done. The sprite position is updated in RAM, but does not get updated until the next NMI. Once the game engine has finished it goes back to the infinite loop:
Init Code -> Infinite Loop -> NMI -> Graphics Updates -> Read Buttons -> Game Engine --\
^ |
\------------------------------------------------------------------/
7.6. Game State¶
The use of a “game state” variable is a common way to arrange code. The game state just specifies what code should be run in the game engine each frame. If the game is in the title screen state, then none of the ball movement code needs to be run. A flow chart can be created that includes what each state should do, and the next state that should be set when it is done. For Pong there are just 3 basic states:
->Title State /--> Playing State /--> Game Over State
/ wait for start button --/ move ball / wait for start button -\
| move paddles | \
| check for collisions / |
| check for score = 15 -/ |
\ /
\-----------------------------------------------------------------------------------/
The next step is to add much more detail to each state to figure out exactly what is needed. These layouts are done before any significant coding starts. Some of the game engine like the second player and the score will be added later. Without the score there is no way to get to the Game Over State yet
Title State:
if start button pressed
turn screen off
load game screen
set paddle/ball position
go to Playing State
turn screen on
Playing State:
move ball
if ball moving right
add ball speed x to ball position x
if ball x > right wall
bounce, ball now moving left
if ball moving left
subtract ball speed x from ball position x
if ball x < left wall
bounce, ball now moving right
if ball moving up
subtract ball speed y from ball position y
if ball y < top wall
bounce, ball now moving down
if ball moving down
add ball speed y to ball position y
if ball y > bottom wall
bounce, ball now moving up
if up button pressed
if paddle top > top wall
move paddle top and bottom up
if down button pressed
if paddle bottom < bottom wall
move paddle top and bottom down
if ball x < paddle1x
if ball y > paddle y top
if ball y < paddle y bottom
bounce, ball now moving left
Game Over State:
if start button pressed
turn screen off
load title screen
go to Title State
turn screen on
7.7. Putting It All Together¶
Download and unzip the master.zip sample files. This lesson is in pong1. The playing game state and ball movement code is in the pong1.asm file. Make sure that file, mario.chr, and pong1.bat is in the same folder as NESASM3, then double click on pong1.bat. That will run NESASM3 and should produce pong1.nes. Run that NES file in FCEUXD SP to see the ball moving!
Other code segments have been set up but not yet completed. See how many of those you can program yourself. The main parts missing are the paddle movements and paddle/ball collisions. You can also add the intro state and the intro screen, and the playing screen using the background information from the previous week.