Francisco Sant'Anna

A structured main menu

@_fsantanna

A main menu typically displays a set of buttons that allows players to navigate the game. Selecting a button transfers the game to another screen, such as an options screen or the gameplay itself. Eventually, after the chosen screen terminates, the game transits back to the main menu.

In the demo figure, we symbolize the chosen screens as clickable buttons associated with the user choices. Clicking the button again terminates the screen and returns to the main menu. Our goal is to apply structured reactive techniques in our implementation in Ceu. Let’s discuss it in a top-down approach, starting with the main application:

-- enumeration with the possible menu choices
type Menu = <Story=(), Editor=(), ...>

-- task signatures for the menu and buttons
task main_menu: () -> Menu                       👈 (next posts)
    -- returns the chosen screen to navigate
task menu_button: [pos:Point, lbl:String] -> ()  👈 (next posts)
    -- receives a position and label to show

-- spawns the game code
spawn {
    -- the outer loop                         1️⃣
    loop {
        -- main menu
        var opt = await spawn main_menu ()    2️⃣

        -- chosen screen
        var lbl = ifs {
            opt ? Story  { "Story"  }
            opt ? Editor { "Editor" }
            ... -- other options
        }
        await spawn menu_button [[0,0], lbl]  2️⃣

        -- loops back to menu after screen terminates
    }
}

-- enters the SDL engine loop
call pico_loop ()

The relevant structured mechanism in the code is the outer loop (1️⃣), which alternates between the main menu and the chosen screen. These screens can be arbitrarily complex and are handled with the spawn-await combination (2️⃣), which resembles standard function calls: spawn the task and await its termination. We will discuss the main_menu and menu_button implementations in future posts. While the main task waits in the loop, the current screen executes in a separate task, possibly reacting to events and spawning auxiliary tasks. In the meantime, the language keeps the loop context alive (i.e., locals and program counter) similarly to coroutines. This direct style with spawn-await contrasts with the arguably more intricate continuation passing style (CPS), which is one of the control-flow patterns described in the previous post:


The original implementation in C++ uses CPS and pushes the screen navigation to occur inside the button click callback, thus textually far away from the main game function:

MainMenu::MainMenu () {
    ...
    story_but = gui->create<MenuButton>(...)
    ...  // other menu buttons
}

void MainMenu::on_click (MenuButton* button) {
    if (button == story_but) {
        ...
        Screen::push_screen(worldmap);  // screen navigation
        ...
    } else ... {    // other buttons actions
        ...
    }
}

The implementation in C++ also relies on an explicit stack to alternate between the main menu and the chosen screen: it pushes a new screen on top of the main menu, which must be explicitly popped when terminating:

void Worldmap::update (...) {
    ...
    if (exit_worldmap) {
        Screen::pop_screen();
    }
    ...
}
void WorldmapCloseButton::on_click() {
    Screen::pop_screen();
}

Not only this approach requires the called screen to be aware of its parent navigation track, but also relies on a data structure to simulate a control-flow mechanism.


Comment on @_fsantanna.