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

Pantallas de victoria y derrota (Game Over)

Clase 40 de 60 · 50 min

También disponible en: EN

Introducción: Cerrando el bucle principal del juego

Todo videojuego cuenta con un ciclo de retroalimentación fundamental: el jugador interactúa con las mecánicas, supera desafíos o comete errores y, finalmente, alcanza un desenlace. Hasta este punto del curso, nuestro proyecto cuenta con un personaje capaz de desplazarse, saltar, colisionar con enemigos, recibir daño, perder vidas y recolectar objetos, además de contar con un HUD informativo y un menú principal. Sin embargo, cuando la salud o las vidas llegan a cero, o cuando se alcanza el final del recorrido, el juego no ofrece una conclusión formal.

Las pantallas de fin de partida cumplen dos funciones esenciales: proporcionan una conclusión narrativa y psicológica inmediata al esfuerzo del jugador, y gestionan técnicamente la transición del estado de la aplicación. En esta lección aprenderás a estructurar de forma limpia y profesional las pantallas de derrota (*Game Over*) y de victoria (*Victory*), utilizando salas dedicadas (*rooms*), eventos de dibujo de interfaz (`Draw GUI`), controladores de teclado y un reinicio riguroso de las variables globales para garantizar que cada partida comience en un estado predecible y libre de errores.

Diagrama de flujo esquemático que muestra la transición desde la sala de juego hacia la pantalla de Game Over o la pantalla de Victoria, y el retorno al nivel o al menú principal.
Figura 1: Arquitectura de transición entre el nivel de juego y las pantallas de fin de partida. · Imagen generada con IA

Estructura de salas y recursos necesarios

Para implementar estas pantallas mantendremos una separación arquitectónica estricta. En lugar de superponer interfaces complejas dentro del mismo nivel, utilizaremos salas independientes. Esto garantiza que todos los enemigos, proyectiles, partículas y cálculos físicos del nivel se descarguen de la memoria RAM automáticamente al salir de la sala de juego.

En el *Asset Browser*, crearemos los siguientes recursos dentro de sus respectivas carpetas:

Nombre del RecursoTipo de RecursoPropósito en el Proyecto
rm_gameoverRoomSala que aloja la interfaz visual y lógica tras agotar todas las vidas.
rm_victoryRoomSala a la que se transporta al jugador al alcanzar la meta final.
spr_goalSpriteIcono o bandera de 32x32 píxeles que delimita el final del nivel.
obj_goalObjectObjeto interactivo que dispara el evento de victoria al colisionar con el jugador.
obj_gameover_controllerObjectControlador sin sprite que gestiona la pantalla y el menú de derrota.
obj_victory_controllerObjectControlador sin sprite que gestiona la pantalla y el menú de victoria.

💡 Recuerda que en GameMaker las salas se configuran con un tamaño de vista predeterminado. Para asegurar consistencia visual, asigna a 'rm_gameover' y 'rm_victory' las mismas dimensiones base que definiste para la resolución de pantalla en las lecciones anteriores (por ejemplo, 640x360 o 1280x720).

Condición de derrota: Redirección hacia Game Over

En el Módulo 7 implementamos el sistema de vidas mediante la variable global `global.player_lives`. Cuando el jugador entra en contacto letal con un enemigo o cae en un precipicio, pierde una vida. Si aún le quedan vidas, el personaje reaparece en el punto de control (*checkpoint*). No obstante, cuando las vidas caen a cero o menos, debe activarse la secuencia de derrota definitiva.

Para evitar un salto de pantalla instantáneo y abrupto que desoriente al jugador, es una buena práctica de diseño introducir una breve pausa o retardo temporal utilizando una alarma (*Alarm*). Esto permite que la animación de muerte o el sonido de derrota se reproduzcan antes de abandonar el nivel.

Abramos el objeto del jugador, `obj_player`, y revisemos la función o fragmento de código donde se procesa la pérdida de vida dentro del evento **Step** o en el método de daño personalizado:

// Fragmento dentro de obj_player -> Evento Step o función de daño
if (hp <= 0 && !is_dead)
{
    is_dead = true;
    global.player_lives -= 1;
    
    // Detenemos el movimiento horizontal y vertical
    hsp = 0;
    vsp = 0;
    
    // Verificamos si aún quedan vidas disponibles
    if (global.player_lives > 0)
    {
        // Si quedan vidas, disparamos la alarma de reaparición (1 segundo = 60 steps)
        alarm[0] = 60;
    }
    else
    {
        // Si se agotaron las vidas, disparamos la alarma hacia la pantalla de Game Over
        alarm[1] = 90;
    }
}

A continuación, añadimos el evento **Alarm 1** en `obj_player` con la instrucción encargada de efectuar el cambio de sala:

/// Evento: Alarm 1 en obj_player
// Cambiamos de forma segura a la sala de derrota
room_goto(rm_gameover);

Condición de victoria: El objeto meta

Para la condición de victoria crearemos un objeto de llegada, `obj_goal`. Este objeto representará un poste de meta, un cofre o una puerta que, al hacer contacto con la caja de colisión del jugador, desencadena la victoria.

En el evento **Create** de `obj_goal`, inicializamos una bandera booleana para evitar que la colisión se dispare múltiples veces en fotogramas sucesivos:

/// Evento: Create en obj_goal
triggered = false;

En el evento **Collision** de `obj_goal` con `obj_player`, gestionamos la victoria desactivando el control del personaje e iniciando una pequeña espera antes del cambio de sala:

/// Evento: Collision con obj_player en obj_goal
if (!triggered)
{
    triggered = true;
    
    // Congelamos al jugador para que no siga moviéndose
    other.can_move = false;
    other.hsp = 0;
    other.vsp = 0;
    
    // Iniciamos la alarma para realizar la transición
    alarm[0] = 60;
}

Y en el evento **Alarm 0** de `obj_goal`, ejecutamos la transición:

/// Evento: Alarm 0 en obj_goal
room_goto(rm_victory);

Construcción del controlador de derrota (obj_gameover_controller)

Una vez que el jugador llega a `rm_gameover`, necesitamos un objeto que presente la información en pantalla y escuche las órdenes del usuario (reintentar la partida o volver al menú inicial). Colocaremos una única instancia de `obj_gameover_controller` en la sala `rm_gameover`.

En su evento **Create**, definimos las opciones del menú de derrota y una variable de índice para navegar entre ellas:

/// Evento: Create en obj_gameover_controller
menu_options = ["REINTENTAR", "MENU PRINCIPAL"];
menu_index = 0;
menu_total = array_length(menu_options);

// Bloqueo de entrada para evitar lecturas indeseadas inmediatas
input_cooldown = 10;

En el evento **Step**, gestionamos la navegación por teclado (flechas arriba/abajo o W/S) y la confirmación mediante la tecla Enter o Espacio:

/// Evento: Step en obj_gameover_controller
if (input_cooldown > 0)
{
    input_cooldown -= 1;
    exit;
}

var key_up = keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W"));
var key_down = keyboard_check_pressed(vk_down) || keyboard_check_pressed(ord("S"));
var key_select = keyboard_check_pressed(vk_enter) || keyboard_check_pressed(vk_space);

if (key_up)
{
    menu_index -= 1;
    if (menu_index < 0) menu_index = menu_total - 1;
}

if (key_down)
{
    menu_index += 1;
    if (menu_index >= menu_total) menu_index = 0;
}

if (key_select)
{
    switch (menu_index)
    {
        case 0:
            // Reintentar: Reiniciar estadísticas globales y cargar el primer nivel
            scr_restart_game_state();
            room_goto(rm_level_1);
            break;
            
        case 1:
            // Menú principal: Reiniciar estadísticas y volver a la sala de título
            scr_restart_game_state();
            room_goto(rm_menu);
            break;
    }
}

Para pintar el mensaje en pantalla, recurrimos al evento **Draw GUI**, garantizando que el texto se dibuje con independencia de cámaras o desplazamientos:

/// Evento: Draw GUI en obj_gameover_controller
var gui_w = display_get_gui_width();
var gui_h = display_get_gui_height();

// Fondo oscuro semitransparente sobre la sala
draw_set_color(c_black);
draw_set_alpha(0.85);
draw_rectangle(0, 0, gui_w, gui_h, false);
draw_set_alpha(1.0);

// Dibujamos el título principal
draw_set_halign(fa_center);
draw_set_valign(fa_middle);

draw_set_color(c_red);
draw_text_transformed(gui_w * 0.5, gui_h * 0.3, "GAME OVER", 2.0, 2.0, 0);

// Dibujamos las opciones de navegación
for (var i = 0; i < menu_total; i++)
{
    var opt_y = (gui_h * 0.55) + (i * 40);
    
    if (i == menu_index)
    {
        draw_set_color(c_yellow);
        draw_text(gui_w * 0.5, opt_y, "> " + menu_options[i] + " <");
    }
    else
    {
        draw_set_color(c_white);
        draw_text(gui_w * 0.5, opt_y, menu_options[i]);
    }
}

// Restauramos los alineamientos estándar de dibujo
draw_set_halign(fa_left);
draw_set_valign(fa_top);

Construcción del controlador de victoria (obj_victory_controller)

La pantalla de victoria requiere una estructura similar a la de derrota, pero con un enfoque celebratorio. Es aquí donde solemos premiar al jugador mostrando su puntuación final (`global.player_score`) acumulada a lo largo del nivel.

Creamos el objeto `obj_victory_controller` y configuramos su evento **Create**:

/// Evento: Create en obj_victory_controller
menu_options = ["JUGAR DE NUEVO", "MENU PRINCIPAL"];
menu_index = 0;
menu_total = array_length(menu_options);
input_cooldown = 10;

El evento **Step** replica la lectura del teclado para permitir decidir entre rejugar o regresar al menú principal:

/// Evento: Step en obj_victory_controller
if (input_cooldown > 0)
{
    input_cooldown -= 1;
    exit;
}

var key_up = keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W"));
var key_down = keyboard_check_pressed(vk_down) || keyboard_check_pressed(ord("S"));
var key_select = keyboard_check_pressed(vk_enter) || keyboard_check_pressed(vk_space);

if (key_up)
{
    menu_index -= 1;
    if (menu_index < 0) menu_index = menu_total - 1;
}

if (key_down)
{
    menu_index += 1;
    if (menu_index >= menu_total) menu_index = 0;
}

if (key_select)
{
    switch (menu_index)
    {
        case 0:
            scr_restart_game_state();
            room_goto(rm_level_1);
            break;
            
        case 1:
            scr_restart_game_state();
            room_goto(rm_menu);
            break;
    }
}

En el evento **Draw GUI**, agregamos el despliegue del puntaje obtenido:

/// Evento: Draw GUI en obj_victory_controller
var gui_w = display_get_gui_width();
var gui_h = display_get_gui_height();

// Fondo oscuro decorativo
draw_set_color(make_color_rgb(20, 25, 40));
draw_set_alpha(0.9);
draw_rectangle(0, 0, gui_w, gui_h, false);
draw_set_alpha(1.0);

draw_set_halign(fa_center);
draw_set_valign(fa_middle);

// Mensaje de felicitación
draw_set_color(c_yellow);
draw_text_transformed(gui_w * 0.5, gui_h * 0.25, "¡NIVEL COMPLETADO!", 2.0, 2.0, 0);

// Mostrar puntuación acumulada
draw_set_color(c_white);
draw_text(gui_w * 0.5, gui_h * 0.4, "Puntuacion Final: " + string(global.player_score));

// Opciones interactivas
for (var i = 0; i < menu_total; i++)
{
    var opt_y = (gui_h * 0.6) + (i * 40);
    
    if (i == menu_index)
    {
        draw_set_color(c_lime);
        draw_text(gui_w * 0.5, opt_y, "> " + menu_options[i] + " <");
    }
    else
    {
        draw_set_color(c_gray);
        draw_text(gui_w * 0.5, opt_y, menu_options[i]);
    }
}

draw_set_halign(fa_left);
draw_set_valign(fa_top);

Reinicio riguroso del estado del juego con Scripts

Uno de los errores más comunes cometidos por principiantes consiste en llamar directamente a `room_goto(rm_level_1)` o `game_restart()` sin restablecer deliberadamente las variables globales. Si el jugador perdió porque sus vidas llegaron a cero y vuelve al nivel con `global.player_lives = 0`, morirá instantáneamente en el primer fotograma.

Para evitar código duplicado entre los controladores de derrota y victoria, crearemos un script reutilizable llamado `scr_game_flow` con la función `scr_restart_game_state()`:

/// Script: scr_game_flow
function scr_restart_game_state()
{
    // Restablecer variables globales de progresión y estadísticas
    global.player_lives = 3;
    global.player_score = 0;
    global.coins_collected = 0;
    
    // Restablecer posición de checkpoint si existiese
    global.checkpoint_x = -1;
    global.checkpoint_y = -1;
}

💡 Evita utilizar la función nativa 'game_restart()' para reintentar un nivel común. 'game_restart()' recarga el juego entero desde la primera sala, reiniciando configuraciones de audio y pantalla completa, lo cual genera un parpadeo innecesario en la ventana de la aplicación.

Ejemplo práctico: Flujo completo de fin de nivel

Ejercicios prácticos

Pon en práctica los conceptos adquiridos realizando los siguientes tres ejercicios diseñados para consolidar tu dominio sobre las transiciones de fin de partida:

1. **Efecto de fundido a negro (Fade Out):** En lugar de cambiar instantáneamente con `room_goto()`, crea un objeto `obj_fade` con una variable de opacidad `alpha = 0` que se incremente en cada paso (`alpha += 0.05`). Cuando `alpha >= 1`, ejecuta la llamada a la sala de destino. Comprueba que la pantalla se oscurece con suavidad antes de mostrar la interfaz de Game Over.

2. **Registro de récord de puntuación (High Score):** Añade una variable global `global.high_score = 0;` en el script de inicio. En `obj_victory_controller`, antes de dibujar el puntaje, verifica si `global.player_score > global.high_score`. Si es así, actualiza `global.high_score` con el nuevo valor y dibuja un mensaje parpadeante de '¡NUEVO RECORD!' en color dorado.

3. **Penalización por tiempo restante:** Implementa en el nivel un temporizador que descuente un segundo cada 60 pasos. Si el temporizador llega a cero antes de tocar `obj_goal`, haz que el jugador pierda una vida por límite de tiempo y, si era su última vida, sea transportado a `rm_gameover` indicando como causa 'TIEMPO AGOTADO'.

Resumen

  • Las condiciones de victoria y derrota cierran formalmente el bucle de juego y proveen retroalimentación clara al usuario.
  • Separar la jugabilidad y las pantallas finales en diferentes salas (rm_gameover y rm_victory) simplifica la gestión de memoria y la limpieza de instancias.
  • El uso de alarmas antes de la transición permite que las animaciones de derrota o victoria se reproduzcan sin cortes abruptos.
  • El evento Draw GUI es el canal idóneo para renderizar textos, puntajes y opciones de menú centrados en pantalla sin depender de la posición de la cámara.
  • Un script centralizado de reinicio (scr_restart_game_state) previene bugs críticos, asegurando que vidas y puntos vuelvan a sus valores iniciales al reiniciar.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Cuál es la principal ventaja técnica de utilizar salas (rooms) independientes para las pantallas de victoria y Game Over en lugar de superponer interfaces en el mismo nivel?

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

Ejercicio 2 · 1 punto

Ordena los pasos que ocurren en el objeto de llegada (obj_goal) desde que hace contacto con el jugador hasta que se efectúa la transición a la pantalla de victoria:

  1. 1Se comprueba que la bandera triggered sea falsa y se establece inmediatamente en true.
  2. 2Se congela al jugador modificando sus variables de movimiento y can_move.
  3. 3Se ejecuta el evento Alarm 0 llamando a room_goto(rm_victory).
  4. 4Se inicia la cuenta regresiva de la transición asignando alarm[0] = 60.
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 3 · 2 puntos

Completa el código del evento Alarm 1 en obj_player para realizar la transición de pantalla hacia la sala de derrota rm_gameover mediante la función correspondiente de GameMaker.

Tu código no se ejecuta: se comprueban requisitos concretos sobre el texto.

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

Ingresa para guardar tu progreso — Ingresar