The simplest game structure I can think of is this:
Code: Select all
TitleScreenStart:
;INITIALIZE THE TITLE SCREEN STATE
TitleScreenLoop:
;DO TITLE SCREEN LOGIC
;WAIT FOR VBLANK
;DO TITLE SCREEN PPU UPDATES
jmp TitleScreenLoop
MainGameStart:
;INITIALIZE THE MAIN GAME STATE
MainGameLoop:
;DO MAIN GAME LOGIC
;WAIT FOR VBLANK
;DO MAIN GAME PPU UPDATES
jmp MainGameLoop
GameOverStart:
;INITIALIZE THE GAME OVER STATE
GameOverLoop:
;DO GAME OVER LOGIC
;WAIT FOR VBLANK
;DO GAME OVER PPU UPDATES
jmp GameOverLoop
For each program mode you have an initialization step, where you should set everything up. For the title screen and game over modes, this would be a good time to draw the screens. The initialization of the main game is more complex... in addition to drawing the first screen you'll have to initialize objects, set up pointers to the level data, and so on. After the initialization comes the loop, which runs over and over and alternates between logic and PPU updates, with a Vblank wait in-between. This VBlank wait should NOT be polling $2002, bacause as has been pointed out, 2002 is not reliable for in-game use. The correct solution here would be to poll a flag that's modified by a barebones NMI routine (i.e. INC VBlankFlag; RTI;).
With this structure, you can switch to another game mode at any time by jumping to its Initialization label. For example, if you're in the MainGameLoop and detect that the player lost all his lives, you can simply JMP to GameOverStart to show the game over screen. It will of course look better if you fade out before doing that, to make the transition smooth (assuming that the game over state fades in)! the only serious disadvantage with this approach is that when your game logic takes longer than a frame to complete, you'll miss a VBlank. This means the music will slow down and raster effects (like status bars or parallax scrolling) might break (and possibly crash the program if you're not prepared to handle these slowdowns). If you're 100% sure your game logic will never go over a frame's time, you don't have to worry.
If slowdowns are a possibility, you can't just "wait for VBlank", you must let the NMI do what it's supposed to do and interrupt your game logic (after all, the "I" in NMI means interrupt!). The problem is that since you have many different game modes, your NMI code will either have to be very generic (so that it can be used for all game modes) or you'll have to set up a way to run different NMIs for the different game modes.
Generic NMI handlers are great: they use buffers (address, legth and data) to update the name tables, they update sprites with a sprite DMA, they set the scroll and finally call the music engine. If you can get away with using that for all you game modes, good for you, but that's not always possible.
In my case, I have chosen to use 2 NMI routines: one that just increments a flag (so it can be used to wait for VBlank) and another one for the main game, which is the only program mode that can actually slow down. So in this particular NMI handler I'll only perform the PPU updates if the game logic has finished, but raster effects and the music are processed regardless.