Recommended resources
The freely available resources this lesson will direct you to.
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.
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.
Downarrow: IncrementsselectedIndex.Uparrow: DecrementsselectedIndex.Confirmbutton: Executes an action based on the value ofselectedIndex.Cancelbutton: 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
Confirmbutton is pressed, aswitchstatement orif/elseblock usesselectedIndexto 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
- Update the
menuItemsarray: Change['Items', 'Status', 'Save', 'Exit']to['Items', 'Skills', 'Status', 'Save', 'Exit']. - Create a placeholder
SkillsScene: You'd need aclass SkillsScene extends UIComponent { ... }for the manager to push. - Update the
handleMenuConfirmswitch statement: Add a newcasefor the "Skills" option. Since "Skills" is now at index 1, you would need to shift the other cases down and addcase 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
selectedIndexvariable. - Reusable components like
WindowandSelectableListallow 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
SceneManageris 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.