Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 10: Diseño de niveles, cámaras y progresión.
Puertas y transición entre múltiples rooms
Clase 48 de 60 · 60 min
También disponible en: EN
Introducción a la transición de niveles en GameMaker
En el desarrollo de videojuegos de plataformas 2D, la progresión espacial es el motor que mantiene el interés del jugador. Alcanzar el final de una sección, cruzar un umbral o abrir una compuerta debe sentirse como un logro claro y recompensado. En GameMaker, cada nivel, pantalla o menú se estructura dentro de un recurso denominado Room (sala). Por lo tanto, cambiar de nivel significa instruir al motor para que descargue los datos de la sala actual y cargue en memoria el contenido de la siguiente.
Aunque la acción de cambiar de sala puede resolverse con una única línea de código, hacerlo de manera descuidada genera problemas habituales: la pantalla se corta de forma abrupta, el personaje aparece en coordenadas incorrectas en el nuevo escenario o los datos acumulados (como monedas y vidas) se reinician involuntariamente. En esta lección diseñaremos una mecánica de cambio de nivel modular, elegante y reutilizable para todo nuestro proyecto, asegurando que el jugador pase de un escenario a otro mediante un elemento interactivo (puerta o meta) y una transición visual suave.
El ciclo de vida del cambio de Room
Cuando ejecutamos una orden de cambio de sala en GameMaker mediante GML Code, el motor realiza una serie de pasos críticos de limpieza y reconstrucción en el orden de ejecución de la escena:
1. Se activa el evento 'Other -> Room End' en todas las instancias activas que contengan este evento programado. 2. Todas las instancias que no estén marcadas como 'Persistent' (persistentes) son destruidas y liberadas de la memoria RAM. 3. Se descarga la estructura gráfica y las capas de tiles del room que abandonamos. 4. Se cargan los recursos visuales, capas de fondos y tilemaps del nuevo room. 5. Se crean las instancias definidas en las capas del nuevo room, ejecutando su respectivo evento 'Create'. 6. Se dispara el evento 'Other -> Room Start' en las instancias presentes.
💡 Es fundamental recordar que las variables comunes pertenecientes a una instancia (como 'x', 'y' o variables locales creadas en su evento Create) desaparecen cuando esa instancia es destruida al salir de la sala. Si queremos que un valor sobreviva al cambio de sala, debemos almacenarlo en una variable global o en un objeto gestor marcado como persistente.
Funciones esenciales de control de salas en GML
GameMaker ofrece un conjunto de funciones integradas para consultar y manipular el flujo de las salas. A continuación revisamos las más importantes que utilizaremos a lo largo de este módulo:
| Función GML | Descripción | Uso típico en plataformas |
|---|---|---|
| room_goto(room_id) | Cambia de forma inmediata a la sala especificada por su identificador. | Puertas que conducen a niveles específicos predeterminados. |
| room_goto_next() | Avanza a la sala que sigue inmediatamente en el 'Room Manager'. | Completar niveles en orden lineal estricto. |
| room_exists(room_id) | Devuelve 'true' si el identificador de sala existe en el proyecto. | Validación de seguridad antes de ejecutar una transición. |
| room_restart() | Reinicia la sala actual desde su estado original. | Reintentar el nivel al caer en un abismo o perder una vida. |
💡 Evita llamar directamente a 'room_goto_next()' sin comprobar si existe una sala posterior con 'room_next(room)'. Si intentas avanzar más allá del último nivel registrado en la lista de salas del proyecto, GameMaker arrojará un error fatal deteniendo el juego.
Diseño del objeto interactivo: obj_puerta
En lugar de crear un objeto puerta diferente para cada nivel ('obj_puerta_nivel1', 'obj_puerta_nivel2'), aplicaremos el principio de diseño modular: un único objeto 'obj_puerta' capaz de teletransportar al jugador a cualquier sala y a cualquier coordenada específica mediante variables configurables por instancia.
Comienza creando un sprite llamado 'spr_puerta'. Puede ser un marco de madera de 32x48 píxeles con una zona oscura en el interior, o un portal místico dibujado en el editor de sprites con dos o tres fotogramas para darle animación. Configura la máscara de colisión en modo rectángulo cerrado asegurando que cubra el área donde el jugador interactuará.
A continuación, crea el objeto 'obj_puerta', asígnale el sprite 'spr_puerta' y desmarca la casilla 'Solid' (queremos que el jugador pueda superponerse a la puerta sin chocar contra ella como si fuera un muro). En su evento Create, definiremos los valores por defecto:
// Recurso: obj_puerta
// Evento: Create
// Sala a la que enviará esta puerta (valor por defecto: ninguna sala específica)
destino_room = noone;
// Coordenadas donde aparecerá el jugador en la nueva sala
destino_x = 0;
destino_y = 0;
// Control para evitar activaciones múltiples en fotogramas sucesivos
activada = false;GameMaker permite sobreescribir estas variables en cada instancia individual colocada en el escenario. Al situar una copia de 'obj_puerta' en una habitación, puedes hacer doble clic en ella dentro del editor de salas, presionar 'Variables' y asignar allí el nombre de la sala destino (por ejemplo, 'rm_nivel_2') y las coordenadas exactas de llegada. También puedes hacerlo mediante código de creación de instancia en la propia sala.
Detección de activación: Colisión e interactividad
En los juegos clásicos de plataformas encontramos dos filosofías para cruzar una puerta: cruzarla automáticamente al tocarla (ideal para metas de fin de fase o salidas de tuberías) o presionar una tecla de interacción como la flecha hacia arriba o la tecla 'W' estando frente a ella. Implementaremos la segunda opción, ya que ofrece mayor control al jugador y evita accidentes si este retrocede cerca del umbral.
// Recurso: obj_puerta
// Evento: Step
// Si la puerta ya fue usada, no comprobamos nada más
if (activada) exit;
// Verificamos si el jugador está colisionando con la puerta
if (place_meeting(x, y, obj_jugador))
{
// Leemos si el jugador presiona la tecla de acción / flecha arriba
var tecla_entrar = keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W"));
if (tecla_entrar)
{
// Validamos que la sala de destino exista en el proyecto
if (destino_room != noone && room_exists(destino_room))
{
activada = true;
// Guardamos las coordenadas deseadas en variables globales
global.jugador_spawn_x = destino_x;
global.jugador_spawn_y = destino_y;
global.aplicar_spawn = true;
// Creamos el efecto de transición en lugar de cambiar de golpe
var trans = instance_create_depth(0, 0, -9999, obj_transicion_fade);
trans.sala_objetivo = destino_room;
}
else
{
show_debug_message("Aviso: La puerta en (" + string(x) + ", " + string(y) + ") no tiene una sala de destino válida asignada.");
}
}
}
Control del punto de aparición en la nueva Room
Un error muy común entre principiantes es asumir que el personaje mantendrá mágicamente su posición o que la sala sabrá automáticamente dónde ponerlo. Si tu objeto 'obj_jugador' no es persistente, cuando 'rm_nivel_2' se cargue, el jugador nacerá en la posición exacta donde fue colocado dentro del editor de esa sala.
Para resolver esto sin complicaciones de punteros de memoria, utilizamos el patrón de 'Variables Globales de Aparición' ('global.jugador_spawn_x' y 'global.jugador_spawn_y'). Inicializamos estas variables en el objeto de control del juego o en un script de arranque global, y luego instruimos al jugador para que lea dichas coordenadas al iniciar cualquier nivel nuevo.
En el evento 'Create' de nuestro objeto controlador global (por ejemplo, 'obj_game_manager'), definimos las variables:
// Recurso: obj_game_manager
// Evento: Create
global.jugador_spawn_x = -1;
global.jugador_spawn_y = -1;
global.aplicar_spawn = false;Luego, en el objeto 'obj_jugador', agregamos el evento 'Other -> Room Start' (Otros -> Inicio de Room). Este evento se dispara automáticamente cada vez que una nueva sala comienza a ejecutarse:
// Recurso: obj_jugador
// Evento: Other -> Room Start
// Verificamos si la puerta previa solicitó reposicionar al jugador
if (global.aplicar_spawn)
{
x = global.jugador_spawn_x;
y = global.jugador_spawn_y;
// Restablecemos el indicador para no alterar posiciones en reinicios normales
global.aplicar_spawn = false;
// Reiniciamos las velocidades acumuladas para evitar salir disparado
hspeed = 0;
vspeed = 0;
if (variable_instance_exists(id, "vel_x")) vel_x = 0;
if (variable_instance_exists(id, "vel_y")) vel_y = 0;
}Efecto visual de transición: Fundido a negro (Fade)
Pasar de una sala a otra instantáneamente causa una interrupción visual desagradable conocida como 'hard cut'. Una transición de fundido suave (Fade Out y Fade In) le da al juego un acabado profesional de consola retro. Para lograrlo, crearemos un objeto independiente: 'obj_transicion_fade'.
Este objeto debe marcarse como 'Persistent' en sus propiedades básicas. La persistencia es indispensable aquí: el objeto debe comenzar a oscurecer la pantalla en el nivel 1, mantenerse vivo durante el salto interno de 'room_goto', y luego aclarar la pantalla gradualmente ya dentro del nivel 2 antes de autodestruirse.
// Recurso: obj_transicion_fade
// Evento: Create
// Configuración visual
alpha = 0; // Nivel de opacidad (0 = transparente, 1 = negro absoluto)
velocidad_fade = 0.04; // Ritmo de cambio por fotograma
estado = "oscureciendo"; // Estados: "oscureciendo" o "aclarando"
sala_objetivo = noone; // Sala a la que debemos viajarEn el evento Step controlamos la lógica de la animación de opacidad y el momento exacto en el que se ejecuta el cambio de escenario:
// Recurso: obj_transicion_fade
// Evento: Step
if (estado == "oscureciendo")
{
alpha += velocidad_fade;
// Cuando la pantalla está completamente negra
if (alpha >= 1)
{
alpha = 1;
// Cambiamos de sala con la pantalla completamente cubierta
if (sala_objetivo != noone && room_exists(sala_objetivo))
{
room_goto(sala_objetivo);
}
// Cambiamos de fase: ahora toca aclarar la nueva sala
estado = "aclarando";
}
}
else if (estado == "aclarando")
{
alpha -= velocidad_fade;
// Cuando vuelve a ser totalmente transparente, la transición concluyó
if (alpha <= 0)
{
alpha = 0;
instance_destroy(); // Eliminamos el objeto para liberar memoria
}
}Para dibujar el rectángulo negro cubriendo toda la pantalla sin importar dónde esté ubicada la cámara del nivel, utilizamos el evento 'Draw GUI' (Dibujar Interfaz). El espacio de la GUI es independiente de las coordenadas del mundo del juego:
// Recurso: obj_transicion_fade
// Evento: Draw GUI
// Solo dibujamos si el efecto tiene alguna opacidad visible
if (alpha > 0)
{
// Obtenemos las dimensiones de la capa de interfaz actual
var ancho_gui = display_get_gui_width();
var alto_gui = display_get_gui_height();
// Fijamos color y transparencia
draw_set_color(c_black);
draw_set_alpha(alpha);
// Dibujamos el rectángulo que cubre toda la pantalla
draw_rectangle(0, 0, ancho_gui, alto_gui, false);
// BUENA PRÁCTICA: Restablecemos siempre los valores de dibujo por defecto
draw_set_alpha(1.0);
draw_set_color(c_white);
}Bloqueo de movimiento durante la transición
Un detalle de pulido fundamental es qué sucede con el jugador mientras la pantalla se está oscureciendo. Si no deshabilitamos el control, el jugador podría seguir caminando a ciegas, caer en un pozo o chocar contra un enemigo durante el segundo que dura el fade. Para evitarlo, podemos desactivar temporalmente el procesamiento de controles en el jugador cuando exista una transición activa.
En el evento 'Step' de 'obj_jugador', justo antes de procesar las pulsaciones del teclado para el movimiento horizontal y el salto, incluimos la siguiente salvaguarda:
// Recurso: obj_jugador
// Evento: Step (al inicio del evento)
// Si existe una transición en pantalla, congelamos el movimiento del jugador
if (instance_exists(obj_transicion_fade))
{
vel_x = 0; // Frenamos la inercia horizontal
exit; // Salimos del evento Step de inmediato; no leemos teclado
}Ejercicios prácticos de consolidación
Lleva a cabo las siguientes actividades prácticas para poner a prueba tu comprensión y afianzar la arquitectura de cambio de niveles en tu proyecto retro de plataformas:
Ejercicio 1: Puerta con cerrojo condicionado por coleccionables
Crea una variante de puerta o amplía 'obj_puerta' para que incluya una variable 'monedas_requeridas'. La puerta solo debe abrirse y activar la transición si la variable global de monedas recolectadas del jugador alcanza o supera dicha cifra. En caso contrario, el objeto debe mostrar un mensaje temporal o cambiar brevemente de subimagen para indicar que está sellada. Criterio de validación: Coloca en una sala de prueba una puerta configurada con 'monedas_requeridas = 5'. Comprueba que presionar la tecla de acción con 3 monedas no ejecute el cambio de nivel, y que al recolectar la quinta moneda la transición funcione con normalidad.
Ejercicio 2: Puertas bidireccionales entre dos estancias
Configura una mecánica de ida y vuelta. En 'rm_nivel_1', coloca una puerta que lleve al interior de una cueva o casa ('rm_cueva'). Dentro de 'rm_cueva', coloca otra puerta que permita regresar exactamente frente a la puerta exterior de 'rm_nivel_1'. Criterio de validación: El personaje debe poder cruzar de la superficie a la cueva y retornar a la superficie tantas veces como desee, apareciendo en cada ocasión con las velocidades en cero y ubicado justo enfrente de la puerta correspondiente sin quedarse atascado dentro de la geometría de las paredes o suelos.
Ejercicio 3: Transición visual personalizada y sonido ambiental
Modifica 'obj_transicion_fade' para que admita un parámetro 'color_fade' en su evento Create (por defecto 'c_black'). Añade la reproducción de un efecto de sonido de apertura de puerta de madera o resonancia mágica mediante 'audio_play_sound()' justo en el instante en que el jugador activa la transición. Criterio de validación: Prueba configurar una puerta que utilice 'c_white' para simular un destello de luz brillante al salir al exterior, verificando que el audio se escuche con claridad y que el color del rectángulo en Draw GUI respete el valor configurado.
Resumen
- Las Rooms en GameMaker representan niveles o escenarios completos cuyo cambio destruye por defecto las instancias no persistentes.
- La función 'room_goto(room_id)' permite saltar a cualquier sala específica, mientras que 'room_exists()' garantiza que el identificador sea válido para prevenir errores fatales.
- Un objeto 'obj_puerta' modular y personalizable mediante variables de instancia ('destino_room', 'destino_x', 'destino_y') evita la duplicación innecesaria de objetos en el proyecto.
- El reposicionamiento seguro del jugador se logra mediante variables globales leídas durante el evento 'Other -> Room Start'.
- El objeto 'obj_transicion_fade', marcado como persistente y dibujado en el evento 'Draw GUI', brinda un acabado visual profesional mediante fundido a negro y bloquea los controles durante la carga.
Practica lo aprendido
Ejercicio 1 · 1 punto
Ordena cronológicamente los pasos que ejecuta GameMaker durante el ciclo de vida del cambio de sala (Room):
- 1Se crean las instancias del nuevo room ejecutando su evento Create.
- 2Se destruyen y liberan de la RAM las instancias que no son persistentes.
- 3Se activa el evento 'Other -> Room End' en las instancias activas.
- 4Se descargan la estructura gráfica y las capas de tiles del room saliente.
- 5Se cargan los recursos visuales y capas del nuevo room.
- 6Se dispara el evento 'Other -> Room Start' en las instancias presentes.
Ejercicio 2 · 1 punto
¿Por qué es indispensable marcar como persistente ('Persistent') el objeto 'obj_transicion_fade' al implementar el efecto de fundido?
Ejercicio 3 · 1 punto
Al configurar el objeto 'obj_puerta' en su evento Create, definimos variables para modularizar su funcionamiento. La variable encargada de almacenar la sala a la que se viajará es , mientras que las coordenadas donde aparecerá el jugador se configuran por defecto en 0 mediante las variables y .
Ingresa para guardar tu progreso — Ingresar