MEGASAMPLER BONUS 03: Multiple Menus

In this tutorial, we will break down how to create a robust menu system for the Sega Genesis using SGDK. We'll use the MEGAPONG project as our case study; however, the menu logic is relatively applicable to other MEGA-PROJECTS [MEGARUNNER, MEGALAGA, etc.].

1. The Core Concept: The State Machine

A game is rarely just one big loop. It usually has distinct "modes": a Title Screen, an Options Menu, the Gameplay itself, and a Game Over screen.

In megapong.c, this is handled by a simple enum (enumeration) and a switch statement. This is called a Finite State Machine. We’ve covered enumeration and switch statements in previous lessons if you need a refresher.

Step A: Define Our States

First, we list every possible "screen" the game can be in:

typedef enum { STATE_MENU, // The Main Title Screen STATE_OPTIONS, // The Settings Screen STATE_PLAY, // The actual game STATE_EXIT // Quits the game (returns to launcher) } PongState;

Step B: The Main Loop

In our main() (or runMegapong in this specific file), the game enters a loop that checks the currentState and launches the appropriate function. When that function finishes, it returns the next state the game should go to.

void runMegapong() { PongState currentState = STATE_MENU; while(currentState != STATE_EXIT) { switch(currentState) { case STATE_MENU: currentState = megapong_menu(); // Run Menu, wait for it to return next state break; case STATE_OPTIONS: currentState = megapong_options(); // Run Options break; case STATE_PLAY: currentState = playMegapong(); // Run Game break; } } }

Why this is good: It keeps our code isolated. The "Gameplay" code doesn't need to know how the "Options" menu works. It just needs to know when to start and stop.

2. Drawing the Menu (Visual Feedback)

In megapong_menu(), we need to show the player what they are selecting. We use a simple integer variable selection to track where the cursor is.

The "Dirty" Rendering Trick

On the Sega Genesis, writing text to the screen (VRAM) is relatively slow compared to modern PCs. You don't want to redraw the entire menu text every single frame (60 times a second).

Instead, we only redraw text when something changes.

// Inside the menu loop... if (selection != old_selection) { // 1. Clear the old text so artifacts don't remain VDP_clearText(14, 10, 20); VDP_clearText(14, 12, 20); VDP_clearText(14, 14, 20); // 2. Draw the cursor ">" based on the current selection VDP_drawText(selection == 0 ? "> START GAME" : " START GAME", 14, 10); VDP_drawText(selection == 1 ? "> OPTIONS" : " OPTIONS ", 14, 12); VDP_drawText(selection == 2 ? "> EXIT" : " EXIT ", 14, 14); // 3. Update tracker old_selection = selection; }

3. Handling Inputs (The "Pressed" Check)

One of the biggest mistakes beginners make is checking if a button is held down rather than pressed once.

In megapong.c, we calculate pressed manually:

u16 current_state = JOY_readJoypad(JOY_1); // What is being held right now? static u16 last_state = 0; // What was held last frame? // BITWISE MAGIC: // (current_state) = Buttons currently down // (~last_state) = Buttons NOT down last frame // (&) = Buttons down NOW that were NOT down BEFORE u16 pressed = current_state & ~last_state; last_state = current_state; // Save for next frame if (pressed & BUTTON_UP) { selection--; // Only moves once per press! }

4. The Options Menu: Modifying Global Settings

The megapong_options() function shows how to change game variables. Because settings is a global struct, changes made here persist when we switch to STATE_PLAY.

Cycling Through Enums

The code uses a clever math trick to cycle through options (like Difficulty) without using a dozen if/else statements.

The Goal: Cycle through SLOW (0), NORMAL (1), FAST (2), HYPER (3).

// 'dir' is -1 (Left) or +1 (Right) settings.difficulty = (settings.difficulty + dir + 4) % 4;

Dynamic Flavor Text

The menu also features scrolling text that explains what each option does. This is handled by a switch statement that updates a string pointer flavor based on the currently selected row.

switch(opt_sel) { case 0: // Difficulty Row if(settings.difficulty == DIFF_HYPER) flavor = "HYPER: BLAST PROCESSING SPEED!"; break; // ... } updateScrollingText(flavor); // Function to handle the scrolling ticker

Menu Results

Menu Result Image 1
Menu Result Image 2

Tutorial Summary

  1. Use Enums: Give our game states (Menu, Play, Options) readable names.
  2. Separate Logic: Keep our input handling separate from our drawing code.
  3. Detect Presses, Not Holds: Use bitwise logic to detect single button presses for menu navigation.
  4. Update Only on Change: Don't VDP_drawText every frame. Only draw when the user presses a button.