Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 8: HUD, interfaces, menús y sistema de pausa.
Sistema de pausa funcional
Clase 39 de 60 · 60 min
También disponible en: EN
Arquitectura y Fundamentos de un Sistema de Pausa
Pausar un videojuego puede parecer a simple vista una tarea trivial: simplemente 'detener el tiempo'. Sin embargo, en el desarrollo de software y en particular dentro del bucle de juego (Game Loop) de GameMaker, la computadora no se detiene de forma mágica; el motor sigue procesando fotogramas, evaluando eventos y dibujando elementos en pantalla continuamente. Si detuviéramos la ejecución del motor por completo, el sistema operativo consideraría que la ventana del juego se ha congelado, dejando de responder a cualquier interacción del usuario.
Existen principalmente dos filosofías para construir un sistema de pausa en entornos bidimensionales:
1. **Pausa mediante banderas condicionales (Flags):** Consiste en envolver todo el código de movimiento, colisión y animación de cada objeto dentro de una condición como `if (!global.juego_pausado) { ... }`. Si bien es intuitiva para proyectos muy pequeños, resulta propensa a errores humanos a medida que el proyecto crece. Olvidar una comprobación en un solo enemigo, proyectil o alarma provoca que dicho elemento continúe moviéndose mientras el resto del mundo permanece estático.
2. **Pausa por congelamiento de instancias y captura de superficie:** Es el enfoque profesional y estándar de GameMaker. Se basa en desactivar temporalmente todas las instancias del mundo físico y lógico mediante las funciones nativas `instance_deactivate_all()` o `instance_deactivate_region()`. Al desactivarse, las instancias suspenden de inmediato sus eventos `Step`, `Alarm` y `Collision`. Para evitar que el mundo desaparezca de la pantalla al dejar de ejecutarse sus eventos `Draw`, capturamos una instantánea visual del juego a través de una superficie (`surface`) y la dibujamos estática debajo de nuestro menú de pausa interactivo.
A lo largo de esta lección, implementaremos este segundo método creando un controlador dedicado: `obj_pausa`. Este objeto administrará la captura visual, el congelamiento del mundo, la navegación del menú flotante y la reactivación limpia de todas las entidades.

Creación del Controlador de Pausa: obj_pausa
Para garantizar que nuestro sistema de pausa funcione de manera autónoma sin interferir con la lógica interna de los niveles ni con el HUD que construimos en lecciones anteriores, crearemos un objeto controlador especializado llamado `obj_pausa`. Este objeto debe ser persistente para que no tengamos que recrearlo al cambiar de habitación, o bien colocarse en cada nivel jugable.
Crea un nuevo objeto en el panel de recursos de GameMaker (Asset Browser), asígnale el nombre `obj_pausa` y no le asignes ningún sprite. Marca la casilla **Persistent** (Persistente) si deseas que acompañe al jugador en todas las salas jugables, aunque también es completamente válido colocarlo directamente en la capa de instancias (`Instances`) o en una capa de gestión (`Controllers`) de la sala `rm_nivel_1`.
Inicialización de Variables en el Evento Create
En el evento `Create` de `obj_pausa`, declaramos las variables que gobernarán el estado de la pausa, la referencia a la superficie gráfica donde almacenaremos la captura del fotograma congelado, y los elementos que integran el menú interactivo flotante.
/// @description Evento Create de obj_pausa
// Estado de la pausa
esta_pausado = false;
// Identificador de la superficie de congelamiento
surf_pausa = -1;
// Opciones del menú de pausa
opciones = [
"Reanudar",
"Reiniciar Nivel",
"Menú Principal"
];
// Índice de la opción seleccionada actualmente (0 es "Reanudar")
opcion_actual = 0;
total_opciones = array_length(opciones);
// Propiedades visuales del menú
ancho_gui = display_get_gui_width();
alto_gui = display_get_gui_height();
separacion_lineas = 40;
// Control de entrada para evitar selecciones repetitivas involuntarias
espera_entrada = 0;
tiempo_espera = 10;Analicemos con detalle estas declaraciones:
| Variable | Tipo | Propósito |
|---|---|---|
| esta_pausado | Booleano | Determina si el bucle principal de juego está congelado y el menú activo. |
| surf_pausa | ID de Superficie | Almacena el índice de la superficie de memoria que contendrá el clon de la pantalla. |
| opciones | Arreglo (Array) | Contiene las cadenas de texto que se desplegarán como opciones navegables. |
| opcion_actual | Entero | Apunta al índice del arreglo que el jugador tiene seleccionado en un instante dado. |
| espera_entrada | Entero | Temporizador que suaviza el desplazamiento por el menú cuando se sostiene una tecla. |
Captura Visual y Desactivación en el Evento Step
El evento `Step` de `obj_pausa` es el cerebro operativo del sistema. Cuando el juego se encuentra en ejecución normal, este evento escucha la pulsación de la tecla de escape (`vk_escape`) o el botón Start del gamepad. Cuando el juego entra en pausa, el mismo evento asume la responsabilidad de interpretar la navegación por las opciones del menú.
Un detalle crítico en GameMaker: al llamar a `instance_deactivate_all(true)`, el argumento `true` (parámetro `notme`) indica al motor que congele todas las instancias de la escena **excepto** la instancia que está ejecutando la llamada (es decir, `obj_pausa`). Gracias a esto, `obj_pausa` seguirá procesando sus eventos `Step` y `Draw GUI` con total normalidad, permitiendo responder al teclado y dibujar el menú flotante.
/// @description Evento Step de obj_pausa
// 1. Detección de la entrada de activación / desactivación
var _tecla_pausa = keyboard_check_pressed(vk_escape) || gamepad_button_check_pressed(0, gp_start);
if (_tecla_pausa)
{
esta_pausado = !esta_pausado;
if (esta_pausado)
{
// Obtenemos el tamaño real del lienzo de renderizado
var _w = surface_get_width(application_surface);
var _h = surface_get_height(application_surface);
// Creamos una superficie temporal y clonamos el contenido actual del juego
surf_pausa = surface_create(_w, _h);
surface_copy(surf_pausa, 0, 0, application_surface);
// Desactivamos todas las entidades del mundo, conservando únicamente este objeto
instance_deactivate_all(true);
// Reseteamos el cursor al abrir el menú
opcion_actual = 0;
}
else
{
// Si reanudamos, reactivamos todas las entidades inmediatamente
instance_activate_all();
// Liberamos la memoria de la superficie clonada si aún existe
if (surface_exists(surf_pausa))
{
surface_free(surf_pausa);
surf_pausa = -1;
}
}
}
// 2. Lógica de navegación interna del menú si estamos en pausa
if (esta_pausado)
{
// Lectura vertical (arriba / abajo)
var _arriba = keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W")) || gamepad_button_check_pressed(0, gp_padu);
var _abajo = keyboard_check_pressed(vk_down) || keyboard_check_pressed(ord("S")) || gamepad_button_check_pressed(0, gp_padd);
var _seleccionar = keyboard_check_pressed(vk_enter) || keyboard_check_pressed(vk_space) || gamepad_button_check_pressed(0, gp_face1);
// Movimiento vertical del cursor con ciclo cerrado
if (_arriba)
{
opcion_actual -= 1;
if (opcion_actual < 0)
{
opcion_actual = total_opciones - 1;
}
}
if (_abajo)
{
opcion_actual += 1;
if (opcion_actual >= total_opciones)
{
opcion_actual = 0;
}
}
// Ejecución de la opción confirmada
if (_seleccionar)
{
switch (opcion_actual)
{
case 0: // Reanudar
esta_pausado = false;
instance_activate_all();
if (surface_exists(surf_pausa))
{
surface_free(surf_pausa);
surf_pausa = -1;
}
break;
case 1: // Reiniciar Nivel
esta_pausado = false;
instance_activate_all();
if (surface_exists(surf_pausa))
{
surface_free(surf_pausa);
surf_pausa = -1;
}
room_restart();
break;
case 2: // Menú Principal
esta_pausado = false;
instance_activate_all();
if (surface_exists(surf_pausa))
{
surface_free(surf_pausa);
surf_pausa = -1;
}
room_goto(rm_menu);
break;
}
}
}💡 ¡Regla de oro de GameMaker! Siempre que reinicies un nivel con room_restart() o cambies de habitación con room_goto() desde un estado de pausa, DEBES llamar a instance_activate_all() y liberar la superficie primero. Si cambias de sala con instancias desactivadas, GameMaker no ejecutará los eventos Destroy ni Clean Up de dichas entidades desactivadas, originando fugas de memoria y bloqueos de lógica en tus datos persistentes.
Renderizado del Menú Flotante en Draw GUI
La presentación visual de la pausa se compone de tres capas superpuestas que dibujamos exclusivamente dentro del evento `Draw GUI`:
1. **El fotograma de fondo:** La superficie congelada (`surf_pausa`). Si no la dibujáramos, el fondo se volvería negro o parpadearía, pues las instancias del juego ya no están ejecutando sus eventos de dibujo regulares. 2. **La cortina de atenuación:** Un rectángulo negro con transparencia parcial (alfa al 50% o 60%) que oscurece el mundo congelado para darle un matiz dramático y facilitar el contraste y legibilidad de los textos. 3. **El panel y los textos del menú:** Una ventana centrada con bordes definidos que alberga el título 'PAUSA' y la lista de opciones navegables, resaltando la opción seleccionada con un color diferente o un icono indicador.
/// @description Evento Draw GUI de obj_pausa
// Solo dibujamos la interfaz de pausa si el estado activo es verdadero
if (!esta_pausado) exit;
// Aseguramos dimensiones actualizadas del viewport de la interfaz gráfica
var _gui_w = display_get_gui_width();
var _gui_h = display_get_gui_height();
// 1. DIBUJAR LA SUPERFICIE DE FONDO CONGELADA
// Verificamos rigurosamente si la superficie sigue existiendo en la GPU
if (surface_exists(surf_pausa))
{
// Dibujamos la superficie adaptándola a la escala del canvas GUI
draw_surface_stretched(surf_pausa, 0, 0, _gui_w, _gui_h);
}
else
{
// Si la superficie se destruyó (por ejemplo, por minimizar la ventana),
// dibujamos una base sólida para que el menú siga siendo legible.
draw_set_color(c_dkgray);
draw_rectangle(0, 0, _gui_w, _gui_h, false);
}
// 2. CORTINA DE ATENUACIÓN OSCURA
draw_set_color(c_black);
draw_set_alpha(0.65);
draw_rectangle(0, 0, _gui_w, _gui_h, false);
draw_set_alpha(1.0); // Restauramos el canal alfa de inmediato
// 3. CAJA FLOTANTE DEL MENÚ
var _caja_w = 400;
var _caja_h = 280;
var _centro_x = _gui_w * 0.5;
var _centro_y = _gui_h * 0.5;
var _x1 = _centro_x - (_caja_w * 0.5);
var _y1 = _centro_y - (_caja_h * 0.5);
var _x2 = _centro_x + (_caja_w * 0.5);
var _y2 = _centro_y + (_caja_h * 0.5);
// Fondo del panel flotante
draw_set_color(make_color_rgb(25, 25, 40));
draw_rectangle(_x1, _y1, _x2, _y2, false);
// Borde decorativo exterior
draw_set_color(make_color_rgb(180, 180, 220));
draw_rectangle(_x1, _y1, _x2, _y2, true);
// Borde interior fino
draw_rectangle(_x1 + 4, _y1 + 4, _x2 - 4, _y2 - 4, true);
// 4. TEXTOS Y OPCIONES
// Configuración de alineación de texto
draw_set_halign(fa_center);
draw_set_valign(fa_middle);
// Título principal
draw_set_color(c_yellow);
draw_text_transformed(_centro_x, _y1 + 45, "- PAUSA -", 1.5, 1.5, 0);
// Despliegue dinámico de opciones
var _inicio_opciones_y = _y1 + 120;
for (var i = 0; i < total_opciones; i++)
{
var _pos_y = _inicio_opciones_y + (i * separacion_lineas);
if (i == opcion_actual)
{
// Opción resaltada
draw_set_color(c_white);
// Flechas indicadoras de selección
var _texto_opcion = "> " + opciones[i] + " <";
draw_text(_centro_x, _pos_y, _texto_opcion);
}
else
{
// Opciones secundarias no seleccionadas
draw_set_color(c_gray);
draw_text(_centro_x, _pos_y, opciones[i]);
}
}
// Restaurar configuraciones globales de texto para evitar efectos secundarios en otros objetos
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_set_color(c_white);Gestión Crítica de Memoria: El Evento Clean Up
Las superficies en GameMaker residen en la VRAM (memoria de video de la tarjeta gráfica). A diferencia de las variables numéricas estándar o los textos simples, el recolector de basura de GameMaker no puede eliminar superficies automáticamente cuando una sala termina o un objeto desaparece. Si creas una superficie y destruyes el objeto sin liberar su memoria, esos megabytes quedarán ocupados en la tarjeta gráfica hasta que el jugador cierre el ejecutable, acumulándose progresivamente en lo que se denomina una fuga de memoria (Memory Leak).
Para evitar este problema, siempre debemos implementar el evento `Clean Up` en cualquier objeto que cree superficies, estructuras de datos (`ds_grids`, `ds_lists`) o buffers.
/// @description Evento Clean Up de obj_pausa
// Liberamos la superficie de la VRAM si todavía existe en memoria
if (surface_exists(surf_pausa))
{
surface_free(surf_pausa);
surf_pausa = -1;
}
// Nos aseguramos de que no queden entidades congeladas accidentalmente
instance_activate_all();Ejemplo Práctico Integrado: Pausa de Audio Suave
En un videojuego comercial de plataformas, pausar la imagen de forma abruptly mientras la música de fondo sigue sonando a volumen completo genera una disonancia molesta. Una técnica refinada consiste en pausar todos los efectos de sonido secundarios mientras se reduce suavemente el volumen de la música ambiental o se aplica un amortiguador sonoro.
Peculiaridades y Volatilidad de las Superficies
En sistemas operativos de escritorio como Windows, macOS o Linux, las superficies de GameMaker son 'volátiles'. Esto significa que si el jugador minimiza la ventana, pulsa `Alt + Tab` para cambiar a otra aplicación o la pantalla se bloquea, el sistema operativo puede reclamar la memoria gráfica de la superficie, destruyéndola sin previo aviso.
Por esta razón técnica, el código que colocamos en el evento `Draw GUI` comprueba siempre `if (surface_exists(surf_pausa))` antes de intentar dibujarla. Si intentas ejecutar `draw_surface_stretched(surf_pausa, ...)` con una superficie que ha sido destruida por el sistema operativo, GameMaker arrojará un error fatal deteniendo el juego. Al evaluar su existencia y disponer de un bloque de contingencia con `draw_rectangle`, garantizamos que el jugador jamás experimente un cierre inesperado por haber minimizado la ventana durante una pausa.
Ejercicios Prácticos
Consolida las habilidades adquiridas implementando las siguientes ampliaciones en tu proyecto de plataformas retro:
1. **Submenú de Confirmación para Salir:** Modifica la lógica de la opción 'Menú Principal' para que, en lugar de cambiar de habitación de inmediato, despliegue un submenú con dos opciones: '¿Seguro? Sí / No'. Si el jugador elige 'No', el menú debe regresar a la lista de opciones original (Reanudar, Reiniciar, Salir).
2. **Efecto de Cursor Animado:** Sustituye las flechas de texto estáticas `"> " + opciones[i] + " <"` por un sprite o un elemento gráfico que oscile horizontalmente de forma armónica usando la función `sin(current_time * 0.005) * 8` para aportar dinamismo visual al menú flotante.
3. **Persistencia y Control de Habitaciones:** Configura `obj_pausa` para que no se ejecute si la habitación actual es el menú principal (`rm_menu`). Puedes lograr esto agregando al inicio del evento `Step`: `if (room == rm_menu) exit;`. Comprueba en el ejecutable que pulsar la tecla Escape dentro del menú principal no cause ningún efecto adverso ni intente pausar una pantalla que ya es un menú.
Criterios de Verificación
Verifica tu trabajo comprobando los siguientes aspectos:
• Al pulsar Escape en pleno salto del personaje, este se congela en el aire y ningún enemigo o moneda sigue animándose. • La pantalla no se vuelve negra ni sufre deformaciones geométricas; el fotograma previo permanece intacto con la cortina traslúcida. • La navegación con las flechas o teclas W/S responde de manera fluida sin saltarse posiciones. • Al presionar 'Reanudar', el jugador retoma el salto exactamente desde la posición y velocidad en la que se encontraba, sin desajustes físicos. • Tras pausar y reanudar repetidamente decenas de veces, el consumo de memoria en el Administrador de Tareas permanece constante sin incrementos desmedidos.
Resumen
- Pausar mediante desactivación de instancias (instance_deactivate_all) suspende el bucle físico y lógico de enemigos, partículas y jugadores con una sola instrucción.
- El parámetro 'notme' (verdadero) permite que el controlador obj_pausa continúe operando para recibir entradas y dibujar la interfaz.
- Capturar la application_surface en una superficie auxiliar preserva la imagen estática del nivel en el momento exacto del congelamiento.
- Las superficies residen en la memoria VRAM y son volátiles; siempre se debe comprobar surface_exists() y liberar la memoria con surface_free() en el evento Clean Up.
- Antes de cambiar de sala o reiniciar un nivel desde la pausa, es imprescindible reactivar las entidades con instance_activate_all() para evitar fugas de memoria y errores de persistencia.
Practica lo aprendido
Ejercicio 1 · 1 punto
¿Cuál es el propósito del argumento `true` (parámetro `notme`) al ejecutar la función `instance_deactivate_all(true)` desde el objeto `obj_pausa`?
Ejercicio 2 · 1 punto
Ordena cronológicamente las acciones que realiza el controlador `obj_pausa` al detectar la activación de la pausa en su evento Step:
- 1Obtener el ancho y alto del lienzo con surface_get_width y surface_get_height
- 2Crear la superficie temporal y clonar el contenido del juego con surface_copy
- 3Desactivar todas las demás entidades del mundo con instance_deactivate_all(true)
- 4Restablecer el cursor de selección asignando opcion_actual = 0
Ejercicio 3 · 1 punto
Al reanudar el juego o reiniciar el nivel desde la pausa, se deben reactivar las entidades mediante la función (). Además, para evitar fugas de memoria, se debe verificar si la superficie clonada aún existe con (surf_pausa) y liberarla de la memoria utilizando (surf_pausa).
Ingresa para guardar tu progreso — Ingresar