Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 11: Desarrollo completo de un videojuego de plataformas 2D.

Integración completa de audio, HUD y lógica de juego

Clase 54 de 60 · 55 min

También disponible en: EN

Introducción: La orquestación de los subsistemas del videojuego

A lo largo de los módulos anteriores, has desarrollado componentes esenciales de forma aislada: el movimiento y las colisiones del jugador, el comportamiento de patrulla y ataque de los enemigos, el dibujo de estadísticas en la interfaz de usuario (HUD) mediante el evento Draw GUI, y la reproducción de pistas musicales y efectos de sonido (SFX). Aunque cada una de estas piezas puede funcionar correctamente por separado dentro de una sala de pruebas, el verdadero reto del desarrollo de videojuegos radica en lograr que todos estos subsistemas interactúen entre sí de manera coherente, fluida y robusta.

Un videojuego no se percibe como una experiencia acabada si al recibir daño la barra de vida no reacciona al instante, si el efecto sonoro de impacto se reproduce con retraso, o si la música continúa sonando con el mismo volumen e intensidad tras la derrota del personaje. En esta lección aprenderás a integrar y sincronizar la lógica de juego, la interfaz gráfica y el audio en una sola estructura unificada. Verificaremos que el flujo de información viaje sin fisuras desde los eventos físicos y de control hasta las salidas audiovisuales que percibe el usuario final.

Diagrama esquemático que muestra el flujo de datos entre la lógica del juego, el gestor de audio y la interfaz gráfica HUD a través del Game Manager.
Figura 1: Arquitectura de integración. La lógica central emite eventos y actualiza variables que el HUD y el sistema de audio interpretan en tiempo real. · Imagen generada con IA

Arquitectura de comunicación: Centralización frente a dispersión

Uno de los errores más frecuentes entre los desarrolladores principiantes es dispersar la lógica de presentación a lo largo de decenas de objetos. Por ejemplo, colocar código de dibujo de vida dentro del propio jugador (`obj_player`), reproducir la música de fondo directamente desde el evento Create de cada moneda (`obj_coin`), o reiniciar la sala (`room_restart()`) desde múltiples colisiones sin orden aparente. Este patrón, conocido coloquialmente como 'código espagueti', provoca comportamientos impredecibles: sonidos que se solapan hasta distorsionar el mezclador, elementos del HUD que se duplican al cambiar de sala y variables que no se sincronizan tras reiniciar una partida.

Para evitar este desorden, utilizaremos un patrón de arquitectura centralizado donde cada elemento asume una responsabilidad única y bien delimitada:

SubsistemaObjeto / Recurso principalResponsabilidad claveLo que NUNCA debe hacer
Lógica de Juegoobj_player, obj_enemy, obj_coinCalcular movimiento, detectar colisiones físicas, modificar estados y notificar cambios numéricos.Dibujar interfaces complejas en pantalla o gestionar la reproducción de música en bucle persistente.
Interfaz (HUD)obj_hudLeer variables del estado de la partida y representarlas gráficamente usando Draw GUI.Modificar variables de salud, aplicar daño o alterar la velocidad del jugador.
Audioobj_audio_manager o scripts de sonidoCargar pistas, controlar prioridades, modular volumen y gestionar la continuidad musical entre rooms.Tomar decisiones sobre si el jugador debe morir o pasar de nivel.
Controlador Centralobj_game_managerAlmacenar el estado persistente global (vidas, puntos, checkpoint activo, estado de pausa).Gestionar el código de físicas frame a frame del salto o la fricción.

💡 Regla de oro de la integración: Los objetos del mundo (como monedas o pinchos) solo informan qué ha ocurrido. El gestor global actualiza los datos, y los objetos especializados de salida (HUD y Audio) traducen ese nuevo estado en imágenes y sonidos.

Configuración del controlador central: `obj_game_manager`

El objeto `obj_game_manager` debe ser marcado como **Persistent** (persistente) en el inspector de propiedades de GameMaker. Esto garantiza que una sola instancia del controlador sobreviva al paso entre niveles, pantallas de menú y salas de transición. A continuación, revisamos su inicialización completa.

// ====================================================================
// OBJETO: obj_game_manager
// EVENTO: Create
// PROPÓSITO: Inicializar el estado global persistente del juego
// ====================================================================

// Asegurar que no existan duplicados si se vuelve a instanciar
if (instance_number(obj_game_manager) > 1) {
    instance_destroy();
    exit;
}

// Variables de estado del jugador y partida
global.player_max_hp = 3;
global.player_hp = global.player_max_hp;
global.player_lives = 3;
global.player_score = 0;
global.coins_collected = 0;
global.target_coins_for_life = 50;

// Variables de flujo y transición
global.game_paused = false;
global.level_timer = 0;
global.current_checkpoint_x = -1;
global.current_checkpoint_y = -1;

// Identificador de la música actual para evitar reinicios innecesarios
global.current_bgm = noone;

// Configuración del tamaño virtual de la interfaz GUI (1920x1080 estándar)
display_set_gui_size(1920, 1080);

Con el estado centralizado en variables globales accesibles pero modificadas mediante reglas claras, podemos gestionar el inicio de cada sala usando el evento **Room Start**. Este evento se encarga de reubicar al jugador si existe un punto de control y de asegurar que la música de fondo adecuada comience a reproducirse sin reinicios molestos si la pista de la sala entrante es idéntica a la anterior.

// ====================================================================
// OBJETO: obj_game_manager
// EVENTO: Room Start
// PROPÓSITO: Sincronizar posición, música y persistencia al cargar un nivel
// ====================================================================

// 1. Reposicionamiento del jugador según checkpoint activo
if (instance_exists(obj_player)) {
    if (global.current_checkpoint_x != -1 && global.current_checkpoint_y != -1) {
        obj_player.x = global.current_checkpoint_x;
        obj_player.y = global.current_checkpoint_y;
    }
}

// 2. Comprobación y reproducción de música de fondo por sala
var _desired_bgm = noone;

switch (room) {
    case rm_title:
        _desired_bgm = snd_bgm_title;
        break;
    case rm_level_1:
    case rm_level_2:
        _desired_bgm = snd_bgm_level;
        break;
    case rm_victory:
        _desired_bgm = snd_bgm_victory;
        break;
    case rm_gameover:
        _desired_bgm = snd_bgm_gameover;
        break;
}

// Solo cambiamos la pista musical si es distinta a la que ya está sonando
if (_desired_bgm != noone && global.current_bgm != _desired_bgm) {
    // Detener música previa si existe
    if (global.current_bgm != noone) {
        audio_stop_sound(global.current_bgm);
    }
    
    // Reproducir la nueva pista en bucle con prioridad media
    audio_play_sound(_desired_bgm, 100, true);
    global.current_bgm = _desired_bgm;
}

Integración del subsistema de audio con las acciones mecánicas

El audio no debe limitarse a sonar 'cuando algo choca'; debe enriquecer la respuesta del juego ante los comandos del jugador. Cuando el sistema de audio está deficientemente integrado, suele ocurrir el problema de repetición descontrolada: una moneda activa su sonido tantas veces como frames dura la colisión, o el paso del personaje satura los canales de audio provocando chasquidos digitales.

Para evitar esto, implementaremos funciones modulares que controlen la ganancia, el tono (*pitch*) y la concurrencia. Crearemos un script denominado `scr_audio_effects` que encapsule estas reglas operativas.

// ====================================================================
// SCRIPT: scr_audio_effects
// PROPÓSITO: Funciones auxiliares para la ejecución controlada de SFX
// ====================================================================

/// @function play_sfx(_sound_id, _priority, _pitch_variation)
/// @param {Asset.GMSound} _sound_id Identificador del sonido importado
/// @param {Real} _priority Prioridad en el mezclador (1 a 100)
/// @param {Real} _pitch_variation Variación aleatoria de tono (ej: 0.1)
function play_sfx(_sound_id, _priority, _pitch_variation) {
    var _instance = audio_play_sound(_sound_id, _priority, false);
    
    if (_pitch_variation > 0) {
        // Generar un tono ligeramente aleatorio para evitar monotonía auditiva
        var _random_pitch = random_range(1.0 - _pitch_variation, 1.0 + _pitch_variation);
        audio_sound_pitch(_instance, _random_pitch);
    }
    
    return _instance;
}

A continuación, observa cómo integramos esta llamada en la lógica del jugador al saltar y al recibir daño, garantizando que el audio responda con exactitud al cambio de estado interno.

// ====================================================================
// OBJETO: obj_player
// EVENTO: Step (Fragmento correspondiente a salto y aterrizaje)
// ====================================================================

// Comprobación de suelo antes del movimiento vertical
var _was_on_ground = on_ground;
on_ground = place_meeting(x, y + 1, obj_solid);

// Detección de impacto contra el suelo (aterrizaje)
if (!_was_on_ground && on_ground) {
    play_sfx(snd_land, 40, 0.15);
    // Crear partículas de polvo en los pies del jugador
    instance_create_layer(x, bbox_bottom, "Effects", obj_dust_impact);
}

// Ejecución de salto mediante botón de acción
if (key_jump_pressed && on_ground) {
    v_speed = jump_strength;
    play_sfx(snd_jump, 60, 0.08);
    instance_create_layer(x, bbox_bottom, "Effects", obj_dust_jump);
}

Sincronización del HUD con las variables globales

El HUD debe actuar como un espejo inmediato de lo que ocurre en la lógica, pero con técnicas de pulido visual para que la interfaz se sienta viva. En lugar de limitarse a imprimir un texto plano que cambia de forma brusca, agregaremos dos técnicas básicas de amortiguación visual en `obj_hud`:

1. **Escalado elástico del contador de monedas**: Cada vez que el jugador recolecta una moneda, el icono o texto del contador aumenta su escala momentáneamente y regresa con suavidad a su tamaño original. 2. **Parpadeo de la barra o corazones de vida**: Cuando el jugador recibe daño, el indicador visual de salud parpadea en color rojo o cambia su transparencia para alertar visualmente del impacto.

// ====================================================================
// OBJETO: obj_hud
// EVENTO: Create
// PROPÓSITO: Inicializar variables locales de animación del HUD
// ====================================================================

// Factores de escala para efecto de rebote en textos e iconos
coin_scale = 1.0;
score_display = global.player_score;

// Posición base de la interfaz (en coordenadas del GUI layer)
gui_margin_x = 40;
gui_margin_y = 40;
heart_spacing = 48;

En el evento **Step** de `obj_hud`, actualizamos suavemente los valores numéricos y amortiguamos las escalas hacia su valor de reposo (1.0) utilizando interpolación lineal (`lerp`).

// ====================================================================
// OBJETO: obj_hud
// EVENTO: Step
// PROPÓSITO: Actualizar animaciones visuales de la interfaz de usuario
// ====================================================================

// Interpolar suavemente el puntaje mostrado hacia el puntaje real
score_display = lerp(score_display, global.player_score, 0.15);

// Retornar la escala del icono de moneda a 1.0 tras una recogida
coin_scale = lerp(coin_scale, 1.0, 0.1);

// Si el jugador acaba de sumar una moneda en este frame, disparamos el rebote
if (global.coin_just_collected) {
    coin_scale = 1.5;
    global.coin_just_collected = false;
}

Posteriormente, en el evento **Draw GUI**, dibujamos los corazones de vida, el contador de monedas animado y el puntaje utilizando funciones estándar de renderizado de fuentes y sprites.

// ====================================================================
// OBJETO: obj_hud
// EVENTO: Draw GUI
// PROPÓSITO: Renderizar todos los elementos de pantalla sobre el juego
// ====================================================================

// 1. DIBUJAR VIDAS (Corazones)
for (var i = 0; i < global.player_max_hp; i++) {
    var _heart_x = gui_margin_x + (i * heart_spacing);
    var _heart_y = gui_margin_y;
    
    // Si el índice es menor que la vida actual, dibujamos corazón lleno
    if (i < global.player_hp) {
        draw_sprite(spr_hud_heart_full, 0, _heart_x, _heart_y);
    } else {
        draw_sprite(spr_hud_heart_empty, 0, _heart_x, _heart_y);
    }
}

// 2. DIBUJAR CONTADOR DE MONEDAS CON ESCALADO DINÁMICO
var _coin_icon_x = gui_margin_x;
var _coin_icon_y = gui_margin_y + 60;

draw_sprite_ext(
    spr_hud_coin, 
    0, 
    _coin_icon_x, 
    _coin_icon_y, 
    coin_scale, 
    coin_scale, 
    0, 
    c_white, 
    1.0
);

draw_set_font(fnt_hud);
draw_set_color(c_white);
draw_set_halign(fa_left);
draw_set_valign(fa_middle);

var _coin_str = "x " + string(global.coins_collected);
draw_text_transformed(_coin_icon_x + 36, _coin_icon_y, _coin_str, coin_scale, coin_scale, 0);

// 3. DIBUJAR PUNTAJE
var _score_text = "PUNTOS: " + string(ceil(score_display));
draw_set_halign(fa_right);
draw_text(display_get_gui_width() - gui_margin_x, gui_margin_y, _score_text);

// Restablecer alineación de texto estándar
draw_set_halign(fa_left);
draw_set_valign(fa_top);

Conexión integral en situaciones de juego críticas

Para comprobar que la integración es completa y sólida, analizaremos en detalle los dos eventos mecánicos más exigentes del proyecto: la recolección de coleccionables y el ciclo de daño/muerte.

Ahora examinemos el flujo inverso: qué ocurre cuando el jugador recibe daño de un enemigo o una trampa y sus puntos de vida llegan a cero.

// ====================================================================
// SCRIPT: scr_player_damage
// PROPÓSITO: Manejar el daño al jugador, audio, HUD y derrota
// ====================================================================

function take_player_damage(_amount) {
    // Si el jugador ya es invulnerable o está muerto, salir
    if (obj_player.is_invincible || global.player_hp <= 0) return;
    
    global.player_hp -= _amount;
    
    // Sacudida de cámara (Camera Shake creado en el Módulo 11, Clase 3)
    if (instance_exists(obj_camera)) {
        obj_camera.shake_intensity = 8.0;
    }
    
    if (global.player_hp > 0) {
        // El jugador sobrevive: sonido de golpe e invulnerabilidad temporal
        play_sfx(snd_hurt, 80, 0.05);
        obj_player.is_invincible = true;
        obj_player.alarm[0] = 60; // 1 segundo a 60 FPS de protección
    } else {
        // El jugador pierde toda la salud: sonido de muerte y secuencia final
        play_sfx(snd_player_death, 100, 0.0);
        
        // Reducir vidas globales
        global.player_lives -= 1;
        
        // Desactivar controles y física del jugador
        obj_player.state = "DEAD";
        obj_player.v_speed = -6; // Pequeño salto dramático antes de caer
        
        // Crear un controlador temporal de transición para dar dramatismo
        instance_create_depth(0, 0, 0, obj_death_sequence);
    }
}

En `obj_death_sequence`, tras una pausa de 2 segundos donde el jugador ve caer al personaje fuera de los límites de la pantalla mientras la música baja su volumen paulatinamente con `audio_sound_gain`, se decide si reiniciar en el checkpoint actual o redirigir a la sala `rm_gameover` si `global.player_lives <= 0`.

Matriz de verificación de subsistemas

Antes de dar por concluida la integración de un nivel o prototipo, es indispensable aplicar una lista de control cruzada. Esta matriz resume los comportamientos esperados en cada subsistema durante las situaciones más representativas del juego:

Situación de juegoRespuesta LÓGICA esperadaRespuesta HUD esperadaRespuesta AUDIO esperada
Salto desde el suelov_speed = -salto; cambio a sprite de salto; on_ground = false.Sin alteración numérica.snd_jump con variación leve de tono (pitch).
Recoger monedaMoneda destruida; global.coins_collected++.Contador aumenta escala (rebote) y actualiza texto.snd_coin con tono aleatorio; prioridad media.
Impacto de enemigo (no letal)HP disminuye; estado de invulnerabilidad activado.Corazón cambia a vacío; pantalla parpadea en rojo.snd_hurt reproduce de inmediato; sacudida de pantalla.
Muerte del jugador (vidas > 0)Bloqueo de inputs; jugador cae fuera del escenario.Todos los corazones vacíos; vidas restantes se reducen.snd_player_death; música del nivel se atenúa progresivamente.
Pausa del juegoVelocidades congeladas; variables de Step suspendidas.Se dibuja rótulo 'PAUSA' con cortina semitransparente.Efectos detenidos con audio_pause_all(); música con filtro o volumen reducido.
Victoria de nivelBloqueo de movimiento; jugador entra a puerta o meta.El tiempo transcurrido se congela y se suma al puntaje.Música de nivel se detiene; snd_victory suena una sola vez sin bucle.

Gestión de pausa: Un caso de prueba integral

El sistema de pausa es la prueba definitiva de que los tres subsistemas operan en armonía. Una pausa mal ejecutada suele causar que los efectos de sonido queden sonando indefinidamente en bucle, que la animación del HUD continúe ejecutándose o que el jugador pueda seguir disparando o saltando mientras el menú está activo.

A continuación, observa cómo gestionamos la pausa coordinando lógica, renderizado de HUD y estado sonoro dentro del propio `obj_game_manager`:

// ====================================================================
// OBJETO: obj_game_manager
// EVENTO: Step
// PROPÓSITO: Alternar el estado de pausa y gestionar subsistemas
// ====================================================================

if (keyboard_check_pressed(vk_escape) || gamepad_button_check_pressed(0, gp_start)) {
    // No permitir pausar si el jugador ya está muerto o en menús de título
    if (room != rm_title && room != rm_gameover && room != rm_victory) {
        global.game_paused = !global.game_paused;
        
        if (global.game_paused) {
            // 1. AUDIO: Pausar todos los sonidos activos y emitir SFX de menú
            audio_pause_all();
            // Reproducimos el sonido de pausa independientemente
            audio_play_sound(snd_pause, 100, false);
            
            // 2. LÓGICA: Desactivar instancias que no sean de control
            instance_deactivate_all(true);
            instance_activate_object(obj_game_manager);
            instance_activate_object(obj_hud);
        } else {
            // 1. LÓGICA: Reactivar todas las instancias del nivel
            instance_activate_all();
            
            // 2. AUDIO: Reanudar los sonidos previamente pausados
            audio_resume_all();
            audio_play_sound(snd_unpause, 100, false);
        }
    }
}

💡 Advertencia con instance_deactivate_all(): Cuando desactivas instancias en GameMaker, sus eventos Step y Draw se suspenden por completo. Esto es perfecto para la pausa, pero asegúrate de que el objeto que reactiva la sala (en este caso, obj_game_manager) y el HUD permanezcan activos mediante instance_activate_object().

Ejercicios prácticos

Pon en práctica los conocimientos adquiridos resolviendo los siguientes ejercicios estructurados. Cada uno de ellos exige la vinculación de al menos dos de los tres subsistemas explicados.

**Ejercicio 1: Advertencia audiovisual de vida crítica** Diseña un sistema donde, al quedarle al jugador exactamente 1 corazón de salud (`global.player_hp == 1`), ocurran dos respuestas automáticas: - El corazón restante en el HUD debe oscilar su opacidad (`image_alpha` o valor de dibujo) entre 0.3 y 1.0 cada 0.5 segundos. - Un sonido de advertencia tipo latido (`snd_heartbeat`) debe reproducirse en bucle suave hasta que el jugador recupere vida o muera. *Criterio de verificación:* Reduce intencionalmente la vida del jugador a 1 en el nivel. Comprueba que el latido comience y el corazón parpadee, y asegúrate de que ambos cesen al tomar una poción o coleccionable de salud que eleve la vida a 2 o más.

**Ejercicio 2: Racha de monedas con modulación ascendente de tono** Modifica la recolección de monedas para que, si el jugador recolecta monedas consecutivas en menos de 1 segundo entre una y otra, el sonido `snd_coin` incremente su afinación (*pitch*) en un 5% por cada moneda sucesiva (hasta un tope de 1.8x), volviendo a 1.0x si transcurre más de 1 segundo sin recoger ninguna. *Criterio de verificación:* Coloca una hilera de 5 monedas continuas en `rm_level_1`. Recórrelas a toda velocidad y comprueba auditivamente que cada recogida emita un sonido más agudo que el anterior, y que al esperar y recoger otra vuelva a su tono base.

**Ejercicio 3: Transición de audio y HUD al completar el nivel** Crea el objeto meta `obj_goal_door`. Al entrar en contacto con `obj_player`, debe: 1. Bloquear los controles del jugador y reproducir la animación de celebración. 2. Detener la música del nivel y ejecutar la fanfarria de victoria (`snd_victory`). 3. En el HUD, mostrar un cartel central con el puntaje final obtenido y los segundos sobrantes del nivel. 4. Esperar 3 segundos antes de hacer una transición suave hacia la siguiente sala (`rm_level_2` o `rm_victory`). *Criterio de verificación:* Llega al final del nivel con monedas y tiempo restante. Confirma que la música de fondo se silencie limpiamente, que la fanfarria suene completa y que los datos mostrados en el cartel coincidan con las variables globales calculadas.

Resumen

  • La integración exitosa requiere desacoplar responsabilidades: la lógica gestiona estados físicos y números, el HUD representa esos datos visualmente, y el sistema de audio aporta retroalimentación sonora inmediata.
  • El objeto obj_game_manager persistente actúa como eje central, resguardando la información entre salas y controlando el ciclo general (vidas, checkpoints, música ambiental y pausas).
  • El uso de variaciones de tono (pitch) en los efectos de sonido previene la fatiga auditiva producida por acciones mecánicas continuas como saltar, aterrizar o recolectar ítems.
  • Las micro-animaciones en el HUD (como interpolar el puntaje o rebotar la escala del icono de monedas) fortalecen la retroalimentación sin alterar las reglas matemáticas del juego.
  • El sistema de pausa exige la coordinación total de los tres subsistemas: congelación física de instancias, silenciamiento controlado de sonidos activos y despliegue del menú sobre la capa GUI.

Practica lo aprendido

Ejercicio 1 · 1 punto

Relaciona cada componente del videojuego con su responsabilidad asignada según la arquitectura centralizada explicada en la lección:

  • Objetos del mundo (monedas, pinchos)
  • Gestor global (obj_game_manager)
  • Objetos de salida (HUD y Audio)
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ejercicio 2 · 1 punto

Completa el bloque de código del evento Create de `obj_game_manager` utilizado para evitar instancias duplicadas si el objeto vuelve a cargarse: if (instance_number(obj_game_manager) > ) { (); exit; }

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

Ejercicio 3 · 1 punto

¿Por qué debe configurarse el objeto `obj_game_manager` como Persistent (persistente) en GameMaker?

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

Ingresa para guardar tu progreso — Ingresar