Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 7: Enemigos, inteligencia artificial básica y mecánicas de juego.
Implementación de vidas y barra de salud
Clase 33 de 60 · 55 min
También disponible en: EN
Introducción al sistema de salud y vidas en plataformas 2D
En el desarrollo de videojuegos clásicos de plataformas en dos dimensiones, la supervivencia del personaje principal se articula casi siempre mediante dos niveles de persistencia: la resistencia a corto plazo (salud o puntos de golpe) y la persistencia global de la partida (vidas o intentos totales). Mientras que la salud determina cuántos impactos menores puede soportar el avatar antes de sucumbir momentáneamente, las vidas dictan la cantidad de oportunidades que el jugador posee para superar el nivel antes de enfrentarse a la pantalla definitiva de derrota.
En la lección anterior establecimos el sistema de detección de colisiones entre el jugador y los enemigos patrulleros. Sin embargo, si nos limitáramos a reiniciar la sala cada vez que ocurre un contacto o a restar salud en cada fotograma, el juego sería injusto o injugable: debido a que GameMaker procesa el evento Step 60 veces por segundo de forma predeterminada, un contacto continuo de apenas medio segundo restaría hasta 30 unidades de daño instantáneamente. Por esta razón, un sistema robusto de salud no solo requiere variables numéricas de descuento, sino también una ventana de invulnerabilidad temporal (conocida en la industria como 'invincibility frames' o 'i-frames') y una rutina disciplinada de reubicación o reaparición (respawn).
Variables esenciales para la gestión de salud, vidas y posición
Para construir este sistema sin recurrir a variables globales dispersas que dificulten el mantenimiento, mantendremos el estado dentro de la propia instancia de nuestro jugador (`obj_jugador`), almacenando también las coordenadas espaciales donde deberá volver a materializarse en caso de ser derrotado. A continuación, se detallan los parámetros elementales que gobernarán la lógica de supervivencia:
| Nombre de variable | Tipo de dato | Valor inicial recomendado | Propósito en la mecánica |
|---|---|---|---|
| salud_maxima | Real (Numérico) | 100 | Límite superior absoluto de la barra de vitalidad. |
| salud_actual | Real (Numérico) | 100 | Cantidad de vitalidad vigente en cada fotograma. |
| vidas | Entero (Numérico) | 3 | Número de intentos globales antes del Game Over. |
| es_invulnerable | Booleano | false | Bandera de estado que bloquea la recepción de daño. |
| tiempo_invulnerable | Entero (Numérico) | 0 | Temporizador en pasos (steps) para expirar la inmunidad. |
| duracion_invulnerable | Entero (Numérico) | 60 | Pasos totales de protección tras un golpe (1 segundo a 60 fps). |
| origen_x | Real (Numérico) | x inicial | Coordenada horizontal de reaparición segura (punto de inicio). |
| origen_y | Real (Numérico) | y inicial | Coordenada vertical de reaparición segura (punto de inicio). |
Es fundamental comprender la diferencia entre `salud_maxima` y `salud_actual`. Utilizar un valor fijo en el código (conocido como 'hardcoding') para rellenar la vida cuando el personaje reaparece es una práctica desaconsejada. Si más adelante en el proyecto permites que el jugador compre mejoras de vitalidad o consiga contenedores de corazón adicionales, bastará con modificar `salud_maxima`, y todas las rutinas de cálculo porcentual y curación se adaptarán automáticamente sin errores colaterales.
Configuración del evento Create en obj_jugador
Abramos nuestro objeto principal `obj_jugador`. En lecciones anteriores ya declaramos variables de movimiento como `velocidad_x`, `velocidad_y`, `gravedad` y `fuerza_salto`. Ahora integraremos las variables de estado físico y coordenadas de reaparición al final del evento Create para asegurar que el personaje nazca con sus atributos listos desde el primer instante en que la sala se carga.
/// @description Inicialización de salud, vidas y punto de partida
// Parámetros de resistencia física
salud_maxima = 100;
salud_actual = salud_maxima;
vidas = 3;
// Control de inmunidad temporal frente a golpes continuos
es_invulnerable = false;
tiempo_invulnerable = 0;
duracion_invulnerable = 60; // Equivalente a 1 segundo si la sala corre a 60 FPS
// Registro de posición para reaparición (respawn)
origen_x = x;
origen_y = y;Al asignar `x` e `y` a las variables `origen_x` y `origen_y`, GameMaker captura con precisión las coordenadas exactas donde colocaste la instancia en el editor de niveles de la sala (Room). De este modo, no dependes de valores arbitrarios y puedes posicionar al jugador en cualquier rincón del mapa desde el editor visual sin alterar una sola línea de código.
El ciclo de recepción de daño e invulnerabilidad temporal
El problema clásico del principiante al programar colisiones con enemigos es restar daño directamente en el evento de colisión sin verificar si el jugador ya se encuentra en un estado herido. Al hacer esto, si el jugador se cruza con un enemigo que inflige 25 puntos de daño, la salud descenderá de 100 a 0 en apenas cuatro fotogramas (menos de la décima parte de un segundo), dando la falsa impresión de que el enemigo mata de un solo golpe.
Para solventarlo, se utiliza una ventana de inmunidad. Cuando el jugador es impactado por un adversario, debe ocurrir una secuencia lógica estructurada: primero se comprueba que `es_invulnerable` sea falso; si lo es, se resta el daño correspondiente, se enciende la bandera `es_invulnerable = true` y se inicializa `tiempo_invulnerable = duracion_invulnerable`. Durante todos los fotogramas en que el temporizador sea mayor a cero, cualquier otra colisión dañina será descartada por completo.

Actualización y retroalimentación visual en el evento Step
En el evento Step de `obj_jugador`, debemos reducir el contador de invulnerabilidad fotograma a fotograma hasta que llegue a cero. Además de la lógica puramente matemática, es fundamental ofrecer retroalimentación sensorial inmediata: el jugador necesita saber que su personaje está herido y protegido temporalmente. En la tradición de los juegos retro de 8 y 16 bits, esto se consigue alternando la visibilidad o la transparencia del sprite (canal alfa) para producir un parpadeo constante.
Incorpora el siguiente bloque de código dentro del evento Step de `obj_jugador`, situándolo justo antes o después del código de movimiento y gravedad que desarrollamos en los módulos anteriores:
/// @description Gestión de temporizador de invulnerabilidad y parpadeo
if (es_invulnerable)
{
tiempo_invulnerable -= 1;
// Efecto visual de parpadeo modificando image_alpha cada pocos fotogramas
if ((tiempo_invulnerable mod 8) < 4)
{
image_alpha = 0.3; // Semitransparente
}
else
{
image_alpha = 1.0; // Totalmente opaco
}
// Restauración del estado normal al agotarse el tiempo
if (tiempo_invulnerable <= 0)
{
es_invulnerable = false;
image_alpha = 1.0; // Garantizar opacidad total final
}
}Analicemos la expresión `(tiempo_invulnerable mod 8) < 4`. El operador `mod` (módulo) obtiene el resto de la división entera entre el valor actual del contador y 8. Esto produce una secuencia cíclica de números enteros entre 0 y 7. Mientras el resultado se mantenga por debajo de 4, el valor de `image_alpha` se sitúa en 0.3 (haciendo que el sprite se vuelva casi transparente); cuando alcanza valores de 4 a 7, recupera la opacidad plena (1.0). El resultado en pantalla es un parpadeo regular, limpio y libre de artefactos visuales.
Colisión con el enemigo y aplicación de daño
En la clase previa aprendiste a detectar la presencia de `obj_enemigo_patrullero`. Vamos a refinar esa detección para que no solo exista contacto, sino que el enemigo sea capaz de transferir un daño determinado al jugador y empujarlo ligeramente hacia atrás para despegar los cuerpos y evitar que queden atascados dentro de sus respectivas cajas de colisión (máscaras).
Dentro del evento Step de `obj_jugador`, utilizaremos la función nativa `instance_place(x, y, obj_enemigo_patrullero)`. Esta función devuelve la identificación única (`id`) de la instancia concreta del enemigo con la cual estamos colisionando en ese preciso instante. Si no hay colisión, devuelve la constante especial `noone`.
/// @description Detección de contacto con enemigos y recepción de daño
var _enemigo = instance_place(x, y, obj_enemigo_patrullero);
if (_enemigo != noone and !es_invulnerable)
{
// Obtenemos el daño del enemigo o aplicamos un valor por defecto si no lo tiene definido
var _dano_recibido = 25;
if (variable_instance_exists(_enemigo, "dano"))
{
_dano_recibido = _enemigo.dano;
}
// Aplicar el daño y activar la ventana de protección
salud_actual -= _dano_recibido;
salud_actual = clamp(salud_actual, 0, salud_maxima);
es_invulnerable = true;
tiempo_invulnerable = duracion_invulnerable;
// Pequeño impulso de retroceso (knockback)
velocidad_y = -4; // Salto involuntario hacia arriba
if (x < _enemigo.x)
{
velocidad_x = -3; // Impulso a la izquierda
}
else
{
velocidad_x = 3; // Impulso a la derecha
}
}💡 La función `clamp(val, min, max)` es fundamental en el diseño de videojuegos: restringe un número para que nunca caiga por debajo de un mínimo ni supere un límite superior. Al usar `salud_actual = clamp(salud_actual, 0, salud_maxima);`, nos aseguramos de que la vida jamás se vuelva negativa ni sobrepase el cien por cien al recoger pociones curativas.
Mecánica de pérdida de vida y reaparición (Respawn)
Cuando `salud_actual` alcanza el valor cero, el ciclo de vida del personaje ha concluido. En lugar de destruir la instancia mediante `instance_destroy()`, lo cual rompería las referencias de la cámara o detendría la ejecución de scripts asociados a ese objeto, llevaremos a cabo una reaparición controlada:
1. Descontar una vida del contador global (`vidas -= 1`). 2. Teletransportar inmediatamente al personaje a las coordenadas almacenadas en `origen_x` y `origen_y`. 3. Cancelar todas las fuerzas acumuladas de inercia (`velocidad_x = 0`, `velocidad_y = 0`) para evitar que el avatar caiga o se mueva descontroladamente al aparecer. 4. Restablecer la salud a su valor tope (`salud_actual = salud_maxima`). 5. Conceder un período breve de invulnerabilidad de cortesía para que el jugador no reciba daño instantáneo si reaparece cerca de una amenaza. 6. Comprobar si las vidas han llegado a cero. Si es así, se desencadena el estado de derrota o se reinicia el nivel.
Añade la siguiente comprobación en el evento Step de `obj_jugador`, inmediatamente después del cálculo de daño:
/// @description Comprobación de muerte, pérdida de vida y reaparición
if (salud_actual <= 0)
{
vidas -= 1;
if (vidas > 0)
{
// Reaparición en el punto de control u origen
x = origen_x;
y = origen_y;
// Frenar completamente cualquier inercia de movimiento
velocidad_x = 0;
velocidad_y = 0;
// Restaurar salud completa para el nuevo intento
salud_actual = salud_maxima;
// Otorgar inmunidad temporal breve tras el respawn
es_invulnerable = true;
tiempo_invulnerable = duracion_invulnerable;
}
else
{
// Se agotaron todas las vidas de la partida
// Por ahora reiniciamos la sala; en el Módulo 8 implementaremos la pantalla de Game Over
room_restart();
}
}Representación visual: Dibujando la barra de salud
Aunque en el próximo módulo (Módulo 8) profundizaremos exhaustivamente en la capa de interfaz de usuario con el evento Draw GUI, resulta indispensable contar desde este momento con una respuesta visual inmediata sobre el estado de la salud en la propia pantalla de juego. Existen dos métodos principales para representar la salud del jugador de manera temprana: una barra flotante que persigue al personaje en el espacio del mundo mediante el evento Draw tradicional, o una barra fija en pantalla mediante primitivas geométricas.
Implementaremos una barra de salud flotante situada justo encima de la cabeza del avatar. Para ello, crearemos el evento Draw en `obj_jugador`. Es primordial recordar una regla de oro en GameMaker: **si colocas código en el evento Draw de un objeto, GameMaker dejará de dibujar el sprite predeterminado de manera automática**. Por lo tanto, la primera instrucción que siempre debemos escribir en el evento Draw es `draw_self()`, la cual se encarga de renderizar el sprite habitual antes de pintar los elementos decorativos encima.
/// @description Evento Draw de obj_jugador: renderizado del sprite y barra flotante
// 1. Dibujar el sprite del personaje respetando su animación y alfa
draw_self();
// 2. Parámetros geométricos de la barra flotante
var _ancho_barra = 32;
var _alto_barra = 4;
var _desplazamiento_y = 20; // Píxeles por encima del origen del sprite
// Coordenadas calculadas en torno a la posición del jugador
var _x1 = x - (_ancho_barra / 2);
var _y1 = y - sprite_yoffset - _desplazamiento_y;
var _x2 = x + (_ancho_barra / 2);
var _y2 = _y1 + _alto_barra;
// 3. Dibujar el fondo oscuro de la barra (vida faltante / contenedor)
draw_set_color(c_black);
draw_rectangle(_x1 - 1, _y1 - 1, _x2 + 1, _y2 + 1, false);
// 4. Calcular el ancho de la porción verde proporcional a la salud restante
var _porcentaje = salud_actual / salud_maxima;
var _x_vida = _x1 + (_ancho_barra * _porcentaje);
// 5. Determinar color dinámico: verde si está sano, amarillo si está herido, rojo en crítico
var _color_salud = c_lime;
if (_porcentaje <= 0.25)
{
_color_salud = c_red;
}
else if (_porcentaje <= 0.5)
{
_color_salud = c_yellow;
}
// 6. Dibujar el relleno de salud actual
if (salud_actual > 0)
{
draw_set_color(_color_salud);
draw_rectangle(_x1, _y1, _x_vida, _y2, false);
}
// 7. Restaurar el color base de dibujo para no alterar otros elementos gráficos
draw_set_color(c_white);Examinemos con atención el cálculo geométrico. Al restar `sprite_yoffset`, nos aseguramos de que sin importar dónde hayas fijado el punto de anclaje (origen) del sprite del personaje (en el centro, en la base de los pies o en la esquina superior izquierda), la barra siempre se dibujará con precisión 20 píxeles por encima del borde superior real del cuerpo. Adicionalmente, el cambio condicional de color (`c_lime`, `c_yellow`, `c_red`) añade una advertencia intuitiva al jugador conforme su situación se vuelve crítica.
Ejemplo práctico guiado: Demostración de impacto y reaparición
Con el fin de verificar que todas las piezas encajan a la perfección, sigue este procedimiento de prueba estructurado dentro del entorno de desarrollo:
1. Abre la sala principal del proyecto (`Room1` o `rm_nivel_1`). 2. Coloca una instancia de `obj_jugador` en una plataforma segura a la izquierda del escenario. 3. Coloca una instancia de `obj_enemigo_patrullero` en la misma plataforma, permitiéndole patrullar hacia la derecha. 4. Ejecuta el juego pulsando F5 (o el botón de reproducción verde en la barra superior). 5. Camina intencionalmente hacia el enemigo para provocar el primer impacto. Observa cómo el jugador sale despedido levemente hacia arriba y hacia la izquierda, su sprite comienza a parpadear con semitransparencia y la barra sobre su cabeza disminuye un tercio de su longitud cambiando a color verde o amarillo. 6. Intenta colisionar nuevamente contra el enemigo mientras el parpadeo continúe activo. Podrás comprobar que el cuerpo del enemigo no te causa más daño durante ese segundo de gracia. 7. Permite que el parpadeo finalice y recibe dos impactos adicionales. Tan pronto como la salud desciende a cero, la instancia desaparece del lugar de la caída y reaparece instantáneamente en la posición original donde iniciaste la sala, con la barra de salud completamente regenerada al 100% y con una vida menos en su registro interno.
Ejercicios prácticos
A continuación, dispones de tres actividades progresivas para consolidar lo aprendido, reforzar la escritura en GML Code y resolver problemas comunes del desarrollo de mecánicas:
<strong>Ejercicio 1: Zona de caída mortal (muerte por precipicio)</strong> En la mayoría de juegos de plataformas, si el jugador cae al vacío fuera de la pantalla, no debe flotar indefinidamente ni sufrir daño fraccionado: debe perder una vida al instante y reaparecer de inmediato. Escribe una comprobación condicional en el evento Step de `obj_jugador` que evalúe si la posición vertical `y` supera la altura máxima de la sala (`room_height + 32`). En caso afirmativo, reduce su `salud_actual` directamente a 0 para forzar el protocolo de respawn. <em>Criterio de corrección:</em> Al dejar caer deliberadamente al personaje por un agujero entre plataformas, debe reaparecer en `origen_x` y `origen_y` con una vida descontada, sin generar errores de ejecución.
<strong>Ejercicio 2: Marcador numérico de vidas flotante</strong> Modifica el evento Draw de `obj_jugador` para que, justo a la derecha de la barra rectangular de salud, se dibuje un texto con la cantidad de vidas restantes utilizando la función nativa `draw_text(x, y, string)`. Recuerda configurar `draw_set_color(c_white)` antes de dibujar el texto para asegurar su legibilidad. <em>Criterio de corrección:</em> En pantalla debe visualizarse un texto claro como 'x 3' o 'Vidas: 3' que acompañe a la barra y disminuya a 'x 2' y 'x 1' cada vez que el personaje sufra un colapso de salud.
<strong>Ejercicio 3: Puntos de control manuales (Preámbulo de Checkpoints)</strong> Declara en el evento Step una tecla de depuración rápida (por ejemplo, la barra espaciadora o la tecla 'C') que, al ser presionada mediante `keyboard_check_pressed(ord("C"))`, actualice los valores de `origen_x` y `origen_y` con la posición actual del jugador (`origen_x = x; origen_y = y;`). <em>Criterio de corrección:</em> Tras avanzar a una plataforma lejana y pulsar la tecla 'C', al morir contra un enemigo o caer al vacío el jugador no debe regresar al inicio del nivel, sino reaparecer exactamente en el nuevo punto donde activó el guardado manual.
Resumen
- Un sistema sólido de vida separa la resistencia a corto plazo (salud/HP) de la persistencia de intentos a largo plazo (vidas).
- Los cuadros de invulnerabilidad temporal (i-frames) evitan que una colisión continua reste daño en cada fotograma sucesivo.
- El parpadeo visual del sprite se logra de forma matemática y limpia mediante el operador módulo (mod) aplicado al canal image_alpha.
- La función clamp() garantiza que la salud se mantenga siempre dentro de los márgenes seguros comprendidos entre 0 y el valor tope de salud_maxima.
- La reaparición no debe destruir la instancia del jugador; consiste en reubicar sus coordenadas (x, y) en el origen, reiniciar velocidades y restaurar la vitalidad.
- Al redefinir el evento Draw para pintar barras o textos, es imperativo iniciar con draw_self() para evitar que el sprite principal del personaje deje de verse en pantalla.
Practica lo aprendido
Ejercicio 1 · 1 punto
¿Por qué un contacto continuo con un enemigo de apenas medio segundo puede llegar a restar hasta 30 unidades de daño si no se programa una ventana de invulnerabilidad temporal?
Ejercicio 2 · 1 punto
Para asegurar que los puntos de vida del personaje jamás caigan por debajo de cero ni sobrepasen el tope establecido al curarse, se utiliza la siguiente línea de código: salud_actual = (salud_actual, 0, );
Ejercicio 3 · 1 punto
Relaciona cada elemento del código analizado en la lección con su función correspondiente dentro del sistema de salud y reaparición:
- image_alpha
- origen_x y origen_y
- instance_place
- duracion_invulnerable
Ingresa para guardar tu progreso — Ingresar