Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Main Menu Implementation

Hello! Welcome back to our JRPG development course.

In the last lesson, we designed a robust, reusable UI component architecture for our canvas-based game. We established a plan based on a layered model (Rendering, State, Interaction) and a composable component tree, much like you'd find in modern front-end frameworks.

Today, we move from theory to practice. We'll leverage that architecture to tackle the learning outcome: implement the main menu scene, allowing access to sub-screens. This menu is the central hub the player uses to manage their party, items, and progress. We'll build our first interactive UI screen, handle player input for navigation, and, most importantly, implement the logic to transition to sub-menus like Items, Status, and Save.

1. The Logic of a JRPG Menu

Before writing any JavaScript, let's analyze how a classic JRPG menu functions. The logic is remarkably consistent across games and can be broken down into a simple state machine. RPG Maker's event-based system provides a clear, visual way to understand this logic.

Eventing a Picture-Based Menu

The blog post 'Eventing a Picture-Based Menu' from the official RPG Maker website provides a perfect, non-code-based walkthrough of the logic behind a custom menu. As you read, notice how it uses variables, conditional branches, and loops to manage the menu's state and behavior. This is the exact logic we will be implementing in code.

Please read the article starting from the section with the heading 'With our menu visible, we can move on to the interactive part.' Focus on these key concepts: State Tracking: How a variable is used to track the currently selected option. Input Handling: How conditional branches check for button presses (left/right, ok, cancel) to change the state or trigger actions. Rendering: How the variable's value determines which picture gets highlighted. Scene Transitions: Pay close attention to the script calls like SceneManager.push(Scene_Item);. This is the core mechanism for opening sub-screens.

From that resource, we can distill the core mechanics of our menu:

  • State: A single variable, let's call it selectedIndex, holds the current cursor position (e.g., 0 for "Items", 1 for "Skills").
  • Input: The system constantly listens for input.
    • Down arrow: Increments selectedIndex.
    • Up arrow: Decrements selectedIndex.
    • Confirm button: Executes an action based on the value of selectedIndex.
    • Cancel button: Closes the menu.
  • Rendering: On every frame, the game draws the list of options and a cursor or highlight corresponding to the selectedIndex.
  • Transitions: When the Confirm button is pressed, a switch statement or if/else block uses selectedIndex to determine which new scene to push onto the scene stack (e.g., SceneManager.push(new ItemScene())).

This is our blueprint. RPG Maker uses visual event commands; we'll use JavaScript classes and methods to achieve the same result.

2. Implementing the Menu Components

Based on our architecture from the last lesson, the MainMenuScene will be a UIComponent that composes other, smaller components. For our main menu, we need two primary components: Window and SelectableList.

Let's start by defining the code for these reusable components.

The Window Component

This is a simple visual container. For now, it will just draw a bordered rectangle. In the future, this could be enhanced with a 9-slice image for a classic JRPG look.

// /components/Window.js
import { UIComponent } from './UIComponent.js';

export class Window extends UIComponent {
    constructor({ x, y, width, height }) {
        super({ x, y, width, height });
    }

    draw(context) {
        if (!this.visible) return;

        // Draw background
        context.fillStyle = 'rgba(0, 0, 0, 0.7)';
        context.fillRect(this.x, this.y, this.width, this.height);

        // Draw border
        context.strokeStyle = '#fff';
        context.lineWidth = 2;
        context.strokeRect(this.x, this.y, this.width, this.height);

        super.draw(context); // Draw children
    }
}

The SelectableList Component

This is the heart of our menu. It manages a list of options, tracks the selected index, handles navigation input, and notifies its parent when an option is confirmed. This is analogous to a controlled component in a front-end framework.

// /components/SelectableList.js
import { UIComponent } from './UIComponent.js';

export class SelectableList extends UIComponent {
    constructor({ x, y, width, items, onConfirm, onCancel }) {
        super({ x, y, width });
        this.items = items; // e.g., ['Items', 'Skills', 'Save']
        this.selectedIndex = 0;
        this.lineHeight = 30;
        this.height = items.length * this.lineHeight;

        // Callbacks to notify parent
        this.onConfirm = onConfirm;
        this.onCancel = onCancel;
    }

    update(input) {
        if (!this.visible) return;

        if (input.isPressed('ArrowDown')) {
            this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
        } else if (input.isPressed('ArrowUp')) {
            this.selectedIndex = (this.selectedIndex - 1 + this.items.length) % this.items.length;
        } else if (input.isPressed('Enter')) {
            this.onConfirm(this.selectedIndex);
        } else if (input.isPressed('Escape')) {
            this.onCancel();
        }
    }

    draw(context) {
        if (!this.visible) return;

        context.save();
        context.translate(this.x, this.y);

        context.font = '20px Arial';
        context.fillStyle = 'white';

        this.items.forEach((item, index) => {
            // Draw item text
            context.fillText(item, 20, index * this.lineHeight + 20);
        });

        // Draw cursor
        context.fillText('>', 0, this.selectedIndex * this.lineHeight + 20);

        context.restore();
    }
}

3. Composing the MainMenuScene

Now we can compose these components to create our MainMenuScene. This scene acts as the container and controller, orchestrating the behavior of its children.

Notice how the MainMenuScene provides the callback functions (onConfirm, onCancel) to the SelectableList. This keeps the SelectableList generic and reusable—it doesn't need to know what happens when an item is selected, only that it needs to report the selection.

// /scenes/MainMenuScene.js
import { UIComponent } from '../components/UIComponent.js';
import { Window } from '../components/Window.js';
import { SelectableList } from '../components/SelectableList.js';

// Placeholder scenes for demonstration
class ItemScene extends UIComponent { /* ... */ }
class StatusScene extends UIComponent { /* ... */ }
class SaveScene extends UIComponent { /* ... */ }

export class MainMenuScene extends UIComponent {
    constructor({ sceneManager }) {
        super({ x: 0, y: 0, width: 816, height: 624 }); // Fullscreen
        this.sceneManager = sceneManager;

        const menuItems = ['Items', 'Status', 'Save', 'Exit'];
        
        // Create the main command window
        const commandWindow = new Window({ x: 550, y: 100, width: 200, height: 200 });

        // Create the list inside the window
        const commandList = new SelectableList({
            x: 15, y: 15, // Relative to the window
            width: 170,
            items: menuItems,
            onConfirm: (selectedIndex) => this.handleMenuConfirm(selectedIndex),
            onCancel: () => this.handleMenuCancel(),
        });

        commandWindow.addChild(commandList);
        this.addChild(commandWindow);
    }

    handleMenuConfirm(index) {
        switch (index) {
            case 0:
                console.log('Transitioning to Item Scene...');
                this.sceneManager.push(new ItemScene({ sceneManager: this.sceneManager }));
                break;
            case 1:
                console.log('Transitioning to Status Scene...');
                this.sceneManager.push(new StatusScene({ sceneManager: this.sceneManager }));
                break;
            case 2:
                console.log('Transitioning to Save Scene...');
                this.sceneManager.push(new SaveScene({ sceneManager: this.sceneManager }));
                break;
            case 3:
                this.handleMenuCancel(); // Exit menu
                break;
        }
    }

    handleMenuCancel() {
        console.log('Closing Main Menu...');
        this.sceneManager.pop();
    }
}

4. Managing Scenes with a Stack

The final piece is the SceneManager. As we designed in Module 3 and refined in the last lesson, a stack is the ideal data structure for managing game scenes.

  • push(scene): Pauses the current scene and activates a new one (e.g., opening the menu from the map).
  • pop(): Closes the current scene and resumes the one below it (e.g., closing the menu to return to the map).
  • active(): Returns the scene at the top of the stack.

The game's main loop only ever interacts with the active scene on the stack.

// /managers/SceneManager.js
export class SceneManager {
    constructor() {
        this.scenes = [];
    }

    // Get the scene at the top of the stack
    active() {
        return this.scenes[this.scenes.length - 1];
    }

    push(scene) {
        this.scenes.push(scene);
    }

    pop() {
        this.scenes.pop();
    }

    // Called from the main game loop
    update(input) {
        this.active()?.update(input);
    }

    draw(context) {
        // Draw all scenes, allowing for transparency
        this.scenes.forEach(scene => scene.draw(context));
    }
}

When the player presses the menu button on the map, the MapScene would execute sceneManager.push(new MainMenuScene({ sceneManager })). Our MainMenuScene then takes over. When it's done, it calls sceneManager.pop(), and control returns seamlessly to the MapScene.

Test your understanding!

Imagine you want to add a "Skills" option to the main menu, positioned between "Items" and "Status". What three specific changes would you need to make in the MainMenuScene.js file to implement this?

Show answer
  1. Update the menuItems array: Change ['Items', 'Status', 'Save', 'Exit'] to ['Items', 'Skills', 'Status', 'Save', 'Exit'].
  2. Create a placeholder SkillsScene: You'd need a class SkillsScene extends UIComponent { ... } for the manager to push.
  3. Update the handleMenuConfirm switch statement: Add a new case for the "Skills" option. Since "Skills" is now at index 1, you would need to shift the other cases down and add case 1: this.sceneManager.push(new SkillsScene(...)); break;.

Conclusion

In this lesson, we successfully translated the abstract logic of a JRPG menu into a concrete, working implementation. By building upon our component-based architecture, we created a MainMenuScene that is both functional and well-structured.

Key Takeaways:

  • JRPG menu logic can be modeled as a simple state machine driven by a selectedIndex variable.
  • Reusable components like Window and SelectableList allow us to quickly compose complex UI screens.
  • Using callbacks (like onConfirm) decouples child components from their parents, making the system more modular and scalable. This is a pattern you're already very familiar with from front-end development.
  • A stack-based SceneManager is an effective and powerful pattern for handling transitions between different game states, like moving from the map to the main menu and then into a sub-menu.

Preview of the next lesson:
Now that we can navigate to our sub-screens, it's time to build one out. In the next lesson, we'll implement the character status screen. We will create a StatusScene that fetches data from our actor objects (HP, MP, stats, etc.) and displays it using our reusable Window component and new text-rendering logic, bringing us one step closer to a fully functional game.

Can't find a good explanation? Sign up and we'll make it for you

Sign up