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 GMLDescripciónUso 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.");
        }
    }
}
Diagrama esquemático que ilustra el flujo de cambio de nivel: el personaje entra a una puerta en el nivel 1, se ejecuta el fundido en negro y reaparece en las coordenadas de destino en el nivel 2.
Figura 1: Flujo de control al interactuar con una puerta y reubicar al jugador en la siguiente sala. · Imagen generada con IA

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 viajar

En 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):

  1. 1Se crean las instancias del nuevo room ejecutando su evento Create.
  2. 2Se destruyen y liberan de la RAM las instancias que no son persistentes.
  3. 3Se activa el evento 'Other -> Room End' en las instancias activas.
  4. 4Se descargan la estructura gráfica y las capas de tiles del room saliente.
  5. 5Se cargan los recursos visuales y capas del nuevo room.
  6. 6Se dispara el evento 'Other -> Room Start' en las instancias presentes.
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 2 · 1 punto

¿Por qué es indispensable marcar como persistente ('Persistent') el objeto 'obj_transicion_fade' al implementar el efecto de fundido?

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

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 .

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

Ingresa para guardar tu progreso — Ingresar