How Menus Work ‐ Part 3 - pret/pokeemerald GitHub Wiki
In part 1 of this guide, we covered some of the concepts used in GBA graphics programming, and in part 2, we covered some of the abstractions that Game Freak built to make some common UI tasks easier. Now, we're going to actually look at one of the game's menus — specifically, the options menu — and walk through how these various pieces are used to make the menu work.
-
The Options menu works by creating a single task, and having that task drive the bulk of the menu's behaviors. All menu state is stored in the task data. The task waits for input and, when input is received, it then acts on that input. If an input would cause the menu to close, we switch the task handler to whatever functions handle closing the menu.
When the menu opens, it fades in from black. When the menu closes, it fades out to black. Some task handlers exist which do nothing but wait for the current fade to finish, before then switching over to some code that actually does things.
-
The options menu displays a simple list of options. Most options show the name on the left, and all possible values on the right, with the selected value printed in red. The player changes the value of an option by pressing left and right on the D-Pad. Each option has two functions dedicated to it: one to change the current value, and one to display the available choices.
-
The options menu uses an unusual trick to indicate the currently selected menu item. You may notice that most of the list is darkened, with the current menu item being highlighted. This uses screen windows (from part 1), not to be confused with UI windows (from part 2).
We declare all of our functions up-front before defining them.
Function | Purpose |
---|---|
void Task_OptionMenuFadeIn(taskId) |
The options menu is initially displayed with a fade-in from black. This task handler waits for the fade-in to complete, and then advances the menu to Task_OptionMenuProcessInput . |
void Task_OptionMenuProcessInput(taskId) |
This task handler waits for the player to press any button that this menu cares about. When they do, it responds to that input, sometimes by calling other functions or sometimes by switching to a different task handler. |
void Task_OptionMenuSave(taskId) |
This task handler saves the player's changes to the game options, and then triggers a fade-out to black and advances to Task_OptionMenuFadeOut . |
void Task_OptionMenuFadeOut(taskId) |
This task handler waits for the menu to finish fading out to black, and then destroys the menu state. |
void HighlightOptionMenuItem(selection) |
This function is responsible for highlighting the menu item that the player's cursor is over. |
u8 TextSpeed_ProcessInput(selection) |
This function is responsible for changing the current "text speed" value as needed. It returns the current value after it's done. |
void TextSpeed_DrawChoices(selection) |
This function is responsible for drawing the available values for the "text speed" option. |
... |
..._ProcessInput and ..._DrawChoices functions exist for the other options as well. |
void DrawHeaderText() |
This function draws the menu's header text ("OPTIONS"). |
void DrawOptionMenuTexts() |
This function draws all of the option names. |
void DrawBgWindowFrames() |
This function draws the borders around the two parts of the options menu (the header and body). |
The entry point for the options menu — the place where it all starts — is CB2_InitOptionMenu
. The function is installed by being passed to SetMainCallback2
, which is very convenient for us: when you change main callback 2 this way, the game tracks the previous callback in gMain.savedCallback
, and this means that when we later close the options menu, we can just call SetMainCallback2(gMain.savedCallback)
to go back to whatever the game was doing before it opened the options menu.
CB2_InitOptionMenu
sets up the menu's graphics, creates a task to power the menu, performs a fade-in from black, and then sets main callback 2 to MainCB2
. From that point onward, the task handles all of the menu's behavior. CB2_InitOptionMenu
runs over the course of multiple frames, using a counter to keep track of what step it's on; going frame by frame, here's what it's doing:
-
We call
SetVBlankCallback(NULL)
to temporarily disable all graphics updates. This means that we can set up the menu, and draw its first frame, without worrying about a half-drawn menu being displayed. -
We reset a bunch of VRAM resources, so that we have a blank slate to work with:
- We use DMA functions to completely wipe all of VRAM.
- We force all backgrounds to position (0, 0), so that nothing we draw is shifted around on-screen.
- We deactivate all text printers.
- We set I/O registers related to screen windows, to prep for highlighting the menu item that the player's cursor is over.
We also initialize the VRAM backgrounds we want to use, and the UI windows we want to use. Those are defined in two variables:
sOptionMenuBgTemplates
andsOptionMenuWinTemplates
, and we pass pointers to those variables intoInitBgsFromTemplates
andInitWindows
, respectively. Finally, we useShowBg
calls to make our two background layers visible. -
We reset a few more resources: we interrupt any ongoing color palette fade, we halt any ongoing scanline effect[1], we reset all sprites, and we reset all tasks.
Important
Most of Game Freak's menus reset all tasks when they open. This can help make sure that nothing accidentally "leaks in" from other screens and menus, and it can also make sure that the game engine doesn't have to waste time running code (e.g. some overworld tasks) that shouldn't do anything while the menu is open. However, some major game systems, like multiplayer link communications, also make use of tasks, and resetting those tasks early could break those systems. If a menu can be opened during a multiplayer link, then it shouldn't reset tasks; and indeed, if you look at SetupBagMenu
in item_menu.c
, you'll see that it only resets tasks if no multiplayer link is happening.
In rarer cases, a "parent" menu may rely on tasks, while opening "child" menus that share those tasks, and so the child menus have to avoid resetting tasks in order to avoid disrupting their parent. The PokéNav and Pokédex sub-screens are examples. When in doubt, take a look at other menus that can be opened from the same place as your menu, and see what they do about tasks.
-
We use a function call to load the player's preferred border style for dialogs:
LoadBgTiles(1, GetWindowFrameTilesPal(gSaveBlock2Ptr->optionsWindowFrameType)->tiles, 0x120, 0x1A2)
. The arguments are as follows:- We want this on background layer
1
. -
GetWindowFrameTilesPal
will find the n-th border style, and give you a means to access itstiles
or its colorpal
ette. In this case, n is the player's preference, pulled from the savedata. For this function, we're just loading the tile graphics; we'll load the palette later. - A single tile is 0x20 (32) bytes. We're loading 9 tiles (including an empty tile for the center region inside the borders), so we need to transfer 0x120 bytes (0x20 * 9).
- We have to decide where in VRAM to put these tiles. The choice we've made is tile ID 0x1A2. We're loading nine tiles, so this will consume all tiles in the range [0x1A2, 0x1AA].
- We want this on background layer
-
We load the color palette for the options menu's non-text graphics, and we load the color palette for the player's preferred border style.
-
We load the color palette for the options menu's text.
-
The
PutWindowTilemap
function reserves some RAM for the menu header's UI window. When we make certain changes to the window's tile data, those changes are made to that portion of RAM, so that they can later be copied into VRAM all at once. This is more efficient than constantly making smaller copies from RAM to VRAM.Once we've reserved that memory, we paint into it using
DrawHeaderText
. (The header never changes, so this is the only time we callDrawHeaderText
.) Inside that function, we start by filling the entire UI window with color 1 (white), to blank it out. Then, we draw the options text, setting it to draw instantly rather than with an animation; and finally, we useCopyWindowToVram
to send everything we've just drawn from RAM to VRAM.
static void DrawHeaderText(void)
{
FillWindowPixelBuffer(WIN_HEADER, PIXEL_FILL(1));
AddTextPrinterParameterized(WIN_HEADER, FONT_NORMAL, gText_Option, 8, 1, TEXT_SKIP_DRAW, NULL);
CopyWindowToVram(WIN_HEADER, COPYWIN_FULL);
}
-
We spend one frame doing nothing. Maybe an older menu design did something here, and the code was scrapped before the game was released?
-
We set up the options list UI window similarly to the header:
PutWindowTilemap
and thenDrawOptionMenuTexts
. This draws the names of each option, but not their values. The menu never scrolls or hides any option, so we never need to callDrawOptionMenuTexts
again after this. -
We call
DrawBgWindowFrames
to draw the borders around the two parts of the options menu. This is the only time we call that function.If we scroll down to its definition, we'll see that the code defines a bunch of convenience macros for readability: the tile IDs of the different tiles that make up the dialog border style. We loaded those tiles to ID 0x1A2, so they're numbered from there onward. We call
FillBgTilemapBufferRect
repeatedly to draw each tile in its proper place: that function modifies a background layer's tilemap, allowing you to paste a single tile ID over any rectangular group of tiles in the layer. -
We create a task to power the menu, and set up the task data. We then draw the options' values, and use
CopyWindowToVram
to copy the drawn content from RAM into VRAM. -
Finally, we've finished setting up the menu and drawing its initial content, so we now prepare to display it. We trigger a fade-in from black, reset the v-blank callback so that the GBA hardware can process graphics updates again, and set main callback 2 to a simple function that performs basic sprite and graphical updates on demand. From here on out, main callback 2 won't power our menu; the task that we've created will.
The menu task's initial handler is Task_OptionMenuFadeIn
. This doesn't do anything other than wait for the current fade-in from black to complete, by checking !gPaletteFade.active
. After that, it sets the task's handler to Task_OptionMenuProcessInput
. From this point onward, the menu is fully open and interactive.
The task is now using Task_OptionMenuProcessInput
as its handler function. This task uses the JOY_NEW
macro to check whether certain buttons have just been pressed down on the current frame. (You can use TEST_BUTTON
to see if they're down, without caring whether they just went down or are already down. You can use JOY_HELD
to see if a button has been held, or JOY_REPEAT
to see if a button is auto-repeating.)
-
If the player presses A, then we check if the menu cursor is over the "Cancel" item. If so, we switch the task handler to
Task_OptionMenuSave
. -
If the player presses B, then we switch the task handler to
Task_OptionMenuSave
. We don't care where the cursor is. -
If the player presses D-Pad Up or D-Pad Down, then we move the menu cursor. We make sure not to move the cursor past the edges of the menu, and we allow it to wrap around: moving above the top of the menu pops the cursor down to the bottom, and vice versa. We then call
HighlightOptionMenuItem
to update the highlight effect on the current menu item. -
If the player presses any other button, then we check what menu item the cursor is on. If it's on the menu item for any option, then we store the previous value of that option, and then call that option's "process input" function. This function will check whether the player pressed D-Pad Left or D-Pad Right and if so, it'll set
sArrowPressed
toTRUE
and then change the value of that option. Either way, it returns the value of the option, which we use to update the menu's task data. We then check whether the option's current value is different from its previous value, and if so, we call the "draw choices" function.After handling each specific option, we check whether
sArrowPressed
isTRUE
. If so, then we assume that the option value has changed, and we callCopyWindowToVram(WIN_OPTIONS, COPYWIN_GFX)
to perform a partial copy of the UI window's data from RAM to VRAM. (At this stage in the process, this is enough to update the window's visuals. We went over why in part 2.) We also make sure to resetsArrowPressed
back toFALSE
.This isn't the cleanest way to do all of this, but it works fine enough. It would perhaps be cleaner to have a "has this option just been changed?" variable local to the function, and set that variable at the same time that we check whether the option has changed, instead of defining a
static
variable likesArrowPressed
.
Let's look at the "process input" function for a particular option: TextSpeed_ProcessInput
. This function is quite simple: we just check whether the player has pressed D-Pad Left or D-Pad Right, and if so, we set sArrowPressed
and return the new option value. Most of the "process input" functions for these options work the same way, though FrameType_ProcessInput
is a bit special: after updating the current border style, it actually goes out and loads the tiles and palette for that border style directly into VRAM, overwriting the border style we loaded during menu setup, so you can preview the different styles in real time.
Once these inputs have been handled, the task's handler exits. If we didn't swap to a new handler, then we'll run again next frame and check for more inputs.
Let's look at TextSpeed_DrawChoices
.
We create an array of three u8
s called styles
— one u8
for each option value we wish to draw — and we then set them up so that all of them are 0
except for the one that represents the selected value; that one's 1
. We then use the DrawOptionMenuChoice
helper function to draw each option value, passing the appropriate entry in styles
. The array, then, just gets rid of the need to use any if
statements to pick a color for each individual value.
We use GetStringWidth
to measure the width of each value name. We use this to right-align the "FAST" value, and to center the "MID" value between "SLOW" and "FAST."
DrawOptionMenuChoice
may be familiar to you: we went over it in the previous part of this tutorial. This function's approach to drawing colored text isn't very good. Each option value has text formatting codes baked directly into the value name; for example, gText_TextSpeedMid
is defined as "{COLOR GREEN}{SHADOW LIGHT_GREEN}MID"
, where each of those format codes is one byte. What DrawOptionMenuChoice
does is copy the string into a local variable, and then bulldoze over some of those formatting codes if it's been told that the text we want to draw is selected. We talked about how it would've been better to keep formatting codes out of the option text, and use AddTextPrinterParameterized3
to change the color instead. (Maybe that function was created after Game Freak built the options menu, and they just never went back to change things.)
Most other options draw their choices similarly, though FrameType_DrawChoices
is a bit different, since it just draws the current frame number.
We covered this in part 1 of the tutorial. To recap: we use screen windows (a GBA hardware feature, not to be confused with UI windows, which are a Game Freak code feature) to dim everything in the "options" UI window except the row that the cursor is on.
The Task_OptionMenuSave
function kicks off the process of closing the menu. We copy the player's chosen options out of task data and into the savegame. We then begin a fade-out to black, and switch the task handler to Task_OptionMenuFadeOut
.
Task_OptionMenuFadeOut
is the mirror image of Task_OptionMenuFadeIn
. It waits for any ongoing palette fades to finish. Then, it destroys our task, releases any resources used by our UI windows, and returns us to wherever we were when we entered the options menu.
The general flow for a menu, then, is this:
-
Use a main callback 2 handler to set up your menu and create a task for it.
- Reset VRAM and other resources.
- Set up your background layers and UI windows.
- Draw your menu in its initial state.
- Start a fade-in from black.
- Spawn a task to power your menu.
- Set ordinary functions for the main callbacks.
-
Have your task handler wait for the fade-in to finish.
-
Have your task handler wait for input, and react to it.
-
Exit your menu.
- Tear down the resources you set up, as appropriate.
- Start a fade-out to black.
- Wait for the fade-out to finish.
- Set main callback 2 to whatever function should pick up from where you've left off.
[1] Scanline effects are, broadly, effects that apply individually to each row of pixels on-screen: you can ask the GBA to run a fast, simple function in the brief microseconds between when it finishes updating one physical row of pixels and when it starts updating the next. Two examples of scanline effects in Emerald are wavy distortions applied to the screen (when starting some battles from the overworld) and the black shadow drawn when using Flash in dark caves (screen windows are set to pitch-black, and their sizes are changed at each row of pixels, to create a round hole in the windows). ↩