So thus far the game loop only accomodates for only one activity: the main game. Maybe other features or screens are to be added, which—with this kind of setup—might make a mess out of the codebase.
Many games have a “game mode” (or “game state”) pattern for its main loop. Essentially, what this means is that every mode in the game has its own dedicated loop subroutine that processes only the logic relevant to that mode, and it’s the main loop’s job to execute the correct one for each game mode.
I think it’s a good idea to start implementing that now, since I have a working game and thus a known “good” state.
The pattern basically works like this:
GameLoop::
@{Determine which game mode is to be run and perform initializations}
@{Perform game mode loop}
@{Wait one frame}
jp GameLoop
I’ll want to keep track of the current game mode, and also the previous game mode, so that my routine knows when a new one is loaded. Gonna put them in HRAM for quick access.
hGameMode:: db
hOldGameMode:: db
Not only would I want every game mode have its own loop routine, but also an initialization routine that runs only when the game mode is switched to. For both, I’ll be using a jumptable with the pointers to each routine.
ld hl, hOldGameMode
ldh a, [hGameMode]
cp [hl]
jr z, .skip_init
ld hl, GMInitJumptable
call GotoJumptableEntry
ldh a, [hGameMode] ; reload game mode
ldh [hOldGameMode], a ; replace old game mode
.skip_init
Used by 1
And here’s the function that will execute the appropriate routine. The A register determines which entry is to be selected, and HL must be advanced accordingly. Since the jumptable entries are simple pointers, they’ll be two bytes each—so HL + 2 × A.
I’ll retrieve the value of HL (the entry itself) from where HL is pointing to (the address of the entry), and then jump there.
;;--
;; Go to jumptable entry
;;
;; @param HL Jumptable address containing pointers
;; @param A Entry number
;; @clobber DE
;;--
GotoJumptableEntry::
ld e, a
ld d, 0
add hl, de
add hl, de
ld a, [hl+]
ld h, [hl]
ld l, a
jp [hl]
Used by 1
I’ll reserve the init routines jumptable here. (Game mode related stuff have the “GM” prefix, for “Game Mode”)
GMInitJumptable::
@{Pointers to game mode initialization routines}
Since I copied hGameMode
to hOldGameMode
, A contains hGameMode
. So, executing the loop is simply:
And the associated jumptable:
GMLoopJumptable::
@{Pointers to game mode loop routines}
Last, I reserve some slots for the actual init and loop routines in the main program code:
Let’s adapt the main loop I have now into a “self-contained” game mode. First I’ll reserve some constants for the game mode identifier.
Next, I’ll move the portion of the existing init code into its own function.
GM_Game_init::
call DisableLCD
@{Set up the game graphics}
@{Set up sprites and variables}
jp EnableLCD
Used by 1
Likewise with the existing loop code.
GM_Game_loop::
@{Handle joypad input}
@{Handle ball physics}
@{Update screen}
ret
Used by 1
I’ll add the addresses of both routines to their respective jumptables.
Next, I’ll have to change the initialization routine to use the new system, by setting the initial game mode.
Init::
@{Disable interrupts}
@{Save the Game Boy type}
@{Turn off the screen}
@{Clear RAM and place the stack pointer}
@{Reset audio}
@{Reset the screen}
@{Copy HRAM DMA code}
; +++
@{Set the initial game mode}
; +++
@{Turn on screen}
@{Enable the interrupts again}
I set the initial hOldGameMode
to force the game to fire the initialization routine.
ld a, $ff
ldh [hOldGameMode], a
xor a ; ld a, GM_GAME
ldh [hGameMode], a
Used by 1
I’ll also do this when resetting the game state from the scoring routine.
; force a reinitialization
ld a, $ff
ldh [hOldGameMode], a
jp GameLoop
After this, the game should look and play identical as before. The changes are really just under-the-hood stuff that’ll make it more convenient to me to add more screens to the game.
This is kind of a quick hack (out of several quick hacks done so far, really) so that the score doesn’t update all the time, only when asked to.
wShouldUpdateScore:: db
; +++
ld a, [wShouldUpdateScore]
and a
jr z, .skip_score_update
; +++
ld a, [wLeftScore]
ld hl, LEFT_SCORE_VRAM_START
call ShowScore
ld a, [wRightScore]
ld hl, RIGHT_SCORE_VRAM_START
call ShowScore
; +++
.skip_score_update
; +++
And I set it to be enabled when the game first boots up.
ld a, 1
ld [wShouldUpdateScore], a
Well, it’s time to use this new system to add a new screen: the title screen. At the moment, it really should just do nothing but say “Press Start”, and then kicks you into straight into the game. Although from the player’s perspective it comes before the gameplay, internally I’m going to define this mode after the gameplay mode.
Because its only job is to show the game’s title and wait for joypad input, the code is gonna be really simple:
GM_Title_init::
call DisableLCD
@{Set up the title screen graphics}
jp EnableLCD
Used by 1
Of course, the initialization is simply just copying the necessary graphics data to VRAM…
ld hl, vChars0
ld de, TitleScreenGFX
ld bc, TitleScreenGFX_END - TitleScreenGFX
call CopyMem16
ld hl, vBGMap0
ld de, TitleScreenMAP
ld bc, TitleScreenMAP_END - TitleScreenMAP
call CopyMem16
And both the tileset and the tilemap are generated off of a single png…
TitleScreenGFX::
incbin "gfx/title-screen.2bpp"
TitleScreenGFX_END::
TitleScreenMAP::
incbin "gfx/title-screen.map"
TitleScreenMAP_END::
The loop is just checking whether or not the START button is pressed, with an early ret
if not:
call ReadJoypad
ldh a, [hInput]
bit BUTTONF_START, a
ret z
Used by 1
But if it is pressed, it’ll jump to the game.
xor a ; ld a, GM_GAME
ldh [hGameMode], a
ret
Let’s add all of the stuff above into the ROM.
dw GM_Title_init
dw GM_Title_loop
I’ll change the starting game mode here.
GM_TITLE equ 1
ld a, $ff
ldh [hOldGameMode], a
ld a, GM_TITLE ; +++
ldh [hGameMode], a
I want to add a nice fade in/out effect to the game. Originally I was gonna be fancy and make a “unique” fading routing that switches between phases every frame, but I think I’ll just be making a simple one.
The idea here is change the palettes every odd frame or so:
I’ll want to write a routine to delay C amount of frames, calling DelayFrame
and decrementing C each time until C is 0.
;;--
;; @param C amount of frames to wait
;; @return C 0
;;--
DelayFrames::
.loop
call DelayFrame
dec c
jr nz, .loop
ret
Used by 1
Next up, a shortcut to apply the palette data and wait 2 frames using the above function. If doing 2 frames, call DelayFrame; jp DelayFrame
may suffice, but for some reason I want the delay between phases to be configurable.
;;--
;; Applies palette A and then wait 2 frames.
;;
;; @param A palette data
;;--
ApplyPaletteWait::
ldh [rBGP], a
ldh [rOBP0], a
ldh [rOBP1], a
ld c, 2 ; frames to wait between phases
jp DelayFrames
Used by 1
Then, the actual routines. I’ll just be writing palette data and then a short delay between writes.
FadeOut::
ld a, %11100100
call ApplyPaletteWait
ld a, %10010000
call ApplyPaletteWait
ld a, %01000000
call ApplyPaletteWait
xor a ; %00000000
jp ApplyPaletteWait
Used by 1
FadeIn::
xor a ; %00000000
call ApplyPaletteWait
ld a, %01000000
call ApplyPaletteWait
ld a, %10010000
call ApplyPaletteWait
ld a, %11100100
jp ApplyPaletteWait
Used by 1
First I’ll apply the fade-in effect to the title screen. I replaced the jp EnableLCD
instruction with the call
equivalent, and then jumping to FadeIn
.
GM_Title_init::
call DisableLCD
@{Set up the title screen graphics}
call EnableLCD
jp FadeIn
For the gameplay entry, I decided to use DelayFrames
to wait some amount of time before fading out and resetting the game state.
GM_Game_init::
ld c, 32
call DelayFrames
call FadeOut
call DisableLCD
@{Set up the game graphics}
@{Set up sprites and variables}
call EnableLCD
jp FadeIn