Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 10: Diseño de niveles, cámaras y progresión.
Gestión de datos persistentes entre niveles
Clase 49 de 60 · 55 min
También disponible en: EN
El reto de la persistencia: ¿Por qué desaparecen los datos al cambiar de nivel?
En las lecciones anteriores aprendiste a construir salas interactivas, colocar obstáculos y configurar portales o puertas que transportan al personaje de una habitación (`room`) a otra mediante la función `room_goto()`. Sin embargo, al probar esa transición por primera vez, surge un problema estructural clásico en el desarrollo de videojuegos: el jugador recoge monedas en el primer nivel, acumula 350 puntos, cruza la puerta hacia el nivel dos y, de repente, su marcador vuelve a marcar cero y sus vidas se restauran al valor inicial. Este fenómeno suele desconcertar a los desarrolladores principiantes, pero responde a una regla operativa fundamental de GameMaker: la limpieza de memoria entre salas.
Por defecto, cuando GameMaker ejecuta una transición de sala, destruye automáticamente todas las instancias presentes en la sala saliente para liberar la memoria RAM del dispositivo. Esto significa que tu objeto jugador (`obj_jugador`), los enemigos, los coleccionables y los paneles de interfaz desaparecen por completo. Cuando la nueva sala se carga, el motor lee el diseño que preparaste en el editor de salas y crea instancias completamente nuevas desde cero. Al crearse, cada objeto ejecuta nuevamente su evento Create, restableciendo cualquier variable interna a sus valores predeterminados de fábrica. Si definiste `puntos = 0;` y `vidas = 3;` en el Create de tu jugador, cada nivel nuevo reiniciará irremediablemente el progreso acumulado por el usuario.
Para construir una experiencia de juego cohesiva donde el jugador sienta que su esfuerzo tiene continuidad a lo largo de múltiples mundos y desafíos, necesitamos desacoplar los datos esenciales de la existencia física de las instancias temporales de cada nivel. Existen dos caminos principales en GameMaker para resolver este dilema: hacer que ciertos objetos sean persistentes o almacenar la información en variables de ámbito global. A lo largo de esta clase examinaremos ambas técnicas, centrándonos especialmente en las variables globales, que constituyen el estándar más limpio, predecible y fácil de depurar para principiantes y profesionales.
Ámbitos de variables en GameMaker: Locales, de instancia y globales
Para dominar la persistencia, es indispensable repasar con precisión quirúrgica cómo GameMaker clasifica y aísla la memoria a través de los ámbitos de variables (variable scopes). Una variable no es más que un contenedor con nombre que almacena un dato en la memoria; el ámbito determina quién tiene permiso para leer o modificar ese contenedor y durante cuánto tiempo existe en el ciclo de vida del juego.
| Tipo de variable | Declaración sintáctica | Alcance / Visibilidad | Tiempo de vida útil |
|---|---|---|---|
| Local (Temporal) | var mi_variable = 10; | Únicamente dentro del evento, función o script donde se declara. | Se destruye al instante en que el evento termina su ejecución en ese fotograma. |
| De Instancia | mi_variable = 10; | Accesible por la instancia propietaria y por otras mediante 'id.variable'. | Permanece viva mientras la instancia exista; se destruye al cambiar de sala o al morir. |
| Global | global.mi_variable = 10; | Accesible desde cualquier objeto, script, función o evento de todo el juego. | Vive indefinidamente durante toda la ejecución del juego hasta que se cierre. |
Las variables locales (precedidas por la palabra clave `var`) son efímeras y se utilizan para cálculos matemáticos rápidos dentro de un solo paso de simulación. Las variables de instancia pertenecen al cuerpo de un objeto concreto: si tienes tres monedas en pantalla, cada una tiene su propia variable de instancia `x` e `y`. Si una moneda desaparece, sus variables desaparecen con ella. En cambio, las variables globales no le pertenecen a ningún objeto físico del escenario; pertenecen directamente al núcleo central del juego. Esta independencia las convierte en la herramienta perfecta para almacenar la salud restante, el inventario, las monedas y el récord de puntuación.

Estrategias de inicialización de variables globales
Uno de los errores más frecuentes al trabajar con variables globales es definir su valor inicial dentro del evento Create de un objeto que se repite en cada sala, como el jugador o el marcador. Si escribes `global.puntuacion = 0;` en el Create de `obj_jugador`, cada vez que cargues un nuevo nivel y el jugador nazca en la nueva posición, esa línea sobreescribirá la puntuación acumulada devolviéndola a cero. Para evitar esta trampa, las variables globales deben inicializarse exactamente una sola vez durante el arranque del juego.
En las versiones modernas de GameMaker, disponemos de dos métodos seguros y profesionales para lograr una inicialización única y blindada: el uso de scripts de configuración global automática y el diseño de una sala de arranque dedicada (`rm_init`). Analizaremos ambos mecanismos para que elijas el que mejor se adapte a tu flujo de trabajo.
Método 1: Declaración en un Script de inicialización (Recomendado en GameMaker moderno)
Cualquier fragmento de código escrito en la raíz de un script de GML (fuera de cualquier declaración de función) se ejecuta automáticamente en el mismísimo primer microsegundo en que arranca el juego, incluso antes de que se dibuje la primera sala en pantalla. Esta característica permite establecer el estado inicial de toda la aventura en un archivo centralizado y ordenado.
Crea un nuevo recurso de tipo Script en el panel del explorador de recursos (Asset Browser) y asígnale el nombre `scr_game_init`. Dentro de él, escribe el siguiente bloque de código fundamental:
// Script: scr_game_init
// Este código se ejecuta una sola vez al arrancar el juego completo.
// Progresión y estado del jugador
global.vidas = 3;
global.vidas_maximas = 3;
global.puntuacion = 0;
global.monedas = 0;
// Registro de control de niveles
global.nivel_actual = 1;
global.juego_pausado = false;
// Registro para depuración en la consola de salida
show_debug_message("Sistema global inicializado con éxito: Vidas = " + string(global.vidas));Al declarar estas variables con el prefijo `global.`, GameMaker reserva el espacio de memoria de inmediato. A partir de ese momento, cualquier sala puede leer o reescribir `global.puntuacion` sin temor a que el valor vuelva a inicializarse inadvertidamente durante los cambios de escenario.
Método 2: Sala de precarga o inicialización (rm_init)
El segundo método, ampliamente utilizado en proyectos de mediana escala y con menús complejos, consiste en crear una sala vacía y ligera ubicada en la primera posición de la lista de salas en el Asset Browser, llamada convencionalmente `rm_init` o `rm_preload`. Esta sala solo contiene una instancia de un objeto gestor (`obj_game_manager`). En su evento Create, dicho objeto define las variables y, en la siguiente línea, salta inmediatamente al menú principal o al primer nivel mediante `room_goto(rm_nivel_1)`.
💡 Regla de oro: Sea cual sea el método que elijas, nunca declares el valor inicial de una variable global dentro del evento Create del objeto del jugador ni de ningún objeto que aparezca físicamente dentro de las salas de juego comunes.
El Objeto Controlador Central: Creación de obj_game_manager
Aunque las variables globales flotan de manera independiente en la memoria, es una buena práctica de arquitectura de software contar con un objeto coordinador que se encargue de supervisar la lógica global: detectar si el jugador se ha quedado sin vidas para enviarlo a la pantalla de Game Over, reproducir efectos de felicitación cuando se alcanza un nuevo récord o gestionar el paso ordenado entre fases. A este objeto lo llamaremos `obj_game_manager`.
Crea un nuevo objeto en el Asset Browser con el nombre `obj_game_manager`. No es necesario que le asignes ningún sprite, ya que actuará como una entidad lógica invisible en el mundo del juego. En su panel de propiedades, asegúrate de marcar la casilla **Persistent** (Persistente).
Cuando la casilla **Persistent** está activada en un objeto, GameMaker altera su ciclo de vida habitual: al cambiar de sala mediante `room_goto()`, la instancia de este objeto no se destruye; en su lugar, se traslada intacta a la nueva sala, conservando todas sus variables de instancia y continuando la ejecución de sus eventos sin interrupción.
// Objeto: obj_game_manager
// Evento: Create
// Verificamos si ya existe otro gestor para evitar duplicados indeseados
if (instance_number(obj_game_manager) > 1)
{
instance_destroy();
exit;
}
// El objeto no se destruirá al cambiar de sala gracias a la propiedad Persistent
persistent = true;
// Variable de control local del gestor
tiempo_en_nivel = 0;El condicional `if (instance_number(obj_game_manager) > 1)` es una salvaguarda conocida como patrón Singleton simple. Si por descuido dejaste colocado un `obj_game_manager` en el editor del Nivel 1 y otro en el Nivel 2, el objeto duplicado detectará que ya hay un gestor veterano viniendo de la sala anterior y se autodestruirá de inmediato, protegiendo al juego de conflictos lógicos.
Reescritura de los coleccionables: Sumando a la memoria global
En el Módulo 7 programamos la recolección de monedas modificando una variable local dentro del jugador (`puntos += 10;`). Ahora que disponemos de un sistema persistente robusto, debemos actualizar el comportamiento de nuestros coleccionables (`obj_moneda`) para que alteren directamente la bolsa de recursos global. De esta forma, el valor ganado existirá al margen de la sala donde se recogió.
Abre el objeto `obj_moneda`. Si estás gestionando la colisión dentro de la propia moneda frente al jugador, accede a su evento de colisión con `obj_jugador` y escribe el siguiente bloque de código depurado:
// Objeto: obj_moneda
// Evento: Collision con obj_jugador
// Incrementamos las variables globales directamente
global.monedas += 1;
global.puntuacion += 100;
// Comprobación de bonificación: 100 monedas otorgan una vida extra
if (global.monedas >= 100)
{
global.monedas -= 100;
global.vidas += 1;
// Reproducir sonido especial de vida extra (si está disponible en audio)
if (audio_exists(snd_vida_extra))
{
audio_play_sound(snd_vida_extra, 10, false);
}
}
// Reproducir sonido de moneda común
if (audio_exists(snd_moneda))
{
audio_play_sound(snd_moneda, 5, false);
}
// Destruir la moneda recolectada para que no se sume dos veces
instance_destroy();Fíjate en la lógica: al recolectar la moneda, no nos importa si el jugador cambia de nivel diez segundos después o salta por una puerta inmediatamente. Los 100 puntos y la moneda recolectada ya no residen en la memoria efímera de la sala, sino en las variables globales, listas para ser leídas por cualquier sala futura.
Integración del daño, vidas y reaparición (Respawn)
El siguiente componente crítico es la gestión del daño y la pérdida de vidas. Anteriormente, si el jugador caía al vacío o chocaba contra una trampa, podíamos reiniciar la sala con `room_restart()`. El problema radicaba en que, al reiniciar la sala, el jugador volvía a tener 3 vidas completas porque su evento Create se ejecutaba de nuevo. Al vincular la salud a `global.vidas`, el reinicio de sala solo reposiciona los elementos visuales, pero conserva el castigo recibido.
Modifiquemos el evento o script donde el jugador recibe daño fatal (por ejemplo, en el evento Collision de `obj_jugador` con `obj_trampa_pinchos` o con `obj_enemigo`):
// Objeto: obj_jugador
// Evento: Collision con obj_peligro (o al caer en el vacío)
// Restamos una vida al almacén global
global.vidas -= 1;
// Comprobamos si el jugador aún tiene vidas para seguir intentándolo
if (global.vidas > 0)
{
// Reproducir sonido de herida o caída
if (audio_exists(snd_golpe))
{
audio_play_sound(snd_golpe, 8, false);
}
// Reiniciar la sala actual para volver al inicio del nivel
room_restart();
}
else
{
// El jugador ha agotado todas sus oportunidades: Fin de la partida
if (audio_exists(snd_game_over))
{
audio_play_sound(snd_game_over, 10, false);
}
// Transportar a la pantalla de Game Over
room_goto(rm_gameover);
}Observa con atención lo que ocurre aquí: cuando `global.vidas > 0` y GameMaker ejecuta `room_restart()`, la sala vuelve a cargarse exactamente como estaba al entrar, reconstruyendo las cajas y los enemigos. Sin embargo, como `global.vidas` vive fuera del ámbito de la sala, el jugador reaparece en su punto de partida conservando la vida que le queda (por ejemplo, 2 vidas en lugar de 3). La persistencia funciona en ambas direcciones: protege las recompensas y mantiene vigentes las sanciones.
Actualización del HUD: Visualización sincronizada
En el Módulo 8 diseñamos el objeto de interfaz gráfica `obj_hud` utilizando el evento Draw GUI para dibujar los corazones y los textos en pantalla. En aquella ocasión, el HUD leía los datos directamente de la instancia del jugador usando expresiones como `obj_jugador.puntos`. Esto generaba dos graves debilidades: si el jugador moría o se destruía durante una animación de transición, el HUD lanzaba un error de compilación por intentar leer una variable de un objeto inexistente (`Unable to find instance...`). Además, forzaba al HUD a estar acoplado permanentemente a la existencia del personaje.
Al migrar toda la telemetría a variables globales, el HUD se vuelve completamente independiente. Puede dibujarse perfectamente incluso en salas donde el jugador aún no ha nacido o durante una cinemática de victoria. Abre `obj_hud` y localiza su evento **Draw GUI** para vincularlo a las variables globales:
// Objeto: obj_hud
// Evento: Draw GUI
// Configuración visual del texto
draw_set_font(fnt_interfaz);
draw_set_color(c_white);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
// Margen en píxeles relativo a la esquina superior izquierda de la pantalla
var margen_x = 32;
var margen_y = 24;
// Dibujo de las métricas globales
draw_text(margen_x, margen_y, "PUNTOS: " + string(global.puntuacion));
draw_text(margen_x, margen_y + 28, "MONEDAS: " + string(global.monedas));
// Dibujo de vidas mediante texto simple o iconos representativos
draw_text(margen_x, margen_y + 56, "VIDAS: ");
for (var i = 0; i < global.vidas; i++)
{
// Dibujamos un sprite de corazón por cada vida activa
draw_sprite(spr_icono_corazon, 0, margen_x + 90 + (i * 32), margen_y + 60);
}
// Restaurar la alineación por defecto para no afectar otros dibujos
draw_set_halign(fa_left);
draw_set_valign(fa_top);Gracias a este cambio estructural, no importa en qué sala se encuentre la cámara ni si el jugador está cambiando de nivel a través de un fundido en negro: el HUD siempre tendrá acceso inmediato a la información viva del juego sin riesgo de errores de puntero nulo o valores desincronizados.
Objetos persistentes frente a Variables globales: Criterios de diseño
Al principio de esta lección mencionamos que los objetos también pueden marcarse como persistentes marcando la casilla **Persistent** en sus propiedades. Muchos desarrolladores novatos caen en la tentación de marcar a `obj_jugador` como persistente para que no pierda sus datos al cambiar de sala. Aunque parece una solución fácil, en juegos de plataformas 2D suele desencadenar serios problemas de arquitectura si no se gestiona con extremo cuidado:
| Enfoque | Ventajas principales | Riesgos y desventajas frecuentes |
|---|---|---|
| Variables Globales + Controlador | Datos 100% aislados y seguros. Los niveles pueden diseñarse libremente en el editor. Fácil de depurar y guardar en disco. | Requiere escribir 'global.' antes de cada variable o estructurarlas en structs limpios. |
| Jugador Persistente (obj_jugador) | Conserva automáticamente todas las variables internas sin necesidad de adaptarlas a ámbito global. | El personaje aparece en la nueva sala en las coordenadas (x,y) de la sala anterior, pudiendo quedar atrapado dentro de una pared o en el aire. |
Si tu jugador es persistente, cuando salta de `rm_nivel_1` (concluyendo en la posición x: 1800, y: 300) y aterriza en `rm_nivel_2`, aparecerá exactamente en `x = 1800, y = 300`, independientemente de dónde hayas colocado la entrada del nuevo nivel. Resolver esto requiere reposicionar manualmente al personaje mediante código en cada entrada de nivel. Por esa razón, el estándar de la industria para juegos de plataformas clásicos es mantener las salas y al jugador no persistentes (para colocarlos manualmente con precisión en el editor de niveles de GameMaker) y centralizar los datos en variables globales o en un único `obj_game_manager` persistente invisible.
Paso a paso: Flujo de trabajo completo de cambio de nivel con persistencia
A continuación, unificaremos todos los componentes estudiados en un flujo de trabajo práctico. Verás cómo interactúan la puerta de salida, el controlador global y la llegada al nuevo escenario asegurando que ningún dato se extravíe por el camino.
Reinicio completo del juego tras Game Over o Victoria
Dado que las variables globales viven durante toda la ejecución de la aplicación, existe una situación final donde sí queremos borrarlas deliberadamente: cuando el jugador pierde todas sus vidas y regresa al menú de inicio, o cuando decide pulsar el botón de "Reintentar partida". Si no reiniciamos los valores globales en ese momento, una nueva partida comenzaría con cero vidas o con el puntaje residual acumulado de la derrota anterior.
Para solventar esto de manera limpia, crearemos una función en un script accesible por los botones del menú de inicio o la pantalla de Game Over. Añade esta función al final de tu archivo `scr_game_init`:
// Script: scr_game_init (Añadir al final del archivo)
/// @function reiniciar_partida()
/// @description Restaura todas las variables globales a su estado de juego nuevo.
function reiniciar_partida()
{
global.vidas = 3;
global.puntuacion = 0;
global.monedas = 0;
global.nivel_actual = 1;
// Tras limpiar la memoria, viajamos al primer nivel jugable
room_goto(rm_nivel_1);
}Ahora, en el botón de "Reintentar" o "Jugar de nuevo" de tu menú de fin de partida (diseñado en el Módulo 8), simplemente llamas a `reiniciar_partida();` en el evento correspondiente de pulsación de tecla o clic del ratón. El ciclo de vida de los datos queda así perfectamente cerrado y controlado.
Ejercicios prácticos
Pon a prueba tus conocimientos sobre gestión de persistencia resolviendo los siguientes tres ejercicios prácticos sobre tu proyecto de plataformas retro.
Resumen
- Por defecto, GameMaker destruye las instancias y sus variables al cambiar de sala mediante room_goto(), limpiando la memoria del nivel anterior.
- Las variables de instancia solo existen mientras su objeto vive; las variables globales (con prefijo 'global.') residen en la memoria central durante toda la ejecución del juego.
- Las variables globales deben inicializarse exactamente una sola vez, preferentemente mediante un Script general (scr_game_init) o dentro de una sala de precarga dedicada (rm_init).
- Los objetos marcados como 'Persistent' viajan de una sala a otra sin destruirse, pero pueden acarrear complicaciones posicionales si se aplican directamente al jugador.
- Desacoplar el HUD y la puntuación de las instancias físicas previene errores de ejecución y garantiza una progresión fluida a través de múltiples niveles.
Practica lo aprendido
Ejercicio 1 · 1 punto
Relaciona cada tipo de ámbito de variable en GameMaker con su comportamiento y ciclo de vida según la lección:
- Variables locales
- Variables de instancia
- Variables globales
Ejercicio 2 · 1 punto
¿Por qué constituye un error inicializar una variable como `global.puntuacion = 0;` dentro del evento Create del objeto del jugador (`obj_jugador`)?
Ejercicio 3 · 1 punto
Completa el código del evento Create de `obj_game_manager` para evitar instancias duplicadas y habilitar su continuidad entre salas: ```gml if (instance_number(obj_game_manager) > ) { instance_destroy(); exit; } persistent = ; ```
Ingresa para guardar tu progreso — Ingresar