How to view/capture the inputs on $4016/4017
Moderator: Moderators
How to view/capture the inputs on $4016/4017
Hi
I have a question:
The I/O on nes are on $ 4016 / $ 4017,
Usually (nes, snes, gb ...). I don't read this but I look in ram, for example in batman on nes it's the bits of the addresses $f5 and $f7 which can be used to see if inputs from the pad
But if in batman or other, I want to look in $4016/4017 I can't see the inputs
how do you see these inputs on $4016/4017 ??
I have a question:
The I/O on nes are on $ 4016 / $ 4017,
Usually (nes, snes, gb ...). I don't read this but I look in ram, for example in batman on nes it's the bits of the addresses $f5 and $f7 which can be used to see if inputs from the pad
But if in batman or other, I want to look in $4016/4017 I can't see the inputs
how do you see these inputs on $4016/4017 ??
- Controllerhead
- Posts: 314
- Joined: Tue Nov 13, 2018 4:58 am
- Location: $4016
- Contact:
Re: How to view/capture the inputs on $4016/4017
You can't, nor would it be all that helpful. What you can do is set a breakpoint to see when the program reads $4016/$17 and follow the code in a debugger to see what it does with these values. Each game will be different.
https://wiki.nesdev.com/w/index.php/Standard_controller
Re: How to view/capture the inputs on $4016/4017
To further clarify, $4016 and $4017 are memory-mapped I/O registers which perform special actions when you access them, so you can't just look at them as if they were RAM to see which buttons are pressed.
Normal controllers are read using the following procedure:
Normal controllers are read using the following procedure:
- Write #$01 to $4016 (to instruct both controllers to start recording the current states of all pressed buttons)
- Write #$00 to $4016 (to stop recording button states so they can be read out)
- Read $4016 eight times and examine bit 0 on each read - if set, it means a specific button (in order: A, B, Select, Start, Up, Down, Left, and Right) is being pressed
- Read $4017 eight times and examine bit 0 on each read (as above)
Quietust, QMT Productions
P.S. If you don't get this note, let me know and I'll write you another.
P.S. If you don't get this note, let me know and I'll write you another.
Re: How to view/capture the inputs on $4016/4017
THANKS YOU
It seems clearer to me
It seems clearer to me
-
- Posts: 160
- Joined: Sat Apr 24, 2021 7:25 am
Re: How to view/capture the inputs on $4016/4017
The way the NES gets the controller data is pretty inefficient all things considered. Almost every game did the following trick to make things so much easier:
This looks really weird and confusing but I'll go over why this works.
When you first want to get the controller data, like Quietust said, you have to do the following:
This is called "strobing the keys." My understanding is that this process essentially takes a snapshot of which buttons were pressed at that very moment and which ones were not. (Strobing the keys works on BOTH controllers simultaneously even though it looks like you're only strobing Player 1's controller.) Then to get the data you need to read the controller you're interested in 8 times. $4016 for player 1 and $4017 for player 2. So let's say that for example I was holding Player 1's controller and was holding A, Start, and Right when the keys were strobed. I'll go over the value of each read in sequence. The NES always tells you the keys in the following order: A B, Select, Start, Up, Down, Left, Right.
LDA $4016 (A = 1)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 1)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
Now we could just put a compare statement after each LDA and branch to the code that is meant to run when you press that button. But wouldn't it be nice if the NES instead output all 8 bits into the accumulator at once? 8 bits, 8 buttons. Seems reasonable. And this is precisely what the above code does. I'll explain it line by line. To simplify things we'll only concern ourselves with controller 1.
What makes this code so clever is that using ROL (rotate left) will ensure that each time we loop and store the next button, the previous buttons pressed (or not pressed), and the identity of each button, are all preserved. So if I pressed A, Start, and Right, after this loop is all said and done, the variable "joypad" will equal #%10010001.
Code: Select all
ldx #8
.loop:
lda $4016
lsr a
rol joypad1 ;A, B, select, start, up, down, left, right
lda $4017
lsr a
rol joypad2
dex
bne .loop
When you first want to get the controller data, like Quietust said, you have to do the following:
Code: Select all
LDA #$01
STA $4016
LDA #$00
STA $4016
LDA $4016 (A = 1)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 1)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
LDA $4016 (A = 0)
Now we could just put a compare statement after each LDA and branch to the code that is meant to run when you press that button. But wouldn't it be nice if the NES instead output all 8 bits into the accumulator at once? 8 bits, 8 buttons. Seems reasonable. And this is precisely what the above code does. I'll explain it line by line. To simplify things we'll only concern ourselves with controller 1.
Code: Select all
ldx #8 ;we want to loop 8 times, once per button
.loop:
lda $4016 ;get the button press data into accumulator.
lsr a ;push the button into the carry flag. If the button was pressed, carry flag is set. If not, carry is clear.
rol joypad1 ;this stores the button into the first bit of the zero page entry "joypad1."
dex ;decrement x
bne .loop ;if x isn't 0, return to beginning of the loop.
Re: How to view/capture the inputs on $4016/4017
Thanks you for good tricks
Re: How to view/capture the inputs on $4016/4017
It's also good practice to read both bit 0 and bit 1 of $4016 and $4017. Bit 0 contains controller I and II, while bit 1 contains the input state of controller III and IV respectively. Controller III and IV are the controllers connected to the expansion port on Famicom (and also to the bottom larger expansion port on NES with the right adapter).
This is easily done by adding an extra LSR ROL sequence after the first one to rotate in bit 1 as well. Do this for both $4016 and $4017 (if the game uses two controllers) like this:
In this example the input states are written into the following RAM variables:
con_state+0 = controller I
con_state+1 = controller II
temp+0 = controller III
temp+1 = controller IV
The strobe output is connected to the expansion port as well, so controller III and IV also gets strobed when you write the 1 and 0 sequence to $4016. So no need to change the strobing part of your code.
You also need to merge controller III with I and IV with II (unless you want them to be separate controllers in a 4-player game).
This can be done with ORA:
By doing this, people with a Famicom (like me) can play your game with expansion port controllers as an alternative to the normal controllers. This is important since Famicom have the two normal controllers hardwired with very short cords. Controller II is also missing START and SELECT buttons, but expansion controllers have them (which means player II may be able to pause the game).
Not every licensed games did this, but a good number of them do. Homebrew seldom does this, which is why I'm telling you it's a good thing to do.
BTW controller III and IV here has nothing to do with the two extra controllers in a Four Score or other NES multitaps, those are different as they appear serially on bit 0 of $4016 and $4017 and not on bit 1.
This is a simple way to read all 8 buttons through a single pin. If the controller didn't have a 4021, it would need one pin for each button like Atari and Sega joysticks have, and read them parallelly (but Atari and Sega joysticks only has 5 and 6 buttons respectively including the joystick's 4 switches).
I'm not sure if Nintendo saved money or space on the Famicom mother board by using a 4021 for serial reading instead of just adding a pin for each button. An IC chip sounds more expensive than a larger controller connector to me. Reading serially is much slower than reading parallelly, but the time it takes to read the controllers isn't exactly massive anyway.
Someone would have to correct me if I'm wrong, but I think that the 4021 has an additional advantage in that it takes care of button contact bounce (the fact that buttons does microscopic bounces between the finger and the contact surface when pressed which the NES would interpret as a large number of super quick presses when the player thinks he only pressed the button once) and maybe it even inverts the buttons so that 1=pressed instead of 0 (due to buttons being the "normally open" type which closes the electric circuit when pressed). Checking 4021 data sheets I can't see anything mentioning of inverting though, so maybe that's done inside the NES.
BTW 4021 is a common logic IC that you can buy in electronic parts stores. You can thus quite easily build a NES controller yourself.
SNES controllers uses two 4021 chips to add a few extra buttons, but otherwise they work exactly the same way and are even compatible with NES. The Virtual Boy controller also works the same way but adding two more buttons than the SNES.
This is easily done by adding an extra LSR ROL sequence after the first one to rotate in bit 1 as well. Do this for both $4016 and $4017 (if the game uses two controllers) like this:
Code: Select all
.loop:
lda $4016 ;get button data from D0 (con I) and D1 (con III)
lsr a ;shift button data from D0 into carry
rol con_state+0 ;rotate carry into a RAM register (con I buttons)
lsr a ;shift button data from D1 into carry
rol temp+0 ;rotate carry into a RAM register (con III buttons)
lda $4017 ;get button data from D0 (con II) and D1 (con IV)
lsr a ;shift button data from D0 into carry
rol con_state+1 ;rotate carry into a RAM register (con II buttons)
lsr a ;shift button data from D1 into carry
rol temp+1 ;rotate carry into a RAM register (con IV buttons)
dex
bne .loop
con_state+0 = controller I
con_state+1 = controller II
temp+0 = controller III
temp+1 = controller IV
The strobe output is connected to the expansion port as well, so controller III and IV also gets strobed when you write the 1 and 0 sequence to $4016. So no need to change the strobing part of your code.
You also need to merge controller III with I and IV with II (unless you want them to be separate controllers in a 4-player game).
This can be done with ORA:
Code: Select all
lda temp+0
ora con_state+0
sta con_state+0 ;OR-merge con III with con I
lda temp+1
ora con_state+1
sta con_state+1 ;OR-merge con IV with con II
Not every licensed games did this, but a good number of them do. Homebrew seldom does this, which is why I'm telling you it's a good thing to do.
BTW controller III and IV here has nothing to do with the two extra controllers in a Four Score or other NES multitaps, those are different as they appear serially on bit 0 of $4016 and $4017 and not on bit 1.
Yeah you could say that. The controllers are simple devices using a logic chip called "4021". The 4021 is a shift register IC (AKA a parallel-to-serial converter) that is activated by writing 1 and 0 to one of its pins. The $4016 output port is connected to this pin on the controller which explains why strobing works to activate the "snapshot". The controller buttons are each connected to a pin of the 4021, and when activated it will read all those pins and then send them one at a time through another pin which is connected to one of the input ports (either $4016 bit 0, $4017 bit 0, $4016 bit 1 or $4017 bit 1 depending on which controller port it is connected to).puppydrum64 wrote: ↑Tue May 18, 2021 8:51 pm This is called "strobing the keys." My understanding is that this process essentially takes a snapshot of which buttons were pressed at that very moment and which ones were not.
This is a simple way to read all 8 buttons through a single pin. If the controller didn't have a 4021, it would need one pin for each button like Atari and Sega joysticks have, and read them parallelly (but Atari and Sega joysticks only has 5 and 6 buttons respectively including the joystick's 4 switches).
I'm not sure if Nintendo saved money or space on the Famicom mother board by using a 4021 for serial reading instead of just adding a pin for each button. An IC chip sounds more expensive than a larger controller connector to me. Reading serially is much slower than reading parallelly, but the time it takes to read the controllers isn't exactly massive anyway.
Someone would have to correct me if I'm wrong, but I think that the 4021 has an additional advantage in that it takes care of button contact bounce (the fact that buttons does microscopic bounces between the finger and the contact surface when pressed which the NES would interpret as a large number of super quick presses when the player thinks he only pressed the button once) and maybe it even inverts the buttons so that 1=pressed instead of 0 (due to buttons being the "normally open" type which closes the electric circuit when pressed). Checking 4021 data sheets I can't see anything mentioning of inverting though, so maybe that's done inside the NES.
BTW 4021 is a common logic IC that you can buy in electronic parts stores. You can thus quite easily build a NES controller yourself.
SNES controllers uses two 4021 chips to add a few extra buttons, but otherwise they work exactly the same way and are even compatible with NES. The Virtual Boy controller also works the same way but adding two more buttons than the SNES.
-
- Posts: 160
- Joined: Sat Apr 24, 2021 7:25 am
Re: How to view/capture the inputs on $4016/4017
Here's my new and improved code for my game (since it's going to be a Famicom game rather than NES to take advantage of the VRC6's audio I may as well take advantage of the expansion ports.) I own a Famicom as well and those cords are short but luckily the cord to the TV is very long and my setup is close to my bed so I just set the Famicom on the edge of the bed and play it that way
This seems to work from what I can tell. I haven't fully separated player 1 and player 2 yet but I'm working on that. I'll probably just use something simple like TYA followed by BNE until I can come up with a better way.
Code: Select all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CONTROLLER SUBROUTINES;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
read_joypad:
ldy #1
lda joypad1
sta joypad1_old ;save last frame's joypad button states
lda joypad2
sta joypad2_old
lda #1
sta $4016
lda #0
sta $4016
ldx #8
@loop:
lda $4016
lsr a
rol joypad1 ;A, B, select, start, up, down, left, right
;;FAMICOM EXPANSION PORT SUPPORT
lsr a
rol joypad3
;------------------------------
lda $4017
lsr a
rol joypad2
;;FAMICOM EXPANSION PORT SUPPORT
lsr a
rol joypad4
;------------------------------
dex
bne @loop
;Merge Controller 3 with 1 and 4 with 2.
lda joypad3
ora joypad1
sta joypad1
lda joypad4
ora joypad2
sta joypad2
lda joypad1_old ;what was pressed last frame. EOR to flip all the bits to find ...
eor #$FF ;what was not pressed last frame
and joypad1 ;what is pressed this frame
sta joypad1_pressed ;stores off-to-on transitions
lda joypad1_old ;what was pressed last frame.
and joypad1 ;what was pressed this frame. Combine them to find...
sta joypad1_held ;stores buttons held down.
lda joypad1 ;what is pressed this frame. EOR to flip all the bits to find...
eor #$FF ;what is not pressed this frame
and joypad1_old ;what was pressed last frame
sta joypad1_release ;stores on-to-off transitions
;---------------------
; handle_input will perform actions based on input:
handle_input:
LDX #$00
loop_handle_input:
@check_A:
lda joypad2_pressed,y ;because y=1, joypad 1 is actually placed first. Variables are positioned that way.
and #BUTTON_A
beq @check_B
jsr pressA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_B:
lda joypad2_pressed,y
and #BUTTON_B
beq @check_select
jsr pressB
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_select
lda joypad2_pressed,y
and #BUTTON_SELECT
beq @check_start
jsr pressSelect
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_start:
lda joypad2_pressed,y
and #BUTTON_START
beq @check_up
jsr pressStart
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_up:
lda joypad2,y
and #BUTTON_UP
beq @check_down
jsr pressUp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_down:
lda joypad2,y
and #BUTTON_DOWN
beq @check_left
jsr pressDown
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_left:
lda joypad2,y
and #BUTTON_LEFT
beq @check_right
jsr pressLeft
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@check_right:
lda joypad2,y
and #BUTTON_RIGHT
beq doneReadingControllers
jsr pressRight
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
doneReadingControllers:
ldx #$01
dey
bne loop_handle_input
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressA:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS A GOES HERE
LDA lockUserInput
bne @nomoving
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressB:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS B GOES HERE
LDA lockUserInput
bne @nomoving
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressSelect:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS SELECT GOES HERE
LDA lockUserInput
bne @nomoving
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressStart:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS START GOES HERE
lda lockUserInput
bne @unpause
lda #1
sta lockUserInput
RTS
@unpause:
lda #0
sta lockUserInput
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressUp:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS UP GOES HERE
lda lockUserInput
bne @nomoving
; lda object_x_pos,x
; sec
; sbc object_speed,x
; cmp #$1A
; bcc @nomoving
; sta object_x_pos,x
dec $020C
dec $0210
dec $0214
dec $0218
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressDown:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS DOWN GOES HERE
lda lockUserInput
bne @nomoving
inc $020C
inc $0210
inc $0214
inc $0218
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressLeft:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS LEFT GOES HERE
lda lockUserInput
bne @nomoving
dec $020F
dec $0213
dec $0217
dec $021B
@nomoving:
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pressRight:
;;;UR CODE FOR WHAT HAPPENS WHEN U PRESS RIGHT GOES HERE
lda lockUserInput
bne @nomoving
inc $020F
inc $0213
inc $0217
inc $021B
@nomoving:
RTS
Re: How to view/capture the inputs on $4016/4017
Yeah I also just use a long AV-cable so I don't have to sit so close to the TV. Sitting close to the Famicom isn't really a problem. The main appeal with expansion port for me is that I can use my Capcom Power Stick Fighter which has a good Sanwa JLW stick and microswitches for the buttons.
The controller reading routine looks right to me, except that you don't check button transitions for the second controller. You seem to have a "joypad2_pressed" variable, but I don't see it in your controller reading routine so I don't understand where it comes from.
I would have the controller handling in a separate subroutine from the controller reading if I were you. It might be fine if you only have one mode in your game and run both controller reading and handler after each other, but if you have multiple modes (like a title screen, game screen, pause screen, game over screen etc) which all have different controller handling, you don't want to copy the same controller reading routine to all of them.
Simple example:
The controller reading routine looks right to me, except that you don't check button transitions for the second controller. You seem to have a "joypad2_pressed" variable, but I don't see it in your controller reading routine so I don't understand where it comes from.
I would have the controller handling in a separate subroutine from the controller reading if I were you. It might be fine if you only have one mode in your game and run both controller reading and handler after each other, but if you have multiple modes (like a title screen, game screen, pause screen, game over screen etc) which all have different controller handling, you don't want to copy the same controller reading routine to all of them.
Simple example:
Code: Select all
RESET:
[init code here]
...
forever: ;main program loop
jsr read_joypad ;read controllers and store in RAM
jsr handle_input ;controller 1 input handler
...
jmp forever
...
;---------------------
;Subroutines:
read_joypad:
ldy #1
lda joypad1
sta joypad1_old ;save last frame's joypad button states
lda joypad2
sta joypad2_old
...
rts
;---------------------
; handle_input will perform actions based on input:
handle_input:
@check_A:
lda joypad1
and #BUTTON_A
beq @a_exit
jsr pressA ;action when pressing A
@a_exit:
@check_B:
lda joypad1
and #BUTTON_B
beq @b_exit
jsr pressB
@b_exit:
...
rts
-
- Posts: 160
- Joined: Sat Apr 24, 2021 7:25 am
Re: How to view/capture the inputs on $4016/4017
I haven't implemented the joypad2_pressed yet. In my game's current state there is only one game mode so I removed the rts at the end of "read_joypad" to avoid a tail call. As for multiple game modes what's the best way to handle that? I don't want to check game modes during the handle_input routine because it wastes time checking things I know are false. But I also don't want to duplicate the button code structure either. Maybe an RTS Trick?
Re: How to view/capture the inputs on $4016/4017
I think the best way is to use a state machine for modes. A state machine is a system where you hold a state in a variable and each game iteration of the program loop, you check this state and jump to the corresponding logic. I use a vector table (AKA jump table* or computed GOTO) which is a table of jump addresses. You jump to the address that corresponds to the current state according to the state variable. To change mode you just change this state variable to the state you want and also run the necessary initialization routine for the mode (you might want to draw the screen).
You may also want a fallback state (the title screen) in case something goes wrong with the state machine. If you only have 5 states like in my example below but the state variable for some reason gets the value 35 stored, which isn't a valid state in my example, and the state check will not work correctly. In that case you can load the title screen and set the title screen state as a fallback. This is maybe more of an arcade game thing (and also used for vending machines in general to prevent them from locking up), but I think it's good practice to be able to handle any unexpected case in state machines in general. Even if your code would be 100% bug-free unexpected states can still happen for other reasons (like the cartridge has a bad connection, electromagnetic interference etc).
This is how I do this (ASM6 syntax):
First I give the state names so I don't have to remember which number is which state. In the init code, I make sure the title screen is loaded and set as the current state. Then in the main program loop I check the state and fetch the address for the state's program logic routine using a pointer then jumps there. If the state is STATECNT or higher, it's an invalid state so I load the title screen and set that as the current state before the fetching. The state routines (along with their load subroutines) are placed in another place in the ROM. The state routines are not subroutines so they can't end with RTS, but must end with a jump back to the main loop.
In this case the entire game will take place in the state_game routine, and the game will just check the state at the beginning of every main loop iteration after reading the controllers.
I also left the vblank wait stuff in there, but that's unrelated to the state machine. It's for preventing the game from running faster than 60 Hz (or 50 Hz on PAL) and to prevent the NMI from drawing anything if the main loop lags behind (slowdown).
*Jump table is a bit different from a vector table in that it stores the jump instructions instead of just the addresses in a table, but it's basically the same thing. Both are examples of computed GOTO.
You may also want a fallback state (the title screen) in case something goes wrong with the state machine. If you only have 5 states like in my example below but the state variable for some reason gets the value 35 stored, which isn't a valid state in my example, and the state check will not work correctly. In that case you can load the title screen and set the title screen state as a fallback. This is maybe more of an arcade game thing (and also used for vending machines in general to prevent them from locking up), but I think it's good practice to be able to handle any unexpected case in state machines in general. Even if your code would be 100% bug-free unexpected states can still happen for other reasons (like the cartridge has a bad connection, electromagnetic interference etc).
This is how I do this (ASM6 syntax):
Code: Select all
;Constants:
STATE_TITLE = 0 ;title screen state
STATE_GAME = 1 ;main game screen state
STATE_PAUSE = 2 ;pause screen state
STATE_GAMEOVER = 3 ;Game Over screen state
STATE_HISCORE = 4 ;High-Score screen state
STATECNT = 5 ;total number of states
...
;Variables:
.enum $0000
machine_state .dsb 1 ;main program logic state machine
...
.ende
RESET:
...
lda #STATE_TITLE
sta machine_state ;initialize main state machine
jsr load_title ;initialize title screen
...
main:
jsr con_read ;read controller inputs
state_vectortable: ;main state machine state select
lda machine_state
cmp #STATECNT ;load and test current machine state
bcc @jump_fetch
jsr load_title
lda #STATE_TITLE
sta machine_state ;if state is corrupt, fall back to title screen
@jump_fetch: ;fetch jump address from vector table
asl ;multiply by 2 for indexing a word table
tax
lda @vectortable+0,x
sta pointer0+0
lda @vectortable+1,x
sta pointer0+1
jmp (pointer0+0) ;jump to current machine state
@vectortable: ;main state machine vector table
.dw state_title,state_game,state_pause,state_gameover,state_hiscore
state_end: ;each state jumps back here when done
lda #$01
sta draw_flag ;allow NMI to draw, prevents incomplete buffering
nmi_wait:
lda vblankend_flag
beq nmi_wait ;wait for NMI, limits logic to a fixed frame rate
lda #$00
sta vblankend_flag ;clear vblank completion flag
inc frame_count+0
bne @skip
inc frame_count+1 ;increment 16-bit frame counter
@skip:
jmp main
;States:
state_title:
[title screen logic here]
jmp state_end ;exit state
load_title:
[title screen init routine here]
rts
state_game:
[main game logic here]
jmp state_end ;exit state
load_game:
[main game screen init routine here]
rts
state_gameover:
...
In this case the entire game will take place in the state_game routine, and the game will just check the state at the beginning of every main loop iteration after reading the controllers.
I also left the vblank wait stuff in there, but that's unrelated to the state machine. It's for preventing the game from running faster than 60 Hz (or 50 Hz on PAL) and to prevent the NMI from drawing anything if the main loop lags behind (slowdown).
*Jump table is a bit different from a vector table in that it stores the jump instructions instead of just the addresses in a table, but it's basically the same thing. Both are examples of computed GOTO.
-
- Posts: 160
- Joined: Sat Apr 24, 2021 7:25 am
Re: How to view/capture the inputs on $4016/4017
Seems like a jump table is just another form of the "RTS trick" since you mostly go through the same motions. Is one "better" than the other?
Re: How to view/capture the inputs on $4016/4017
Why don't you read the article on the wiki, and then ask questions if anything's unclear? https://wiki.nesdev.com/w/index.php/RTS_Trick
-
- Posts: 160
- Joined: Sat Apr 24, 2021 7:25 am
Re: How to view/capture the inputs on $4016/4017
This seems like an excellent system. I've created a backup of my game and I've implemented it, but unfortunately I'm running into some bugs (the player's cursor won't draw, that's a pretty big one) and the way I've set it up looks like absolute spaghetti. That's more of a formatting issue than anything else but I know it's very important and often something beginner coders like myself do incorrectly.Pokun wrote: ↑Thu May 20, 2021 3:00 am I think the best way is to use a state machine for modes. A state machine is a system where you hold a state in a variable and each game iteration of the program loop, you check this state and jump to the corresponding logic. I use a vector table (AKA jump table* or computed GOTO) which is a table of jump addresses. You jump to the address that corresponds to the current state according to the state variable. To change mode you just change this state variable to the state you want and also run the necessary initialization routine for the mode (you might want to draw the screen).
You may also want a fallback state (the title screen) in case something goes wrong with the state machine. If you only have 5 states like in my example below but the state variable for some reason gets the value 35 stored, which isn't a valid state in my example, and the state check will not work correctly. In that case you can load the title screen and set the title screen state as a fallback. This is maybe more of an arcade game thing (and also used for vending machines in general to prevent them from locking up), but I think it's good practice to be able to handle any unexpected case in state machines in general. Even if your code would be 100% bug-free unexpected states can still happen for other reasons (like the cartridge has a bad connection, electromagnetic interference etc).
This is how I do this (ASM6 syntax):First I give the state names so I don't have to remember which number is which state. In the init code, I make sure the title screen is loaded and set as the current state. Then in the main program loop I check the state and fetch the address for the state's program logic routine using a pointer then jumps there. If the state is STATECNT or higher, it's an invalid state so I load the title screen and set that as the current state before the fetching. The state routines (along with their load subroutines) are placed in another place in the ROM. The state routines are not subroutines so they can't end with RTS, but must end with a jump back to the main loop.Code: Select all
;Constants: STATE_TITLE = 0 ;title screen state STATE_GAME = 1 ;main game screen state STATE_PAUSE = 2 ;pause screen state STATE_GAMEOVER = 3 ;Game Over screen state STATE_HISCORE = 4 ;High-Score screen state STATECNT = 5 ;total number of states ... ;Variables: .enum $0000 machine_state .dsb 1 ;main program logic state machine ... .ende RESET: ... lda #STATE_TITLE sta machine_state ;initialize main state machine jsr load_title ;initialize title screen ... main: jsr con_read ;read controller inputs state_vectortable: ;main state machine state select lda machine_state cmp #STATECNT ;load and test current machine state bcc @jump_fetch jsr load_title lda #STATE_TITLE sta machine_state ;if state is corrupt, fall back to title screen @jump_fetch: ;fetch jump address from vector table asl ;multiply by 2 for indexing a word table tax lda @vectortable+0,x sta pointer0+0 lda @vectortable+1,x sta pointer0+1 jmp (pointer0+0) ;jump to current machine state @vectortable: ;main state machine vector table .dw state_title,state_game,state_pause,state_gameover,state_hiscore state_end: ;each state jumps back here when done lda #$01 sta draw_flag ;allow NMI to draw, prevents incomplete buffering nmi_wait: lda vblankend_flag beq nmi_wait ;wait for NMI, limits logic to a fixed frame rate lda #$00 sta vblankend_flag ;clear vblank completion flag inc frame_count+0 bne @skip inc frame_count+1 ;increment 16-bit frame counter @skip: jmp main ;States: state_title: [title screen logic here] jmp state_end ;exit state load_title: [title screen init routine here] rts state_game: [main game logic here] jmp state_end ;exit state load_game: [main game screen init routine here] rts state_gameover: ...
In this case the entire game will take place in the state_game routine, and the game will just check the state at the beginning of every main loop iteration after reading the controllers.
I also left the vblank wait stuff in there, but that's unrelated to the state machine. It's for preventing the game from running faster than 60 Hz (or 50 Hz on PAL) and to prevent the NMI from drawing anything if the main loop lags behind (slowdown).
*Jump table is a bit different from a vector table in that it stores the jump instructions instead of just the addresses in a table, but it's basically the same thing. Both are examples of computed GOTO.
Code: Select all
MainGameLoop:
JSR read_joypad
LDA gameState
CMP #TOTAL_MAX_GAMESTATES
BCC inBounds
JSR LOAD_TITLE ;shouldn't happen unless by some bug an invalid gamestate is loaded.
inBounds:
ASL
TAX
JSR doGamestate_RTS_Trick
; THE RTS IN THE CHOSEN GAMESTATE WILL RETURN YOU HERE.
JSR waitframe
JMP MainGameLoop
; GAMESTATE_TITLE = $00
; GAMESTATE_MAIN = $01
; GAMESTATE_MINIGAME = $02
; TOTAL_MAX_GAMESTATES = $03
Gamestate_RTS_Table:
.word STATE_TITLE-1
.word STATE_MAIN-1
.word STATE_MINIGAME-1
doGamestate_RTS_Trick:
LDA Gamestate_RTS_Table+1,x
PHA
LDA Gamestate_RTS_Table,x
PHA
RTS ;this will take you to the selected gamestate loop.
STATE_TITLE:
lda joypad1_pressed
AND #BUTTON_START
BNE StartGame
JSR waitframe
RTS ;takes you to MainGameLoop just after doGamestate_RTS_Trick.
StartGame:
JSR LOAD_MAIN
JSR waitframe
RTS
LOAD_TITLE:
pushall
LDA #$00
STA soft2000
STA PPUCTRL
STA soft2001
STA PPUMASK
;DRAW SPRITE ZERO
LDA #32 ;Y POS
STA $0200
LDA #$00 ;TILE
STA $0201
LDA #$02 ;ATTRIB
STA $0202
LDA #$08 ;X POS
STA $0203
;SPRITE ZERO WILL NOT BE VISIBLE TO THE PLAYER BUT IT WILL BE VISIBLE TO PPUSTATUS.
LoadScreen #$00 ;draws palette, background, attribute, and sets gamestate among other things.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LDA #%10010000 ; turn on NMI, set sprites $0000, bkgd to $1000
STA $2000
STA soft2000
LDA #%00011110
STA soft2001
STA $2001
LDA #$00
STA $2005
STA $2005
CLI
popall
RTS
STATE_MAIN:
JSR handle_input
LDA ScreenFlags
BMI dontGenerateOAM
JSR generateOAM
dontGenerateOAM:
JSR calculateCurrentTile
JSR waitframe
LDA gamePaused
BNE skipStuff
JSR setup_AI_motion
skipStuff:
RTS ;takes you to MainGameLoop just after doGamestate_RTS_Trick.
LOAD_MAIN:
pushall
SEI
LDA #$00
STA soft2000 ;Disable NMIs
sta $2000
sta soft2001
sta $2001 ;Disable Screen
LoadScreen #$01
;--------------------------------------------------
CreateObject #$00,#$80,#$80 ;create player cursor
CreateObject #$01,#$40,#$80 ;create air bubble
CreateObject #$02,#$20,#$80 ;create betta fish
CreateObject #$03,#$A0,#$80 ;create angel fish
JSR vBlankWait
LDA #STANDARD_2000 ; turn on NMI, set sprites $0000, bkgd to $1000
STA $2000
STA soft2000
LDA #ENABLE_RENDERING
STA $2001
STA soft2001
LDA #$00
STA $2005
STA $2005
CLI
popall
RTS
STATE_MINIGAME:
RTS