How to view/capture the inputs on $4016/4017

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

Post Reply
gigi5
Posts: 12
Joined: Tue Nov 07, 2017 5:37 pm

How to view/capture the inputs on $4016/4017

Post by gigi5 »

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 ??
User avatar
Controllerhead
Posts: 314
Joined: Tue Nov 13, 2018 4:58 am
Location: $4016
Contact:

Re: How to view/capture the inputs on $4016/4017

Post by Controllerhead »

gigi5 wrote: Tue May 18, 2021 8:20 am 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 ??
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

Image
Image
User avatar
Quietust
Posts: 1918
Joined: Sun Sep 19, 2004 10:59 pm
Contact:

Re: How to view/capture the inputs on $4016/4017

Post by Quietust »

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:
  • 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.
gigi5
Posts: 12
Joined: Tue Nov 07, 2017 5:37 pm

Re: How to view/capture the inputs on $4016/4017

Post by gigi5 »

THANKS YOU

It seems clearer to me
puppydrum64
Posts: 160
Joined: Sat Apr 24, 2021 7:25 am

Re: How to view/capture the inputs on $4016/4017

Post by puppydrum64 »

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:

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
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:

Code: Select all

LDA #$01
STA $4016
LDA #$00
STA $4016
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.

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.
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.
gigi5
Posts: 12
Joined: Tue Nov 07, 2017 5:37 pm

Re: How to view/capture the inputs on $4016/4017

Post by gigi5 »

Thanks you for good tricks :wink:
Pokun
Posts: 2675
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: How to view/capture the inputs on $4016/4017

Post by Pokun »

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:

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
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:

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
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.



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.
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).
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.
puppydrum64
Posts: 160
Joined: Sat Apr 24, 2021 7:25 am

Re: How to view/capture the inputs on $4016/4017

Post by puppydrum64 »

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 :P

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
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.
Pokun
Posts: 2675
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: How to view/capture the inputs on $4016/4017

Post by Pokun »

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:

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
puppydrum64
Posts: 160
Joined: Sat Apr 24, 2021 7:25 am

Re: How to view/capture the inputs on $4016/4017

Post by puppydrum64 »

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?
Pokun
Posts: 2675
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: How to view/capture the inputs on $4016/4017

Post by Pokun »

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):

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:
  ...
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.
puppydrum64
Posts: 160
Joined: Sat Apr 24, 2021 7:25 am

Re: How to view/capture the inputs on $4016/4017

Post by puppydrum64 »

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?
lidnariq
Posts: 11430
Joined: Sun Apr 13, 2008 11:12 am

Re: How to view/capture the inputs on $4016/4017

Post by lidnariq »

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
puppydrum64
Posts: 160
Joined: Sat Apr 24, 2021 7:25 am

Re: How to view/capture the inputs on $4016/4017

Post by puppydrum64 »

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):

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:
  ...
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.
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.

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
Post Reply