Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 7: Enemigos, inteligencia artificial básica y mecánicas de juego.

Sistema de colisión entre jugador y enemigos

Clase 32 de 60 · 60 min

También disponible en: EN

Introducción: El dilema del contacto continuo en videojuegos 2D

En la clase anterior dimos vida a nuestro primer enemigo con una rutina de patrullaje básico. Sin embargo, en este momento el enemigo y el jugador son como fantasmas que se cruzan en pantalla sin interactuar, o peor aún, si configuramos una colisión sólida sin lógica de daño, el enemigo empujará al jugador como si fuera una pared móvil. El objetivo fundamental de un juego de plataformas tradicional es que el contacto no deseado con una amenaza penalice al jugador reduciendo su salud o haciéndolo perder una vida.

Para un principiante, la primera intuición suele ser: «Cuando el jugador toque al enemigo, restamos 1 punto de vida». No obstante, en un motor como GameMaker que ejecuta su bucle principal a 60 cuadros por segundo (60 FPS), la función de colisión devolverá verdadero durante cada fotograma en el que las cajas de colisión (hitboxes) se superpongan. Si el jugador permanece en contacto con el enemigo durante apenas medio segundo (30 fotogramas), el sistema restará 30 puntos de vida de forma fulminante. Este fenómeno se conoce como daño por contacto continuo descontrolado.

Para resolver este problema y ofrecer una experiencia de juego justa y profesional, los videojuegos clásicos recurren a tres mecanismos coordinados: la detección de la colisión discreta, la aplicación de un impulso de retroceso (knockback) que separa físicamente a los personajes, y un período de inmunidad temporal (popularmente conocido como «i-frames» o invulnerability frames). Durante este intervalo de gracia, el jugador no puede recibir daño adicional y recibe una señal visual clara (generalmente un parpadeo intermitente) que comunica su estado de invulnerabilidad.

Métodos de detección en GameMaker: Collision Event vs. Código en Step

GameMaker ofrece dos vías principales para gestionar colisiones entre objetos. La primera es el evento nativo de colisión (Collision Event), accesible desde el panel de eventos del objeto. La segunda es la verificación manual mediante funciones de colisión en el evento Step utilizando GML Code. Aunque ambas son válidas, en proyectos profesionales y mecánicas que requieren alta precisión matemática (como el cálculo de vectores de retroceso y estados de daño), se recomienda centralizar la lógica en el evento Step.

Función / MecanismoRetornoUso recomendado
Collision Event (Nativo)Ejecuta código automáticamente en colisiónMecánicas simples como recoger monedas o activar disparadores (triggers).
place_meeting(x, y, obj)Booleano (true / false)Comprobar si hay colisión antes de moverse (ideal para paredes y suelos sólidos).
instance_place(x, y, obj)ID de la instancia colisionada (o noone)Crucial para combate. Permite leer la posición X e Y exacta del enemigo específico con el que chocamos.

Para nuestra lógica de daño utilizaremos la función instance_place(). A diferencia de place_meeting(), que solo nos avisa si existe una colisión o no con un simple sí o no, instance_place() nos devuelve el identificador único (id) de la instancia concreta del enemigo que nos está tocando. Este dato es vital porque necesitamos saber en qué posición exacta se encuentra ese enemigo respecto al jugador para calcular la dirección hacia la que el jugador debe salir despedido por el impacto.

Diagrama técnico que ilustra la detección de superposición de colisión, el vector de retroceso separando al jugador del enemigo y la barra temporal de cuadros de inmunidad con parpadeo.
Mecánica de resolución de impacto: superposición de máscaras, impulso de retroceso vectorial y activación de la ventana de inmunidad temporal. · Imagen generada con IA

Configuración de variables en el Jugador (obj_player)

Para que nuestro personaje soporte daño, retroceso e inmunidad, primero debemos definir las variables correspondientes en su evento Create. Retomando la base de movimiento que construimos en los módulos anteriores (donde ya contamos con variables como hspd, vspd, walk_speed, etc.), agregaremos las variables de salud y control de daño.

// --- Evento Create de obj_player ---

// Salud del jugador
hp_max = 3;
hp = hp_max;

// Sistema de inmunidad temporal (I-Frames)
is_invincible = false;           // Bandera que indica si el jugador es invulnerable
invincible_timer = 0;            // Contador regresivo en fotogramas
invincible_duration = 90;        // Duración de la inmunidad (90 fotogramas = 1.5 segundos a 60 FPS)

// Retroceso por impacto (Knockback)
knockback_speed_x = 4;           // Fuerza horizontal del golpe
knockback_speed_y = -3;          // Pequeño salto vertical al recibir daño
is_knockbacked = false;          // Estado de pérdida temporal de control por golpe
knockback_timer = 0;             // Duración de la pérdida de control
knockback_duration = 12;         // 12 fotogramas (0.2 segundos) de parálisis por impacto

Analicemos con detalle el propósito de estas variables. La variable invincible_duration define cuántos fotogramas estará protegido el jugador. En un juego configurado a 60 FPS, 90 fotogramas equivalen exactamente a un segundo y medio (90 / 60 = 1.5 s). Durante este tiempo, cualquier contacto adicional con enemigos será completamente ignorado.

Por su parte, is_knockbacked y knockback_timer se encargan de deshabilitar brevemente los controles del teclado o mando del jugador. Si no deshabilitamos el control durante el retroceso, la pulsación del jugador en dirección contraria neutralizaría de inmediato el empuje del impacto, haciendo que el golpe se sienta rígido, falso y carente de peso físico.

Lógica de detección y respuesta en el Evento Step

El evento Step de obj_player debe estructurarse de manera ordenada. Primero debemos actualizar los temporizadores activos; luego verificar si el jugador tiene el control para moverse; y finalmente, si no es invencible, comprobar si está tocando a un enemigo para desencadenar el impacto.

// --- Fragmento en Evento Step de obj_player ---

// 1. ACTUALIZAR TEMPORIZADORES DE INMUNIDAD Y RETROCESO
if (is_invincible)
{
    invincible_timer -= 1;
    if (invincible_timer <= 0)
    {
        is_invincible = false;
    }
}

if (is_knockbacked)
{
    knockback_timer -= 1;
    if (knockback_timer <= 0)
    {
        is_knockbacked = false;
    }
}

// 2. ENTRADA DEL JUGADOR (Solo permitida si no está sufriendo retroceso)
if (!is_knockbacked)
{
    // Lectura normal de teclas (reutilizando variables del módulo 4)
    var _key_left  = keyboard_check(vk_left)  || keyboard_check(ord("A"));
    var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
    var _move_dir  = _key_right - _key_left;
    
    hspd = _move_dir * walk_speed;
    
    // Lógica de salto habitual
    var _key_jump = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
    if (_key_jump && is_grounded)
    {
        vspd = jump_speed;
    }
}

// 3. APLICACIÓN DE GRAVEDAD Y FÍSICA HABITUAL
vspd += grv;

// 4. DETECCIÓN DE COLISIÓN CON EL ENEMIGO
if (!is_invincible)
{
    // Buscamos si existe colisión con el objeto enemigo patrullero
    var _enemy_hit = instance_place(x, y, obj_enemy_patrol);
    
    if (_enemy_hit != noone)
    {
        // Restar vida
        hp -= 1;
        
        // Activar la inmunidad temporal
        is_invincible = true;
        invincible_timer = invincible_duration;
        
        // Activar retroceso (Knockback)
        is_knockbacked = true;
        knockback_timer = knockback_duration;
        
        // Determinar la dirección del empuje horizontal
        // Si el jugador está a la derecha del enemigo, es empujado hacia la derecha (+1)
        // Si está a la izquierda, es empujado hacia la izquierda (-1)
        var _push_direction = sign(x - _enemy_hit.x);
        if (_push_direction == 0) _push_direction = 1; // Salvaguarda si coinciden exactamente en X
        
        hspd = _push_direction * knockback_speed_x;
        vspd = knockback_speed_y; // Pequeño salto hacia arriba
    }
}

Examinemos la línea de cálculo de dirección: var _push_direction = sign(x - _enemy_hit.x);. La función sign() toma cualquier número real y devuelve 1 si es positivo, -1 si es negativo, y 0 si es cero. Si la posición X del jugador es mayor que la posición X del enemigo (_enemy_hit.x), la resta da un valor positivo, lo que significa que el jugador está a la derecha del enemigo; sign() devuelve 1, proyectando a nuestro protagonista a la derecha. Si el jugador está a la izquierda, la resta es negativa, y el jugador es proyectado a la izquierda. Añadir la salvaguarda if (_push_direction == 0) _push_direction = 1; previene que el jugador quede inmóvil si ambos orígenes de coordenadas coinciden en el mismo píxel.

💡 Recuerda que el código de movimiento horizontal y vertical con detección de paredes sólidas (visto en el Módulo 5) debe ejecutarse DESPUÉS de calcular estas velocidades. De este modo, si el retroceso empuja al jugador contra una pared sólida, el sistema de colisión física evitará que atraviese el escenario.

Distinción clásica de plataformas: Aplastamiento superior (Stomp)

En muchos juegos de plataformas clásicos (como Super Mario o Sonic), el jugador no siempre recibe daño al tocar al enemigo. Si el jugador cae desde arriba sobre la cabeza del enemigo, es el enemigo quien resulta dañado o eliminado, y el jugador rebota para continuar su salto. Esta mecánica aporta dinamismo y premia la destreza del usuario.

Para implementar esta condición, antes de aplicar el daño al jugador debemos verificar dos condiciones geométricas: que el jugador esté cayendo (vspd > 0) y que la base del jugador se encuentre por encima del centro o cabeza del enemigo. Si ambas condiciones se cumplen, destruimos o dañamos al enemigo y hacemos rebotar al jugador; de lo contrario, el jugador recibe el daño y el retroceso habituales.

Fíjate en la comprobación bbox_bottom - vspd <= _enemy_hit.bbox_top + 8. bbox_bottom representa la coordenada Y del borde inferior de la máscara de colisión del jugador. Restar vspd nos dice dónde estaba el pie del jugador en el fotograma anterior. Si en el fotograma anterior los pies estaban a la altura o por encima del borde superior del enemigo (bbox_top), confirmamos que la trayectoria fue un pisotón legítimo y no una colisión lateral con caída tardía.

Retroalimentación visual: El efecto de parpadeo (Blinking)

Un error común al programar inmunidad temporal es no otorgar una señal visual clara de que el estado está activo. Si el jugador recibe un golpe, entra en inmunidad y camina a través de un grupo de enemigos como si nada ocurriera con su sprite estático habitual, el usuario sentirá confusión y desorientación visual. Necesitamos comunicar inequívocamente: «Estás temporalmente a salvo».

El estándar de la industria para juegos 2D retro es el parpadeo de transparencia (modulación de canal alfa) o la alternancia de visibilidad. En GameMaker podemos lograr esto de dos formas principales:

TécnicaImplementaciónVentajas y consideraciones
Modificación de image_alpha en Stepimage_alpha = 0.5 (o alternar entre 0 y 1)Muy simple de escribir, pero requiere acordarse de restaurar image_alpha = 1 obligatoriamente al expirar el temporizador.
Control manual en el evento Drawdraw_self() condicional usando el operador módulo (%)Altamente configurable; no altera el estado interno de la instancia fuera del renderizado.

Implementaremos el control en el evento Draw de obj_player, combinándolo con el operador matemático de residuo o módulo (%). Este operador calcula el resto de una división entera. Al evaluar (invincible_timer % 8) < 4, el valor oscila periódicamente cada 8 fotogramas: durante 4 fotogramas la condición es verdadera y durante los siguientes 4 es falsa. Esta matemática pura genera un parpadeo limpio y estable a una frecuencia de 7.5 pulsaciones por segundo a 60 FPS.

// --- Evento Draw de obj_player ---

// Si el jugador es invulnerable, alternamos su transparencia de dibujo
if (is_invincible)
{
    // Hacemos que parpadee entre semi-transparente (0.3) y opaco (1.0)
    // Usamos el operador módulo para alternar cada 6 fotogramas
    if ((invincible_timer % 12) < 6)
    {
        draw_set_alpha(0.3);
    }
    else
    {
        draw_set_alpha(1.0);
    }
    
    // Dibujamos el sprite actual con la transparencia configurada
    draw_self();
    
    // IMPORTANTE: Siempre restaurar el canal alfa a 1.0 para no afectar otros dibujos
    draw_set_alpha(1.0);
}
else
{
    // Comportamiento regular cuando no está en inmunidad
    draw_self();
}

💡 Regla fundamental en GameMaker: Si creas un evento Draw en cualquier objeto, la acción automática de dibujarse se desactiva. Por esta razón es obligatorio llamar explícitamente a draw_self() dentro del evento Draw. Si no colocas draw_self(), la instancia existirá y colisionará en el nivel, pero será 100% invisible para el jugador.

Gestión del fin de la partida: Derrota provisional

Aunque en la siguiente clase desarrollaremos una barra de salud completa en pantalla con persistencia de vidas, nuestro sistema actual de colisión necesita responder cuando la variable hp llega a 0. En este punto de nuestro proyecto, una respuesta simple, robusta y limpia es reiniciar la room actual para que el estudiante pueda probar el bucle de juego una y otra vez.

// --- Al final del bloque de daño o al final del Evento Step de obj_player ---
if (hp <= 0)
{
    // Si la salud llega a cero, reiniciamos la habitación actual
    room_restart();
}

Pruebas y resolución de problemas comunes (Troubleshooting)

Durante las primeras pruebas de este sistema, es habitual toparse con ciertos comportamientos no deseados. A continuación, revisamos los tres errores más frecuentes que experimentan los principiantes y cómo solucionarlos:

1. El jugador queda atrapado dentro de una pared sólida tras el retroceso: Esto ocurre si aplicas el knockback modificando directamente las coordenadas x += knockback_speed_x en lugar de manipular la velocidad hspd. Al alterar hspd, nuestro sistema de colisiones con obj_solid (que implementamos en el Módulo 5) se encarga de frenar al jugador antes de que atraviese cualquier bloque sólido.

2. El parpadeo se queda atascado y el jugador permanece invisible o transparente: Esto sucede cuando se manipula la variable nativa image_alpha dentro del Step pero no se asegura su restablecimiento a 1.0 en el momento en que is_invincible pasa a false. El enfoque que usamos en el evento Draw usando draw_set_alpha(1.0) al final evita por completo este inconveniente.

3. El enemigo y el jugador rebotan infinitamente: Asegúrate de que la condición if (!is_invincible) envuelva la detección instance_place. Si la detección se ejecuta cuando el jugador aún está en período de gracia, el temporizador de knockback podría estarse reiniciando constantemente impidiendo que el jugador recupere el control.

Ejercicios prácticos

Realiza las siguientes tres actividades para consolidar tus conocimientos y poner a prueba la flexibilidad de tu sistema de colisiones:

Ejercicio 1 (Ajuste de tiempos y retroceso): Modifica los valores de invincible_duration y knockback_speed_x. Realiza tres pruebas: primero con una inmunidad muy corta (20 fotogramas / un tercio de segundo), luego con la configurada (90 fotogramas) y finalmente con una extendida (180 fotogramas / 3 segundos). Experimenta con el retroceso horizontal a valores 2, 5 y 8. Observa cómo cambia la sensación de juego (game feel). Criterio de comprobación: A valores bajos de duración de inmunidad, recibirás daño múltiple rápidamente si el enemigo te acorrala contra una pared; comprueba que los controles vuelvan a responder exactamente al terminar knockback_duration.

Ejercicio 2 (Mecánica de aplastamiento con efecto sonoro o rebote variable): Implementa el bloque de código de aplastamiento (Stomp) en tu proyecto. Añade una variación: si el jugador mantiene pulsado el botón de salto mientras cae sobre el enemigo, el rebote debe ser el doble de alto (-9 de vspd en lugar de -5.5). Criterio de comprobación: Colócate en una plataforma elevada, tírate sobre el enemigo sin tocar teclas y anota la altura del rebote. Repite la prueba manteniendo presionada la tecla de salto (Espacio o W) justo al hacer impacto y verifica que el personaje ascienda significativamente más alto.

Ejercicio 3 (Parpadeo cromático en lugar de transparencia): Modifica el evento Draw para que el jugador no solo alterne su canal alfa, sino que cambie de color mediante la función image_blend o draw_sprite_ext. Haz que durante la inmunidad el sprite alterne entre su color original (c_white) y un tinte rojo de peligro (c_red). Criterio de comprobación: Al recibir un impacto, el jugador debe titilar en color rojo durante el segundo y medio de inmunidad, regresando a su coloración normal tan pronto como el temporizador llegue a cero.

Resumen

  • El daño por contacto en videojuegos requiere pausar el registro de impactos continuos mediante períodos de inmunidad (i-frames), evitando muertes instantáneas injustas a 60 FPS.
  • La función 'instance_place(x, y, obj)' es idónea para gestionar combates, ya que devuelve el ID específico de la instancia enemiga con la que se colisiona, permitiendo calcular vectores direccionales.
  • El retroceso físico (knockback) se determina calculando la dirección relativa entre jugador y enemigo con la función 'sign()', inyectando velocidad horizontal y vertical mientras se bloquea brevemente la entrada de control del usuario.
  • La mecánica de aplastamiento (Stomp) evalúa si el jugador tiene velocidad vertical descendente (vspd > 0) y si sus pies estaban por encima de la cabeza del enemigo antes del choque, destruyendo a la amenaza en vez de herir al avatar.
  • El parpadeo visual en el evento Draw mediante el operador módulo (%) ofrece una retroalimentación transparente e intuitiva sin alterar destructivamente el estado interno de las instancias.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Por qué se utiliza la función instance_place() en lugar de place_meeting() al detectar el impacto con el enemigo en el evento Step?

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

Ejercicio 2 · 1 punto

Si el juego se ejecuta a 60 FPS y la variable invincible_duration se configura en 90 fotogramas, ¿cuántos segundos dura la inmunidad temporal (i-frames) del jugador?

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

Ejercicio 3 · 1 punto

Ordena cronológicamente los bloques de código tal como deben ejecutarse dentro del evento Step de obj_player según la lección:

  1. 1Comprobar la colisión con el enemigo y aplicar daño o retroceso (si no es invencible)
  2. 2Aplicar la gravedad y física habitual
  3. 3Actualizar los temporizadores de inmunidad y de retroceso
  4. 4Leer la entrada de controles del jugador (si no está sufriendo retroceso)
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ingresa para guardar tu progreso — Ingresar