Controlling Play Speed

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

User avatar
67726e
Posts: 129
Joined: Sat Apr 03, 2010 5:45 pm
Location: South Carolina
Contact:

Controlling Play Speed

Post by 67726e » Wed Dec 01, 2010 4:28 pm

How do you typically control the play speed of the main loop? I mean if you just put:

Code: Select all

Main:
  ; Some code
  JMP Main
and then place your controller code to just move around a sprite or something it will go ridiculously fast. I've always used a 'timer' mechanism to control the speed of the game play:

In my main loop I have:

Code: Select all

  LDA Timer
  BEQ MainDone
At the end of Main it resets Timer to equal 0.

In the NMI I have:

Code: Select all

  LDA #$01
  STA Timer
This effectively controls the play speed but is this the most common method or do you use something better?

User avatar
cartlemmy
Posts: 193
Joined: Fri Sep 24, 2010 4:41 pm
Location: California, USA
Contact:

Post by cartlemmy » Wed Dec 01, 2010 4:49 pm

Your timer is the NMI, which occurs 60 times a second (or 50 for pal). So you should set a flag after your NMI routine that tells your main loop to do another frame. That looks like what you are doing as far as I can see. I'm sure there are other ways of doing it, but that's what I've been doing.

There's only way to vary the "timer" is to just update every other frame, but I think that the best way to change the speed of objects is just to vary their velocity.

User avatar
67726e
Posts: 129
Joined: Sat Apr 03, 2010 5:45 pm
Location: South Carolina
Contact:

Post by 67726e » Wed Dec 01, 2010 5:23 pm

Yeah, allowing it once per frame based on the NMI interrupts is how I have always done it, I was just wondering about possible alternate methods.

User avatar
tokumaru
Posts: 11998
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Controlling Play Speed

Post by tokumaru » Wed Dec 01, 2010 5:28 pm

You have to wait for the NMI to happen before jumping back to Main.
67726e wrote:

Code: Select all

  LDA Timer
  BEQ MainDone
In the NMI I have:

Code: Select all

  LDA #$01
  STA Timer
That looks strange... if your NMI routine sets the flag, your waiting loop should be waiting for it to get set, like this:

Code: Select all

Wait:
	lda Timer
	beq Wait

	dec Timer
	jmp Main
Most programs use this kind of solution, what varies is how the flag is set and cleared, if it's a flag or a counter, that kind of stuff. But the basic rule, that the NMI is the one to indicate when it's time to process a new frame, doesn't change.

User avatar
67726e
Posts: 129
Joined: Sat Apr 03, 2010 5:45 pm
Location: South Carolina
Contact:

Post by 67726e » Wed Dec 01, 2010 5:33 pm

Hmm... I'm not 100% sure why I wrote the code to jump to the end of the main. I never did that in my last 'game'. However, couldn't I just start of the main loop with:

Code: Select all

Main:
  LDA Timer
  BEQ Main
instead of the 'Wait' area that then jumps to Main.

User avatar
cartlemmy
Posts: 193
Joined: Fri Sep 24, 2010 4:41 pm
Location: California, USA
Contact:

Post by cartlemmy » Wed Dec 01, 2010 5:33 pm

Ah yes, actually I guess I use a counter. I like using the counter, because you can use it for things like "Do this every n frames":

Code: Select all

    LDA counter
    AND $#03 ; EDIT: Changed this to AND, originally I had it as ORA (Oops!)
    BNE notThisFrame

    ;this happens every 4th frame

notThisFrame:
Of course n has to be a power of 2
Last edited by cartlemmy on Thu Dec 02, 2010 9:44 am, edited 1 time in total.

User avatar
tokumaru
Posts: 11998
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru » Wed Dec 01, 2010 5:59 pm

67726e wrote:I was just wondering about possible alternate methods.
One of the alternative methods is to run the whole game logic inside the NMI, so at the end of the game code, instead of waiting for a flag to change you just RTS to the main loop, which is just an empty "Forever: JMP Forever" loop. When the next NMI fires, the next frame will be processed.

Both the other ways I'm aware of has the NMI modify a flag, but one also performs graphical and other important updates inside the NMI, while the other simply returns from the NMI, letting the responsibility of updating the PPU to the main loop.

All solutions, except for the one that performs important updates inside the NMI, will result in music slowdowns and possible graphical glitches in case your frame calculations take longer than a frame to complete. So guess which is my preferred solution? =)

User avatar
tokumaru
Posts: 11998
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru » Wed Dec 01, 2010 6:04 pm

67726e wrote:However, couldn't I just start of the main loop with:

Code: Select all

Main:
  LDA Timer
  BEQ Main
Ah, I see... Yeah, I guess you could to that without problems. It's just my preference to do the waiting at the end, because by that time the frame is already processed and the buffers with all PPU updates are already to be used when the NMI fires.

If you wait at the start, or do all the logic inside the NMI, you have to deal with the case where during the first frame the NMI fires and there is no data to write to the PPU, becaue the frame hasn't been processed yet. Not that this is a problem at all, since we usually have a flag indicating if the updates should happen or not.

User avatar
Kasumi
Posts: 1292
Joined: Wed Apr 02, 2008 2:09 pm

Post by Kasumi » Thu Dec 02, 2010 3:14 am

cartlemmy wrote:

Code: Select all

    LDA counter
    ORA $#03
    BNE notThisFrame

    ;this happens every 4th frame

notThisFrame:
Do you mean "AND #$03"? What you have there will ALWAYS branch since whatever counter is, it's just going to get two bits set which is always non zero.

User avatar
Kasumi
Posts: 1292
Joined: Wed Apr 02, 2008 2:09 pm

Post by Kasumi » Thu Dec 02, 2010 3:36 am

How about detecting dropped frames? It has to do with controlling play speed. Some games detect lag frames and don't run certain logic in those cases. skiplogic is nonzero in those cases.

Code: Select all

NMI: 
     dec framecount

     rti

Main:
     lda framecount
     bmi droppedframes
     bne main
     beq start

droppedframes:
     sta skiplogic;Will be nonzero
     bne start2
start:
     lda #$00
     sta skiplogic

start2:
     lda #$01
     sta framecount
;Other stuff
;Check $2002?
jmp Main
edit: that only detects a CPU loop that took more than TWO frames, so it's probably not very useful... I suppose you could check bit 7 of $2002 at the beginning and end of the main loop which would detect a CPU loop that took more than one frame in most cases.

User avatar
tokumaru
Posts: 11998
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru » Thu Dec 02, 2010 4:56 am

Kasumi wrote:I suppose you could check bit 7 of $2002 at the beginning and end of the main loop which would detect a CPU loop that took more than one frame in most cases.
Not it wouldn't... That flag only stays up during VBlank, and since VBlank doesn't last very long, if your logic took longer than a frame to complete, the chances that it also goes over the time of VBlank are pretty high...

At the end of my main loop, I set a variable indicating that all the logic has completed, and the NMI uses that to decide if it should update the PPU or not (it doesn't if the frame logic isn't complete), but I could also use it to detect when I missed a frame... Every time that the NMI fires and the frame calculations aren't complete, a variable can be updated to count the number of dropped frames. I don't expect to miss more than a frame at a time though...

Wave
Posts: 110
Joined: Mon Nov 16, 2009 5:59 am

Post by Wave » Thu Dec 02, 2010 8:33 am

Just like tokumaru said:
I use a variable that I set when a frameLogic is completed, on the nmi I check for that variable, if it is set I do nmi updates.
I could count dropped frames if I enter the nmi and see that flag is not set, and counting dropped frames like this:

Code: Select all

Nmi:
lda isGameLogicCompleted
beq dropFrame
//do frame stuff
lda #0
sta isGameLogicCompleted
jmp end
dropFrame: inc droppedFrames
end:rti

Main:

lda droppedFrames
bne droppedFrameLogic
//do frame logic
...
jmp endMain
droppedFrameLogic:
//do droppedFrameLogic
...

endMain:
inc isGameLogicCompleted
lda #0
sta droppedFrames

I'm not very used to branches since I use NESHLA, but I guess it could be something like this.

User avatar
cartlemmy
Posts: 193
Joined: Fri Sep 24, 2010 4:41 pm
Location: California, USA
Contact:

Post by cartlemmy » Thu Dec 02, 2010 9:43 am

Kasumi wrote:
cartlemmy wrote:

Code: Select all

    LDA counter
    AND $#03 ; EDIT: Was ORA
    BNE notThisFrame

    ;this happens every 4th frame

notThisFrame:
Do you mean "AND #$03"? What you have there will ALWAYS branch since whatever counter is, it's just going to get two bits set which is always non zero.
OH MY! Shame on me for not proof reading code. I have a really nasty habit of mixing AND and OR without giving a thought.

User avatar
MottZilla
Posts: 2835
Joined: Wed Dec 06, 2006 8:18 pm

Post by MottZilla » Thu Dec 02, 2010 12:15 pm

What is the point in counting the frame updates missed? I don't think that there will be much you can do about it. Generally I think a good way to control "play speed" is having your main program set a flag after all game logic and PPU update buffers are done. Then the next time NMI rolls around it will see this so it will execute the updates and reset the flag letting the main program know it is time to move on. Your NMI if it saw no update was ready for the PPU could just skip over that but still update your sound engine.

I forget which games do it, but some have bad music distorting/slowdown when the game slows down because they do not update the music engine every 1/60th second if the game is slowing down due to heavy cpu load. You should avoid this.

User avatar
tokumaru
Posts: 11998
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru » Thu Dec 02, 2010 1:01 pm

MottZilla wrote:Your NMI if it saw no update was ready for the PPU could just skip over that but still update your sound engine.
...and status bar, if you have one, don't for get that. If you don't set up the IRQ or wait for the sprite 0 hit necessary to properly position it, it will jump/disappear, and those are very unpleasant effects to look at.

Post Reply