Help with separating Game Logic from NMI

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Help with separating Game Logic from NMI
by on (#230728)
Hello! So i am an amateur game programmer that started with C++ and SFML and now is trying to learn how to do games for the NES. Up until now i have been using the Nerdy Nights tutorials to learn how to do stuff and now was trying to do collision and platformer physics, the following is my code just to make the character go left or right.

Code:
.proc nmi_handler
   LDA #$00
   STA $2003

   LDA #$02
   STA $4014

   LDA #$01
   STA $4016
   LDA #$00
   STA $4016

;Reads every button press and do some shift magic to put everything in a variable(controle)
   LDX #$00
   LoopLendoControle:
      LDA $4016
      LSR A
      ROL controle

      INX
      CPX #$08
      BNE LoopLendoControle

;Checks if right button is pressed, if so move every sprite to the right
   LDA controle
   AND #%00000001
   BEQ fimDireita
      LDX #$00
      @LoopSprites:
        ;$0203 as it is the first x position for the sprites.
         INC $0203, x
         
         INX
         INX
         INX
         INX
         CPX #$10
         BNE @LoopSprites
   fimDireita:
;Checks if left button is pressed, if so move every sprite to the left
   LDA controle
   AND #%00000010
   BEQ fimEsquerda
      LDX #$00
      @LoopSprites:
         DEC $0203, x

         INX
         INX
         INX
         INX
         CPX #$10
         BNE @LoopSprites
   fimEsquerda:

   RTI
.endproc
.proc main
   LDX $2002
   LDX #$3f
   STX $2006
   LDX #$00
   STX $2006                   
               
   JSR CarregarPaleta
   JSR CarregarSprites
   JSR CarregarBackground

   LDA #%10010000
   STA $2000

   LDA #%00011110
   STA $2001
forever:
   JMP forever   
.endproc

It is working perfectly, but i have seen somewhere that it's a good thing to separate game logic from the NMI, so i moved my control handling code to after the forever label
Code:
.proc nmi_handler
   LDA #$00
   STA $2003

   LDA #$02
   STA $4014

;Here used to be the game logic, now it is after the 'forever' label.

   RTI
.endproc
.proc main
   LDX $2002
   LDX #$3f
   STX $2006
   LDX #$00
   STX $2006                   
               
   JSR CarregarPaleta
   JSR CarregarSprites
   JSR CarregarBackground

   LDA #%10010000
   STA $2000

   LDA #%00011110
   STA $2001
forever:
        ;Same code as before
   LDA #$01
   STA $4016
   LDA #$00
   STA $4016

   LDX #$00
   LoopLendoControle:
      LDA $4016
      LSR A
      ROL controle

      INX
      CPX #$08
      BNE LoopLendoControle

   LDA controle
   AND #%00000001
   BEQ fimDireita
      LDX #$00
      @LoopSprites:
         INC $0203, x
         
         INX
         INX
         INX
         INX
         CPX #$10
         BNE @LoopSprites
   fimDireita:
   LDA controle
   AND #%00000010
   BEQ fimEsquerda
      LDX #$00
      @LoopSprites:
         DEC $0203, x

         INX
         INX
         INX
         INX
         CPX #$10
         BNE @LoopSprites
   fimEsquerda:
   JMP forever   
.endproc

But then the changes brought with it two problems, the first one is that the sprites move way too fast(when i programmed games with SFML i used a delta time to move stuff according to the framerate, since there is no floating point numbers here i don't really know what to do) and the second is that it seems the control reading is bugged since the sprite starts moving without any input.
tl;dr: How to properly separate my game logic from the NMI?
Re: Help with separating Game Logic from NMI
by on (#230730)
For #1, you need to wait until the nmi handler returns before jumping back to "forever." As it is, the move code is being called multiple times a frame.

I set a frame finished flag at the end of the game logic, and loop until the nmi clears it. Then jump back to the start of the gameplay logic.

#2 might be related to not preserving your registers in your nmi handler (pha/pla...).
Re: Help with separating Game Logic from NMI
by on (#230736)
never-obsolete wrote:
For #1, you need to wait until the nmi handler returns before jumping back to "forever." As it is, the move code is being called multiple times a frame..

Just to be sure i understood you right, this is what you meant?
Code:
vblankwait:
      BIT $2002
      BPL vblankwait

If so, yay cause doing it before any game logic made the speed of light movement go away, thanks!

never-obsolete wrote:
#2 might be related to not preserving your registers in your nmi handler (pha/pla...)

Actually just doing the above fixed the bug, thanks for remembering me that the stack exists though kkkkk
Re: Help with separating Game Logic from NMI
by on (#230739)
Polling $2002 sort-of works, but isn't 100% reliable. It will sometimes miss. It's best to do something like this:

Code:
wait:
 lda nmi_happened
 beq wait
 lda #0
 sta nmi_happened

...

nmi:
 inc nmi_happened
Re: Help with separating Game Logic from NMI
by on (#230740)
Oh, okay then, changed the code to do it according to what you said, thanks everybody!
Re: Help with separating Game Logic from NMI
by on (#230741)
If your nmi code still does...

LDA #$00
STA $2003

LDA #$02
STA $4014

and you have a main thread going, you need to back up your A register, like this.

nmi:
pha ;push A on stack
LDA #$00
STA $2003

LDA #$02
STA $4014
pla ;pull a from stack
rti

because an NMI is an "interrupt", it could happen while the main thread is still thinking, and would return from the nmi interrupt with a changed A register.

If your NMI thread uses X or Y, they also need to be backed up and restored, like.

PHa
TXA
PHA
TYA
PHA

...code...

PLA
TAY
PLA
TAX
PLA
RTI
Re: Help with separating Game Logic from NMI
by on (#230742)
The minimal ca65 example demonstrates a good approach for this, including having the game logic write render commands to a buffer that the NMI then handles at the appropriate time.