Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 8: HUD, interfaces, menús y sistema de pausa.

Creación del menú principal interactivo

Clase 38 de 60 · 60 min

También disponible en: EN

Introducción: La importancia del menú principal en la experiencia del jugador

El menú principal es la primera impresión que un jugador tiene de un videojuego. Lejos de ser un simple trámite técnico para abrir el nivel inicial, una interfaz de inicio bien concebida establece el tono artístico, la ambientación sonora y el nivel de pulido general del proyecto. Desde una perspectiva de arquitectura de software en GameMaker, el menú principal representa el punto de entrada de la máquina de estados global del juego, gestionando el flujo del usuario antes de que se inicialicen las mecánicas complejas del personaje, los enemigos o los sistemas de colisión.

En esta lección aprenderás a construir un sistema de menú principal modular, escalable e interactivo desde cero utilizando GML Code. Implementaremos tres opciones fundamentales: 'Iniciar Juego', 'Opciones' y 'Salir'. Además, resolveremos uno de los mayores desafíos de diseño de interfaces en el desarrollo independiente: la compatibilidad híbrida y simultánea de controles, permitiendo al usuario navegar con las flechas del teclado (o teclas W/S) y presionar Enter, o interactuar directamente haciendo clic con el cursor del ratón sin generar conflictos entre ambos esquemas.

Preparación del entorno: Sala, fuentes y recursos necesarios

Antes de escribir la primera línea de código, debemos preparar la estructura de recursos en el Asset Browser de GameMaker. Un error común al comenzar es mezclar los objetos del menú dentro de las mismas salas donde se desarrollan los niveles. La arquitectura correcta consiste en aislar el menú en su propio espacio de ejecución.

Sigue estos pasos para configurar los recursos básicos:

1. **Creación de la sala del menú:** En la carpeta 'Rooms', crea una nueva sala llamada 'rm_menu'. En el gestor de salas (Room Manager), asegúrate de arrastrar 'rm_menu' hasta la parte superior absoluta de la lista. En GameMaker, la primera sala que aparece en la jerarquía del Room Manager es la que se ejecuta automáticamente al iniciar el ejecutable del juego.

2. **Configuración de dimensiones:** Abre 'rm_menu' y, en las propiedades de la sala (Inspector o Room Properties), define el tamaño en 1280 píxeles de ancho por 720 píxeles de alto, coincidiendo con la resolución estándar en alta definición que venimos utilizando en el proyecto de plataformas.

3. **Creación de la fuente tipográfica:** En el Asset Browser, haz clic derecho sobre la carpeta 'Fonts' y crea un recurso de fuente llamado 'fnt_menu'. Selecciona una tipografía limpia y legible instalada en tu sistema (como Arial, Trebuchet MS o una fuente de estilo pixelado si dispones de una con licencia libre), asígnale un tamaño de 24 puntos y asegúrate de habilitar los caracteres comunes.

4. **Fuente del título:** Crea una segunda fuente llamada 'fnt_menu_titulo' con un tamaño superior, por ejemplo 48 puntos, que servirá para renderizar el logotipo o nombre del videojuego en la parte superior de la pantalla.

5. **Creación del objeto controlador:** En la carpeta 'Objects', crea un objeto llamado 'obj_menu'. Este objeto no necesita un sprite asignado, ya que se encargará exclusivamente de procesar los datos matemáticos de la interfaz y dibujarlos por código en la capa de interfaz de usuario.

Diagrama esquemático que muestra el flujo de entradas de teclado y ratón hacia el objeto controlador obj_menu y la distribución visual de las opciones en la pantalla GUI.
Figura 1: Arquitectura de control y distribución de coordenadas en la interfaz del menú principal. · Imagen generada con IA

Estructura de datos en el evento Create: Modularidad mediante arrays

Para evitar escribir bloques de código redundantes por cada botón que queramos mostrar, utilizaremos una estructura de datos basada en arrays (arreglos). Un array almacena una lista secuencial de elementos accesibles mediante un índice numérico comenzando desde cero. Al definir las opciones de nuestro menú en un array, si en el futuro queremos añadir una opción adicional como 'Créditos' o 'Logros', bastará con agregar una palabra a la lista sin tener que reescribir los algoritmos de detección o dibujo.

Abre 'obj_menu', añade el evento **Create** y escribe el siguiente bloque de código documentado:

// --- Evento Create: obj_menu ---

// 1. Opciones del menú disponibles en orden secuencial
opciones = ["Iniciar Juego", "Opciones", "Salir"];

// 2. Índice de la opción actualmente seleccionada (0 = Iniciar Juego)
indice_seleccionado = 0;

// 3. Cantidad total de elementos en el menú
total_opciones = array_length(opciones);

// 4. Parámetros de diseño visual y espaciado
gui_ancho = display_get_gui_width();
gui_alto  = display_get_gui_height();

menu_x = gui_ancho * 0.5;   // Centro horizontal de la interfaz
menu_y = gui_alto * 0.55;   // Posición vertical del primer botón
espaciado_y = 54;           // Separación en píxeles entre opciones consecutivas

// 5. Paleta de colores para retroalimentación visual
color_normal      = c_ltgray;  // Texto inactivo
color_seleccion   = c_yellow;  // Texto resaltado por teclado o ratón
color_sombra      = c_dkgray;  // Sombra proyectada del texto

// 6. Registro de la última posición del ratón para evitar conflictos de enfoque
mouse_x_prev = device_mouse_x_to_gui(0);
mouse_y_prev = device_mouse_y_to_gui(0);

// 7. Dimensiones aproximadas de la caja de colisión interactiva por botón
boton_ancho_medio = 160;
boton_alto_medio  = 22;

Analicemos las variables clave: `indice_seleccionado` representa cuál de las opciones tiene el 'foco' o atención en este instante. La variable `total_opciones` se actualiza dinámicamente llamando a `array_length(opciones)`. Por su parte, `boton_ancho_medio` y `boton_alto_medio` definirán el área rectangular invisible donde el cursor del ratón detectará que estamos pasando por encima de un botón específico.

Navegación híbrida en el evento Step: Teclado y detección precisa de ratón

El siguiente desafío consiste en programar la respuesta a los controles del usuario. Existen dos problemas comunes en los menús para principiantes:

El primero es el uso incorrecto de coordenadas. Muchos desarrolladores novatos emplean las variables nativas `mouse_x` y `mouse_y` dentro de sus interfaces. Estas variables devuelven la posición del cursor en el espacio del mundo de la sala (Room Space), el cual puede verse desplazado o escalado si existe una cámara activa. Para interactuar con elementos dibujados en la capa de interfaz, es obligatorio utilizar las funciones `device_mouse_x_to_gui(0)` y `device_mouse_y_to_gui(0)`, que traducen la posición física del puntero a las coordenadas de la capa GUI.

El segundo problema es el conflicto de prioridad entre periféricos. Si el ratón permanece quieto sobre la opción 0 ('Iniciar Juego') y el usuario presiona la flecha abajo para elegir 'Opciones', el sistema no debe permitir que el ratón fuerce instantáneamente la selección de vuelta a la opción 0 a menos que el jugador realmente mueva el ratón.

Añade el evento **Step** en 'obj_menu' y copia el código que resuelve ambas necesidades de forma fluida:

// --- Evento Step: obj_menu ---

// 1. Obtener la posición actual del cursor en coordenadas GUI
var _mouse_gui_x = device_mouse_x_to_gui(0);
var _mouse_gui_y = device_mouse_y_to_gui(0);

// Comprobar si el usuario movió físicamente el ratón en este fotograma
var _mouse_se_movio = (_mouse_gui_x != mouse_x_prev || _mouse_gui_y != mouse_y_prev);

// Actualizar la memoria de posición para el próximo fotograma
mouse_x_prev = _mouse_gui_x;
mouse_y_prev = _mouse_gui_y;

// 2. Detección de colisión interactiva con el ratón
for (var i = 0; i < total_opciones; i++) {
    var _opcion_y = menu_y + (i * espaciado_y);
    
    // Definir los límites del rectángulo interactivo para la opción actual
    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;
    
    // Si el puntero está dentro del área y hubo movimiento, actualizar la selección
    if (point_in_rectangle(_mouse_gui_x, _mouse_gui_y, _x1, _y1, _x2, _y2)) {
        if (_mouse_se_movio) {
            indice_seleccionado = i;
        }
    }
}

// 3. Captura de entradas digitales del teclado
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);

// Comprobar si se hizo clic izquierdo con el ratón
var _clic_raton = mouse_check_button_pressed(mb_left);

// 4. Procesar el desplazamiento vertical por teclado
if (_arriba) {
    indice_seleccionado--;
    // Ciclo infinito: si sube más allá de la primera opción, salta a la última
    if (indice_seleccionado < 0) {
        indice_seleccionado = total_opciones - 1;
    }
}

if (_abajo) {
    indice_seleccionado++;
    // Ciclo infinito: si baja más allá de la última opción, vuelve a la primera
    if (indice_seleccionado >= total_opciones) {
        indice_seleccionado = 0;
    }
}

// 5. Confirmar selección (tanto por teclado como por clic sobre la opción enfocada)
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;
    
    // Solo ejecutar si el clic ocurrió dentro del botón seleccionado
    if (point_in_rectangle(_mouse_gui_x, _mouse_gui_y, _sel_x1, _sel_y1, _sel_x2, _sel_y2)) {
        _ejecutar_accion = true;
    }
}

// 6. Ejecución de la lógica según el botón elegido
if (_ejecutar_accion) {
    switch (indice_seleccionado) {
        case 0: // "Iniciar Juego"
            // Verificamos si existe la sala del nivel antes de hacer la transición
            if (room_exists(rm_nivel_1)) {
                room_goto(rm_nivel_1);
            } else {
                show_debug_message("ERROR: La sala rm_nivel_1 no existe en el proyecto.");
            }
            break;
            
        case 1: // "Opciones"
            // Marcador de posición conceptual para futuras configuraciones
            show_debug_message("Accediendo al panel de opciones...");
            break;
            
        case 2: // "Salir"
            game_end();
            break;
    }
}

💡 Nota técnica sobre `game_end()`: Esta función cierra el proceso del juego inmediatamente en sistemas de escritorio como Windows, macOS y Linux. En plataformas web (HTML5) o dispositivos móviles, la función es deliberadamente ignorada por las directrices del sistema operativo para evitar cierres forzados no autorizados por el usuario.

Renderizado visual en el evento Draw GUI

Como aprendimos en lecciones anteriores del Módulo 8, cualquier elemento que forme parte de la interfaz de usuario debe pintarse obligatoriamente dentro del evento **Draw GUI** y nunca en el evento Draw tradicional. Esto garantiza que el texto no se distorsione cuando la cámara siga al jugador o aplique zoom en salas extensas.

En el evento Draw GUI también es imperativo aplicar buenas prácticas de preservación de estado: si cambiamos la alineación de texto (`draw_set_halign`, `draw_set_valign`) o el color activo (`draw_set_color`), debemos restaurarlos a sus valores estándar al finalizar el script. De lo contrario, otros objetos que dibujen texto posteriormente heredarán esa configuración de manera involuntaria, provocando errores visuales difíciles de depurar.

Crea el evento **Draw GUI** en 'obj_menu' e inserta el siguiente código estructurado:

// --- Evento Draw GUI: obj_menu ---

// 1. Dibujar el título principal del videojuego
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;

// Sombra decorativa del título para dar contraste y profundidad
draw_set_color(c_black);
draw_text(_titulo_x + 4, _titulo_y + 4, "AVENTURA RETRO 2D");

// Texto frontal del título
draw_set_color(c_white);
draw_text(_titulo_x, _titulo_y, "AVENTURA RETRO 2D");

// 2. Dibujar las opciones interactivas del 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];
    
    // Evaluar si esta opción específica es la seleccionada
    var _es_activa = (i == indice_seleccionado);
    
    if (_es_activa) {
        // Efecto visual: dibujar un indicador decorativo a los costados
        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, "<");
        
        // Sombra de la opción seleccionada
        draw_set_color(color_sombra);
        draw_text(menu_x + 2, _opc_y + 2, _texto);
        
        // Texto de la opción seleccionada
        draw_set_color(color_seleccion);
        draw_text(menu_x, _opc_y, _texto);
    } else {
        // Opción estándar sin seleccionar
        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. Restauración indispensable de parámetros estándar de renderizado
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_set_color(c_white);

Ejemplo práctico paso a paso: Pulido con animación y respuesta visual

Un buen diseño de interfaz no solo comunica información, sino que ofrece una sensación táctil ('game feel' o 'juice'). En interfaces profesionales, la opción seleccionada no se limita a cambiar de color, sino que responde con microanimaciones como un leve pulso de escala o desplazamiento horizontal.

Comparativa de esquemas de interacción en menús

Es fundamental comprender las fortalezas y limitaciones técnicas que presenta cada método de entrada a la hora de diseñar interfaces de usuario en videojuegos 2D:

Método de EntradaVentajas TécnicasDesafíos de ImplementaciónCompatibilidad Típica
Solo Teclado / GamepadLógica determinista basada en índices discretos (0, 1, 2); no requiere cálculos de áreas de colisión.Menos intuitivo para jugadores casuales de PC que esperan poder hacer clic en pantalla.Consolas, Arcade, PC retro.
Solo Ratón (Puntero)Muy intuitivo; aprovecha directamente la posición espacial del cursor en pantalla.Requiere traducir con precisión las coordenadas físicas a la capa GUI con device_mouse_*_to_gui.Juegos móviles (touch), PC Web, Estrategia.
Híbrido Integrado (El método de esta lección)Máxima accesibilidad y accesos rápidos sin importar cómo elija jugar el usuario.Requiere gestionar la precedencia del movimiento del ratón para no sobreescribir la selección del teclado.Títulos comerciales modernos de PC y multiplataforma.

Instalación en la sala y pruebas de funcionamiento

Una vez escritos los eventos Create, Step y Draw GUI en `obj_menu`, dirígete a la sala `rm_menu` en el Asset Browser. Crea una capa de instancias llamada 'Instances' (si no existe ya) y arrastra una única instancia de `obj_menu` al lienzo. Dado que nuestro código calcula las posiciones basándose en el ancho y alto del área GUI (`display_get_gui_width()` y `display_get_gui_height()`), no importa la posición exacta en la que sitúes la instancia en la sala: el menú siempre se dibujará perfectamente centrado en pantalla.

Ejecuta el juego pulsando F5 (o el botón Run) y realiza las siguientes comprobaciones de depuración:

1. Presiona las flechas de dirección arriba y abajo. La opción resaltada debe alternar cíclicamente de forma instantánea.

2. Coloca el puntero del ratón sobre 'Opciones' y muévelo ligeramente. El resaltado debe cambiar a dicha opción sin necesidad de presionar ninguna tecla.

3. Con el cursor quieto, pulsa la tecla arriba. La selección debe subir inmediatamente a 'Iniciar Juego' sin que el ratón bloquee la acción.

4. Presiona Enter sobre 'Iniciar Juego': la sala debe cambiar limpiamente a `rm_nivel_1`. Si seleccionas 'Salir', la ventana del juego debe cerrarse de manera ordenada.

Ejercicios prácticos

Para consolidar los conocimientos adquiridos en esta clase, realiza las siguientes tres actividades prácticas:

**Ejercicio 1: Incorporación de una nueva opción de créditos en el menú**

Modifica el array `opciones` en el evento Create de `obj_menu` para añadir la opción 'Créditos' entre 'Opciones' y 'Salir'. Modifica la estructura `switch` en el evento Step para que, al confirmar la opción de créditos, se imprima en la consola de depuración mediante `show_debug_message()` el nombre del desarrollador y la versión del juego.

*Criterio de comprobación:* El menú ahora debe mostrar cuatro opciones alineadas. Al presionar la flecha abajo desde la última opción, debe regresar a 'Iniciar Juego' sin desbordamientos de índice ni errores en tiempo de ejecución.

**Ejercicio 2: Sonorización de la navegación**

Aunque la gestión integral de audio se profundizará en el Módulo 9, GameMaker permite reproducir efectos simples con `audio_play_sound(snd_nombre, prioridad, loop)`. Si dispones de un sonido corto en tu proyecto (o tras importar un efecto de clic básico en la carpeta Sounds), programa una condición en el evento Step para que, cada vez que `indice_seleccionado` cambie de valor, se reproduzca el sonido de navegación.

*Criterio de comprobación:* El sonido debe emitirse únicamente cuando el foco cambie de una opción a otra, evitando reproducirse continuamente en cada fotograma si el jugador mantiene el ratón inmóvil sobre el mismo botón.

**Ejercicio 3: Submenú de confirmación de salida**

Implementa una variable booleana `confirmando_salida = false;` en el evento Create. Cuando el usuario elija la opción 'Salir', en lugar de invocar inmediatamente `game_end()`, cambia `confirmando_salida` a `true`. Modifica la lógica para que, mientras este estado esté activo, el menú muestre en pantalla las opciones '¿Seguro que deseas salir? Sí / No', permitiendo volver al menú principal si se elige 'No' o cerrar el juego si se elige 'Sí'.

*Criterio de comprobación:* El sistema debe suspender la navegación del menú raíz y gestionar con éxito la bifurcación condicional, cerrando el juego solo si el usuario confirma de forma explícita.

Resumen

  • El menú principal debe alojarse en una sala independiente (rm_menu) ubicada al principio de la jerarquía del Room Manager para ser la primera pantalla en ejecutarse.
  • El uso de arrays para definir las opciones permite crear interfaces modulares, facilitando añadir o remover botones sin reescribir la lógica de navegación.
  • Para la interacción con ratón en interfaces, es mandatorio utilizar device_mouse_x_to_gui(0) y device_mouse_y_to_gui(0) en lugar de las coordenadas del espacio de sala.
  • El conflicto entre teclado y ratón se soluciona detectando si el puntero se movió físicamente entre fotogramas antes de actualizar el índice por proximidad.
  • El dibujado de interfaces siempre pertenece al evento Draw GUI, garantizando independencia total respecto a la cámara y zoom del nivel.
  • Es imprescindible restaurar las propiedades de texto (draw_set_halign, draw_set_valign, draw_set_color) al finalizar el evento de renderizado para evitar efectos secundarios en otros objetos.

Practica lo aprendido

Ejercicio 1 · 1 punto

Al desarrollar la interacción con el ratón en la interfaz, ¿por qué no es correcto utilizar las variables nativas mouse_x y mouse_y?

Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 2 · 1 punto

Completa el código del evento Create de obj_menu para calcular dinámicamente la cantidad de elementos y registrar la posición inicial horizontal del ratón en la interfaz: // Cantidad total de elementos en el menú total_opciones = (opciones); // Registro inicial de la posición X del ratón en la GUI mouse_x_prev = (0);

Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 3 · 1 punto

En GameMaker, la primera sala que aparece ubicada en la parte superior absoluta de la jerarquía del Room Manager es la que se ejecuta automáticamente al iniciar el ejecutable del juego.

Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ingresa para guardar tu progreso — Ingresar