Introducción al Desarrollo de Videojuegos con GameMaker · Module 8: HUDs, Interfaces, Menus, and Pause Systems
Creating an Interactive Main Menu
Lesson 38 of 60 · 60 min
Also available in: ES
Introduction: The Role of the Main Menu in the Player Experience
The main menu provides a player's first impression of a video game. Far from being a mere technical formality to load the opening level, a well-crafted title screen sets the artistic tone, establishes the audio atmosphere, and conveys the project's overall level of polish. From a software architecture perspective in GameMaker, the main menu serves as the entry point for the game's global state machine, managing user flow before complex character mechanics, enemy behaviors, or collision systems are initialized.
In this lesson, you will learn to build a modular, scalable, and interactive main menu system from scratch using GML Code. We will implement three fundamental options: 'Start Game', 'Options', and 'Exit'. Furthermore, we will resolve one of the most common UI design challenges in indie development: seamless hybrid control support, allowing players to navigate using the keyboard arrow keys (or W/S) and Enter, or interact directly by clicking with the mouse cursor, without input conflicts between the two schemes.
Environment Setup: Room, Fonts, and Required Assets
Before writing the first line of code, we need to prepare our asset structure within the GameMaker Asset Browser. A frequent beginner mistake is placing menu objects inside active gameplay rooms. A cleaner architecture isolates the menu within its own dedicated execution space.
Follow these steps to configure the basic assets:
1. **Creating the Menu Room:** In the 'Rooms' folder, create a new room named 'rm_menu'. In the Room Manager, make sure to drag 'rm_menu' to the absolute top of the list. In GameMaker, the first room listed in the Room Manager hierarchy is automatically loaded when the game launches.
2. **Configuring Room Dimensions:** Open 'rm_menu' and, in the room settings (Inspector or Room Properties), set the width to 1280 pixels and the height to 720 pixels, matching the standard 720p HD resolution used throughout our platformer project.
3. **Creating the Typography Font:** In the Asset Browser, right-click the 'Fonts' folder and create a font asset named 'fnt_menu'. Choose a clean, legible font installed on your system (such as Arial, Trebuchet MS, or an open-licensed pixel font), set its size to 24 points, and ensure common character ranges are enabled.
4. **Title Font:** Create a second font resource named 'fnt_menu_titulo' with a larger size, such as 48 points, which will be used to render the game's title or logo at the top of the screen.
5. **Creating the Controller Object:** In the 'Objects' folder, create an object named 'obj_menu'. This object does not need an assigned sprite, as its sole purpose is processing UI math and drawing visual elements via code on the user interface layer.

Data Structure in the Create Event: Modularity via Arrays
To avoid writing repetitive code blocks for every button on screen, we will use an array-based data structure. An array stores a sequential list of elements accessible through a zero-based numerical index. By defining our menu options within an array, adding a new entry like 'Credits' or 'Achievements' in the future requires merely inserting a new string into the list, without rewriting collision detection or rendering logic.
Open 'obj_menu', add the **Create** event, and write the following documented code:
// --- Create Event: obj_menu ---
// 1. Available menu options in sequential order
opciones = ["Start Game", "Options", "Exit"];
// 2. Index of currently selected option (0 = Start Game)
indice_seleccionado = 0;
// 3. Total number of menu items
total_opciones = array_length(opciones);
// 4. Visual layout and spacing parameters
gui_ancho = display_get_gui_width();
gui_alto = display_get_gui_height();
menu_x = gui_ancho * 0.5; // GUI horizontal center
menu_y = gui_alto * 0.55; // Vertical position of first button
espaciado_y = 54; // Pixel spacing between consecutive options
// 5. Color palette for visual feedback
color_normal = c_ltgray; // Inactive text
color_seleccion = c_yellow; // Highlighted text (keyboard or mouse)
color_sombra = c_dkgray; // Drop shadow color
// 6. Track previous mouse position to avoid focus conflicts
mouse_x_prev = device_mouse_x_to_gui(0);
mouse_y_prev = device_mouse_y_to_gui(0);
// 7. Approximate collision box half-dimensions per button
boton_ancho_medio = 160;
boton_alto_medio = 22;Let's examine the key variables: `indice_seleccionado` tracks which option currently holds 'focus'. The `total_opciones` variable updates dynamically by calling `array_length(opciones)`. Meanwhile, `boton_ancho_medio` and `boton_alto_medio` define the invisible rectangular boundary where the mouse cursor detects hover over a specific button.
Hybrid Navigation in the Step Event: Keyboard and Precise Mouse Hover Detection
The next step is programming the response to player input. Beginner menu implementations often suffer from two common pitfalls:
The first issue stems from incorrect coordinate systems. New developers frequently use GameMaker's built-in `mouse_x` and `mouse_y` variables within UI logic. These variables return the cursor's position in room world coordinates, which shift or scale whenever an in-game camera moves. To interact reliably with elements drawn on the GUI layer, you must use `device_mouse_x_to_gui(0)` and `device_mouse_y_to_gui(0)`, which translate physical pointer coordinates directly into GUI canvas space.
The second issue is input device priority conflicts. If the mouse rests motionless over option 0 ('Start Game') and the player presses the Down arrow to highlight 'Options', the system must not immediately force selection back to option 0 unless the player physically moves the mouse.
Add the **Step** event to 'obj_menu' and insert the code that handles both requirements seamlessly:
// --- Step Event: obj_menu ---
// 1. Get current mouse position in GUI coordinates
var _mouse_gui_x = device_mouse_x_to_gui(0);
var _mouse_gui_y = device_mouse_y_to_gui(0);
// Check if user physically moved the mouse this frame
var _mouse_se_movio = (_mouse_gui_x != mouse_x_prev || _mouse_gui_y != mouse_y_prev);
// Update previous position for the next frame
mouse_x_prev = _mouse_gui_x;
mouse_y_prev = _mouse_gui_y;
// 2. Interactive mouse collision detection
for (var i = 0; i < total_opciones; i++) {
var _opcion_y = menu_y + (i * espaciado_y);
// Define bounding box limits for the current option
var _x1 = menu_x - boton_ancho_medio;
var _x2 = menu_x + boton_ancho_medio;
var _y1 = _opcion_y - boton_alto_medio;
var _y2 = _opcion_y + boton_alto_medio;
// Update selection if mouse is inside area and moved
if (point_in_rectangle(_mouse_gui_x, _mouse_gui_y, _x1, _y1, _x2, _y2)) {
if (_mouse_se_movio) {
indice_seleccionado = i;
}
}
}
// 3. Capture keyboard input
var _arriba = keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W"));
var _abajo = keyboard_check_pressed(vk_down) || keyboard_check_pressed(ord("S"));
var _activar = keyboard_check_pressed(vk_enter) || keyboard_check_pressed(vk_space);
// Check for mouse left click
var _clic_raton = mouse_check_button_pressed(mb_left);
// 4. Process vertical keyboard navigation
if (_arriba) {
indice_seleccionado--;
// Wrap around: if moving above the first item, jump to the last
if (indice_seleccionado < 0) {
indice_seleccionado = total_opciones - 1;
}
}
if (_abajo) {
indice_seleccionado++;
// Wrap around: if moving below the last item, loop to the first
if (indice_seleccionado >= total_opciones) {
indice_seleccionado = 0;
}
}
// 5. Confirm selection (via keyboard or mouse click on focused option)
var _ejecutar_accion = false;
if (_activar) {
_ejecutar_accion = true;
}
if (_clic_raton) {
var _sel_y = menu_y + (indice_seleccionado * espaciado_y);
var _sel_x1 = menu_x - boton_ancho_medio;
var _sel_x2 = menu_x + boton_ancho_medio;
var _sel_y1 = _sel_y - boton_alto_medio;
var _sel_y2 = _sel_y + boton_alto_medio;
// Only execute if click occurred within selected button bounds
if (point_in_rectangle(_mouse_gui_x, _mouse_gui_y, _sel_x1, _sel_y1, _sel_x2, _sel_y2)) {
_ejecutar_accion = true;
}
}
// 6. Execute action based on selected button
if (_ejecutar_accion) {
switch (indice_seleccionado) {
case 0: // "Start Game"
// Verify that the level room exists before transitioning
if (room_exists(rm_nivel_1)) {
room_goto(rm_nivel_1);
} else {
show_debug_message("ERROR: Room rm_nivel_1 does not exist in the project.");
}
break;
case 1: // "Options"
// Conceptual placeholder for future settings panel
show_debug_message("Opening options menu...");
break;
case 2: // "Exit"
game_end();
break;
}
}💡 Technical note on `game_end()`: This function terminates the game process immediately on desktop platforms such as Windows, macOS, and Linux. In HTML5 browser builds or mobile platforms, the function is intentionally ignored by operating system sandboxing guidelines to prevent unprompted force-closures.
Visual Rendering in the Draw GUI Event
As covered in earlier lessons, any element intended for the user interface must be drawn exclusively inside the **Draw GUI** event rather than the standard Draw event. This ensures interface text does not distort or offset when a game camera tracks the player or zooms across large rooms.
The Draw GUI event also demands careful state management: if you modify text alignment (`draw_set_halign`, `draw_set_valign`) or the active drawing color (`draw_set_color`), you must restore them to default values at the end of the script. Otherwise, subsequent objects rendering text will inherit those settings, producing elusive visual glitches.
Create the **Draw GUI** event in 'obj_menu' and insert the following structured code:
// --- Draw GUI Event: obj_menu ---
// 1. Draw main game title
draw_set_font(fnt_menu_titulo);
draw_set_halign(fa_center);
draw_set_valign(fa_middle);
var _titulo_x = gui_ancho * 0.5;
var _titulo_y = gui_alto * 0.25;
// Title drop shadow for contrast and depth
draw_set_color(c_black);
draw_text(_titulo_x + 4, _titulo_y + 4, "RETRO 2D ADVENTURE");
// Title foreground text
draw_set_color(c_white);
draw_text(_titulo_x, _titulo_y, "RETRO 2D ADVENTURE");
// 2. Draw interactive options from array
draw_set_font(fnt_menu);
for (var i = 0; i < total_opciones; i++) {
var _opc_y = menu_y + (i * espaciado_y);
var _texto = opciones[i];
// Check if this option is currently selected
var _es_activa = (i == indice_seleccionado);
if (_es_activa) {
// Visual effect: draw decorative indicator brackets on sides
draw_set_color(color_seleccion);
draw_text(menu_x - boton_ancho_medio + 20, _opc_y, ">");
draw_text(menu_x + boton_ancho_medio - 20, _opc_y, "<");
// Selected option shadow
draw_set_color(color_sombra);
draw_text(menu_x + 2, _opc_y + 2, _texto);
// Selected option text
draw_set_color(color_seleccion);
draw_text(menu_x, _opc_y, _texto);
} else {
// Standard unselected option
draw_set_color(color_sombra);
draw_text(menu_x + 2, _opc_y + 2, _texto);
draw_set_color(color_normal);
draw_text(menu_x, _opc_y, _texto);
}
}
// 3. Essential cleanup: restore default rendering parameters
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_set_color(c_white);Practical Step-by-Step Example: Adding Polish with Animation and Visual Feedback
Great interface design does more than display options; it provides tactile visual feedback, commonly called 'game feel' or 'juice'. In commercial titles, a highlighted menu item rarely just swaps colors—it responds with subtle micro-animations such as scaling pulses or slight horizontal offsets.
Comparison of Menu Interaction Schemes
Understanding the technical trade-offs between different input methods is essential when designing 2D game user interfaces:
| Input Method | Technical Advantages | Implementation Challenges | Typical Fit |
|---|---|---|---|
| Keyboard / Gamepad Only | Deterministic discrete index logic (0, 1, 2); requires no collision box calculations. | Less intuitive for casual PC players who expect point-and-click functionality. | Consoles, Arcade, Retro PC titles. |
| Mouse Only (Pointer) | Highly intuitive; directly utilizes on-screen cursor position. | Requires precise coordinate translation to the GUI layer via device_mouse_*_to_gui. | Mobile touch games, Web PC, Strategy. |
| Integrated Hybrid (This Lesson's Method) | Maximum accessibility and user flexibility regardless of preferred input device. | Requires handling mouse movement precedence to prevent overriding keyboard selection. | Modern commercial PC and multi-platform games. |
Room Placement and Functional Testing
With the Create, Step, and Draw GUI events written in `obj_menu`, navigate to `rm_menu` in the Asset Browser. Create an Instances layer named 'Instances' (if not already present) and place a single instance of `obj_menu` onto the canvas. Because our code calculates positions relative to the GUI viewport dimensions (`display_get_gui_width()` and `display_get_gui_height()`), the placement coordinates of the instance in the room do not matter—the menu will automatically render centered on screen.
Run the game by pressing F5 (or clicking Run) and perform the following verification checks:
1. Press the Up and Down arrow keys. The highlight should wrap smoothly and cycle through all options.
2. Hover the mouse cursor over 'Options' and nudge it slightly. The highlight should switch to that option without any keyboard input.
3. Leaving the cursor stationary, press the Up arrow key. The selection should instantly move up to 'Start Game' without the mouse immediately snapping focus back.
4. Press Enter while 'Start Game' is highlighted: the game should smoothly load `rm_nivel_1`. Highlighting 'Exit' and pressing Enter or clicking should terminate the game window cleanly.
Practical Exercises
To reinforce the concepts covered in this lesson, complete these three hands-on exercises:
**Exercise 1: Adding a Credits Option to the Menu**
Modify the `opciones` array in the Create event of `obj_menu` to insert a 'Credits' option between 'Options' and 'Exit'. Update the `switch` statement in the Step event so that selecting Credits outputs the developer name and game version to the debug output console using `show_debug_message()`.
*Verification Criteria:* The menu now renders four evenly spaced options. Navigating downward past the final option wraps seamlessly back to 'Start Game' without index out-of-bounds runtime errors.
**Exercise 2: Adding Audio Cues to Menu Navigation**
While comprehensive audio architecture is covered in Module 9, GameMaker makes playing one-shot sound effects simple via `audio_play_sound(snd_name, priority, loop)`. Using an existing short sound asset (or importing a simple click sound effect into the Sounds folder), implement a condition in the Step event that triggers the sound effect whenever `indice_seleccionado` changes.
*Verification Criteria:* The audio cue must trigger strictly when focus shifts from one option to another, rather than firing continuously on every frame while the mouse rests on a button.
**Exercise 3: Exit Confirmation Submenu**
Declare a boolean variable `confirmando_salida = false;` in the Create event. When the player chooses 'Exit', rather than immediately calling `game_end()`, set `confirmando_salida` to `true`. Adapt the menu logic so that while this confirmation state is active, the screen prompts 'Are you sure you want to quit? Yes / No', returning to the main menu if 'No' is selected or terminating the game if 'Yes' is confirmed.
*Verification Criteria:* The system suspends regular menu navigation, presents the confirmation prompt, and closes the game only upon explicit user confirmation.
Summary
- The main menu should live in a dedicated room (rm_menu) placed at the very top of the Room Manager hierarchy so it runs first on startup.
- Using arrays to store menu entries produces modular UI architectures that let you add or remove buttons without modifying core input logic.
- For UI mouse interactions, always use device_mouse_x_to_gui(0) and device_mouse_y_to_gui(0) instead of room-space mouse coordinates.
- Keyboard and mouse input conflicts are resolved by verifying whether the mouse physically moved before updating selection based on cursor proximity.
- All user interface rendering belongs in the Draw GUI event to guarantee visual stability independent of room cameras or view zoom.
- Always restore default drawing states (draw_set_halign, draw_set_valign, draw_set_color) at the end of rendering routines to prevent graphical side effects across other objects.
Practise what you learned
Exercise 1 · 1 point
When implementing mouse interaction for a GUI, why is it incorrect to use the built-in variables mouse_x and mouse_y?
Exercise 2 · 1 point
Complete the code in the Create event of obj_menu to dynamically calculate the number of options and store the initial horizontal position of the mouse on the GUI: // Total number of options in the menu total_opciones = (opciones); // Initial record of the mouse X position on the GUI mouse_x_prev = (0);
Exercise 3 · 1 point
In GameMaker, the room placed at the very top of the hierarchy in the Room Manager is the one that runs automatically when the game launches.
Sign in to save your progress — Sign in