Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 5: Colisiones, gravedad y plataformas.

Colisiones horizontales con sólidos

Clase 22 de 60 · 60 min

También disponible en: EN

Introducción: El problema del movimiento y los sólidos

En las lecciones del Módulo 4 aprendiste a capturar las pulsaciones del teclado o mando y a traducirlas en velocidad horizontal mediante una variable como hsp (horizontal speed). Al final de cada ciclo del juego (step), aplicabas esa velocidad a la posición real de la instancia mediante una operación matemática directa: x += hsp. Si bien esta fórmula produce un desplazamiento suave e intuitivo en un espacio vacío, genera un problema inmediato en cuanto construyes un entorno jugable: las instancias no tienen masa ni consistencia física por defecto; son simplemente coordenadas cartesianas y gráficos proyectados sobre una pantalla.

Si una instancia se encuentra en la coordenada x = 100 con una velocidad hsp = 4, en el siguiente fotograma saltará a x = 104, luego a x = 108, y así sucesivamente. Si en x = 105 existe un muro de ladrillos de 16 píxeles de ancho, la matemática del juego trasladará a la instancia directamente a x = 108. Para el motor no ha ocurrido ninguna anomalía: simplemente se calculó una suma de valores enteros o flotantes. El resultado visual para el jugador es que el personaje se incrusta dentro de la pared o la atraviesa por completo (un fenómeno técnico conocido en el desarrollo de videojuegos como «tunneling» o traspaso por teletransporte discreto).

El objetivo de esta lección es transformar ese movimiento libre en un movimiento condicionado por el entorno. Aprenderás a interrogar al motor de colisiones de GameMaker antes de comprometer el cambio de posición en el eje horizontal. Si el espacio proyectado está libre, el personaje avanzará a su velocidad máxima; si hay un obstáculo intermedio, el personaje se detendrá de forma exacta, tocando la superficie del muro sin dejar huecos visibles y sin quedar atascado dentro de la geometría sólida.

Máscaras de colisión y cajas delimitadoras (Bounding Boxes)

Antes de escribir una sola línea de código para colisiones, es fundamental comprender qué compara GameMaker cuando evalúa si dos elementos del juego se tocan. Cada recurso de sprite en GameMaker posee no solo un mapa de píxeles visuales y un punto de origen, sino también una máscara de colisión (Collision Mask). La máscara delimita el área geométrica que interactúa con el mundo físico del juego.

Por defecto, GameMaker asigna a los sprites una máscara automática de tipo rectángulo que engloba todos los píxeles no transparentes de la imagen. Los límites de este rectángulo se identifican internamente mediante cuatro valores cardinales relativos a la sala (room): bbox_left, bbox_right, bbox_top y bbox_bottom. Cuando dos instancias colisionan, el motor compara el solapamiento entre sus respectivas cajas delimitadoras.

Propiedad internaEjeDescripción en el mundo del juego
bbox_leftHorizontal (X)Coordenada X del borde izquierdo de la máscara de colisión de la instancia.
bbox_rightHorizontal (X)Coordenada X del borde derecho de la máscara de colisión de la instancia.
bbox_topVertical (Y)Coordenada Y del borde superior de la máscara de colisión de la instancia.
bbox_bottomVertical (Y)Coordenada Y del borde inferior de la máscara de colisión de la instancia.

Para nuestro proyecto de plataformas retro, trabajaremos con dos objetos fundamentales: obj_player (el personaje controlado por el usuario) y obj_wall (el bloque sólido genérico que servirá para construir suelos, techos y paredes). Para garantizar colisiones estables y sin irregularidades, el sprite del muro (spr_wall) debe ser un cuadrado perfecto (por ejemplo, de 32x32 o 16x16 píxeles) con su máscara cubriendo la totalidad del gráfico (Full Image) y su origen colocado habitualmente en la esquina superior izquierda (Top Left, x: 0, y: 0).

💡 Importante: En el objeto obj_player, mantén la máscara de colisión configurada con valores manuales fijos o define una propiedad mask_index constante. Si la máscara cambia de tamaño entre la animación de reposo (idle) y la animación de carrera (run), el personaje podría cambiar de anchura en mitad de un movimiento y quedar atrapado dentro de un muro adyacente.

Detección proyectada: La función place_meeting

GameMaker ofrece varias funciones para detectar colisiones en GML Code, pero difieren drásticamente en su propósito y precisión. Los principiantes a menudo confunden position_meeting, instance_place y place_meeting. Para el movimiento físico de un personaje, la función idónea es place_meeting.

¿Qué hace exactamente place_meeting(x, y, obj)? Esta función toma la máscara de colisión de la instancia actual (la que ejecuta el código), la traslada virtualmente a las coordenadas (x, y) suministradas como argumento, y comprueba si en esa ubicación hipotética existe algún solapamiento con cualquier instancia del objeto especificado en obj. Devuelve true si hay colisión o false si el espacio está completamente despejado. Lo crucial es que la instancia no cambia de posición en la pantalla ni en la memoria; se trata de una comprobación preventiva o simulada en el futuro inmediato.

Diagrama técnico de detección de colisión horizontal que muestra la máscara actual del jugador, la máscara proyectada en x más hsp solapando el muro, y el ajuste píxel a píxel hasta hacer contacto.
Mecanismo de anticipación: la máscara se evalúa en la posición futura (x + hsp) antes de aplicar el desplazamiento real. · Imagen generada con IA

Comparemos brevemente las tres funciones principales para entender por qué place_meeting es la única herramienta adecuada para controlar el cuerpo del jugador:

Función GMLGeometría evaluadaRetornoUso principal recomendado
position_meeting(x, y, obj)Un único punto infinitesimal (x, y)Booleano (true / false)Clics de ratón, punteros o sensores puntuales.
place_meeting(x, y, obj)La máscara completa de la instanciaBooleano (true / false)Comprobación de movimiento, muros y suelos.
instance_place(x, y, obj)La máscara completa de la instanciaID de la instancia o nooneInteracciones donde necesitas modificar al otro objeto (dañar un enemigo, activar un interruptor específico).

El problema de la detención simple y la distancia residual

Un primer intento intuitivo para resolver las colisiones horizontales consiste en escribir una condición simple: si en la posición hacia la que voy hay un obstáculo, cancelo mi velocidad horizontal poniéndola a cero; de lo contrario, avanzo. Analicemos el siguiente código ingenuo:

// Intento ingenuo (NO recomendado):
if (place_meeting(x + hsp, y, obj_wall)) {
    hsp = 0;
}
x += hsp;

Aunque este código previene eficazmente que el personaje atraviese el muro, crea un defecto visual inaceptable en un juego comercial: el «gap» o espacio residual. Supongamos que tu personaje tiene una velocidad hsp = 4 y se encuentra a solo 2 píxeles de distancia de una pared situada a la derecha. Cuando el código evalúa place_meeting(x + 4, y, obj_wall), detecta que en x + 4 habría colisión. Como resultado, ejecuta hsp = 0 y no se mueve.

El personaje queda congelado a 2 píxeles del muro, flotando en el aire sin tocar visualmente la pared. Si la velocidad de carrera fuera 6, el jugador podría quedarse atascado a 1, 2, 3, 4 o 5 píxeles de distancia del obstáculo dependiendo de la cadencia de pasos previa. Para resolver este defecto y lograr un control pulido de nivel profesional, necesitamos un algoritmo de aproximación píxel a píxel.

La solución clásica: Aproximación píxel a píxel con sign() y while

El estándar de la industria para juegos de plataformas bidimensionales desarrollados con físicas personalizadas consiste en un proceso en dos fases dentro del evento Step: detección anticipada y resolución de contacto. Si determinamos que habrá un impacto en la posición x + hsp, no anulamos de golpe la velocidad sin antes cerrar la brecha existente entre la instancia y el obstáculo.

Para avanzar hacia el obstáculo sin pasarnos, nos movemos paso a paso, exactamente un píxel cada vez, en la dirección correcta. Para saber hacia dónde dar ese paso individual, recurrimos a una función matemática esencial de GameMaker: sign(val).

La función sign(n) evalúa el signo de un número real y devuelve únicamente uno de tres resultados posibles: 1 si el número es positivo, -1 si es negativo, y 0 si el número es exactamente cero. Por ejemplo:

Entrada (Valor de hsp)Resultado de sign(hsp)Dirección física representada
hsp = 4.51Desplazamiento unitario hacia la derecha (+1 en X)
hsp = -3-1Desplazamiento unitario hacia la izquierda (-1 en X)
hsp = 00Inmóvil (sin desplazamiento horizontal)

Con este concepto claro, si detectamos que place_meeting(x + hsp, y, obj_wall) es verdadero, iniciamos un bucle while. La condición del bucle será: «mientras NO haya una colisión a un solo píxel en mi dirección de movimiento (!place_meeting(x + sign(hsp), y, obj_wall)), avanza un píxel (x += sign(hsp))». En el instante en que el personaje queda perfectamente pegado a la pared, la condición se vuelve falsa, el bucle finaliza inmediatamente y establecemos hsp = 0.

Implementación paso a paso en el proyecto

Veamos ahora cómo integrar esta lógica de manera limpia y modular en nuestro proyecto de plataformas retro. Asegúrate de tener creados los recursos necesarios en el Asset Browser antes de proceder con el código.

1. Creación del recurso spr_wall: Crea un sprite de 32x32 píxeles con la apariencia de un bloque de piedra o ladrillo. Deja el origen en Top Left (x: 0, y: 0) y verifica en sus propiedades de Collision Mask que el modo sea «Automatic» y la forma «Rectangle».

2. Creación del objeto obj_wall: Crea un nuevo objeto, nómbralo obj_wall y asígnale el sprite spr_wall. No necesitas marcar la casilla «Solid» que incluye GameMaker en la interfaz de objetos; dicha casilla pertenece al sistema de físicas clásico de arrastrar y soltar (DnD/GML Visual heredado) y puede generar comportamientos erráticos con nuestro sistema manual de scripts.

3. Revisión del evento Create de obj_player: En el objeto de nuestro jugador, nos aseguramos de tener declaradas las variables de velocidad horizontal, aceleración y velocidad base calculadas en las lecciones anteriores.

// ====================================================================
// OBJETO: obj_player
// EVENTO: Create
// ====================================================================

// Variables de movimiento horizontal
hsp = 0;              // Velocidad horizontal actual en píxeles por fotograma
move_speed = 4;       // Velocidad horizontal máxima del jugador

// Variables de gravedad y verticales (de la lección anterior)
vsp = 0;              // Velocidad vertical actual
grav = 0.3;           // Aceleración de la gravedad
grav_max = 8;         // Velocidad terminal de caída

4. Estructuración del evento Step de obj_player: El evento Step debe organizarse en tres fases secuenciales estrictas: Lectura de entradas y cálculo de intenciones, Gestión y resolución de colisiones, y Aplicación final de coordenadas. El siguiente código muestra el bloque completo con la colisión horizontal integrada.

// ====================================================================
// OBJETO: obj_player
// EVENTO: Step
// ====================================================================

// --------------------------------------------------------------------
// FASE 1: ENTRADAS Y CÁLCULO DE VELOCIDAD
// --------------------------------------------------------------------
// Lectura del teclado (flechas o teclas A/D)
var _key_left  = keyboard_check(vk_left)  || keyboard_check(ord("A"));
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));

// Cálculo de la dirección neta (-1, 0, 1)
var _move = _key_right - _key_left;

// Determinación de la velocidad horizontal deseada
hsp = _move * move_speed;

// Aplicación preliminar de gravedad en el eje vertical (explicada en la lección 1)
vsp += grav;
if (vsp > grav_max) {
    vsp = grav_max;
}

// --------------------------------------------------------------------
// FASE 2: DETECCIÓN Y RESOLUCIÓN DE COLISIONES HORIZONTALES
// --------------------------------------------------------------------
// Verificamos si en la posición proyectada (x + hsp) hay contacto con obj_wall
if (place_meeting(x + hsp, y, obj_wall)) {
    // Si hay colisión global en ese trayecto, avanzamos píxel a píxel
    // usando sign(hsp) para movernos exactamente 1 píxel hacia el obstáculo
    while (!place_meeting(x + sign(hsp), y, obj_wall)) {
        x += sign(hsp);
    }
    
    // Una vez que el jugador está en contacto exacto (a 1 píxel de solaparse),
    // cancelamos por completo la velocidad horizontal para este fotograma
    hsp = 0;
}

// --------------------------------------------------------------------
// FASE 3: APLICACIÓN DEL MOVIMIENTO
// --------------------------------------------------------------------
// Aplicamos el desplazamiento horizontal resultante a la posición real
x += hsp;

// (Nota: La resolución vertical y la aplicación de vsp se profundizarán en la siguiente lección)
y += vsp;

Casos especiales, errores comunes y optimización

A medida que los juegos crecen en complejidad, el sistema básico de colisiones puede tropezar con situaciones singulares que causan comportamientos no deseados o caídas drásticas de rendimiento si no se previenen adecuadamente.

1. El riesgo de bucles infinitos con sign(0)

Si por alguna circunstancia hsp llegase a valer 0 pero el código entrase al bloque de colisión (por ejemplo, por modificaciones externas de posición o empujes de otros objetos), sign(0) devolvería 0. Si esto ocurriera dentro de un while que busca avanzar en la dirección sign(hsp), x += 0 no movería la instancia, la condición de colisión nunca cambiaría y el juego se congelaría por completo en un bucle infinito que colgaría el hilo de ejecución de GameMaker.

Para blindar el código contra fallos imprevistos, es una excelente práctica condicionar la comprobación horizontal a que la velocidad sea distinta de cero (hsp != 0), o bien asegurarse de que el valor absoluto de la velocidad sea mayor que un umbral mínimo:

if (hsp != 0) {
    if (place_meeting(x + hsp, y, obj_wall)) {
        while (!place_meeting(x + sign(hsp), y, obj_wall)) {
            x += sign(hsp);
        }
        hsp = 0;
    }
}

2. El uso de familias de colisión mediante Parents (Objetos Padres)

En un videojuego real, obj_player no solo colisionará con muros estáticos. También interactuará con puertas cerradas, bloques destructibles, plataformas metálicas y pilares de roca. Crear un bloque if (place_meeting(...)) independiente para cada tipo de bloque en el evento Step es una pésima práctica de arquitectura de software, ya que multiplica el consumo de CPU y vuelve el código insostenible.

La solución es la herencia mediante Parents en GameMaker. Creamos un objeto vacío llamado obj_solid o par_solid (sin sprite ni código). Luego, en cada objeto con solidez física (obj_wall, obj_block_wood, obj_iron_gate), abrimos sus propiedades y asignamos como Parent a obj_solid.

Gracias al sistema de herencia de GameMaker, place_meeting(x + hsp, y, obj_solid) comprobará simultáneamente el solapamiento con cualquier instancia de obj_solid o con cualquiera de sus objetos hijos, unificando toda la física horizontal en un único bloque de código limpio y eficiente.

3. Velocidades con decimales y precisión fraccionaria

Cuando implementamos aceleraciones o fricciones (por ejemplo, move_speed = 3.75), las variables de posición y velocidad acumulan fracciones decimales. En versiones modernas de GameMaker, place_meeting evalúa las coordenadas con precisión flotante redondeando internamente según el subsistema de renderizado. Sin embargo, si sumas x += sign(hsp) mientras x contiene un decimal (por ejemplo, x = 12.4), x pasará a 13.4, 14.4, etcétera.

En la mayoría de los casos de estética retro pixel-art, esto no causa problemas perceptibles si los muros están alineados en la rejilla de coordenadas enteras (Grid Snapping). No obstante, si notas que en ocasiones el personaje queda separado por una fracción minúscula de píxel, una técnica habitual consiste en redondear la posición al detenerse o utilizar un acumulador de fracciones (técnica que exploraremos en el capítulo de optimización de físicas).

Ejercicios prácticos

Para afianzar los conocimientos adquiridos en esta clase, realiza las siguientes tres actividades prácticas en tu proyecto de GameMaker.

Ejercicio 1: Verificación del espacio residual (El experimento del «Gap») En tu objeto obj_player, desactiva temporalmente el bucle while comentándolo con barras dobles (//), dejando únicamente el if (place_meeting(x + hsp, y, obj_wall)) { hsp = 0; }. Configura la velocidad de movimiento horizontal con un valor alto y poco común, como move_speed = 7. Ejecuta el juego, corre hacia un muro y observa detenidamente con qué frecuencia el personaje se detiene dejando un hueco visible entre su cuerpo y los ladrillos. Vuelve a habilitar el bucle while y comprueba cómo el contacto se vuelve milimétrico independientemente de la velocidad. Criterio de comprobación: Al restaurar el bucle while, el personaje debe tocar físicamente la textura del muro sin un solo píxel de separación visual, tanto al moverse a la izquierda como a la derecha.

Ejercicio 2: Creación de un bloque rompible usando Herencia (Parents) Crea un nuevo sprite llamado spr_crate de 32x32 píxeles con textura de caja de madera. Crea un objeto obj_crate y asígnale el sprite. Define un objeto maestro obj_solid y configura tanto a obj_wall como a obj_crate para que tengan a obj_solid como su «Parent». Modifica el código del evento Step de obj_player para que evalúe colisiones contra obj_solid en lugar de obj_wall. Criterio de comprobación: Coloca cajas y muros en tu room. El jugador debe detenerse de forma perfecta e idéntica ante ambos elementos sin haber añadido ninguna línea de código adicional para la caja.

Ejercicio 3: Dibujo del vector de colisión para depuración visual (Debug Visual) Abre el evento Draw de obj_player. Asegúrate de ejecutar draw_self() para que el sprite del personaje no desaparezca. Justo debajo, dibuja una línea o rectángulo semitransparente que represente hacia dónde está proyectando la colisión el jugador en cada instante utilizando draw_line_color(x, y, x + hsp, y, c_red, c_red). Criterio de comprobación: Al moverte hacia la izquierda o derecha en la sala, deberás ver una línea roja proyectándose desde el origen del personaje hacia el frente, indicando la longitud exacta de la comprobación horizontal anticipada.

Resumen

  • El movimiento horizontal directo (x += hsp) provoca solapamientos y traspaso de sólidos debido al avance discreto de las coordenadas fotograma a fotograma.
  • La función place_meeting(x, y, obj) proyecta la máscara de colisión de la instancia en una coordenada futura simulada sin alterar su posición real en la sala.
  • Una detención simple sin aproximación deja espacios vacíos visibles entre el personaje y el obstáculo cuando la velocidad es mayor a la distancia restante.
  • La función sign(hsp) normaliza cualquier velocidad en un vector unitario (-1, 0 o 1), lo que permite dar pasos individuales de exactamente un píxel.
  • El bucle while (!place_meeting(x + sign(hsp), y, obj_solid)) { x += sign(hsp); } garantiza un contacto píxel a píxel perfecto antes de anular hsp = 0.
  • El uso de objetos padre (Parents como obj_solid) permite agrupar muros, plataformas y obstáculos bajo una misma verificación de colisión modular y eficiente.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Qué defecto ocurre si se utiliza el enfoque ingenuo `if (place_meeting(x + hsp, y, obj_wall)) { hsp = 0; }` cuando el personaje se desplaza con `hsp = 4` y se encuentra a solo 2 píxeles de distancia de una pared?

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

Ejercicio 2 · 1 punto

Completa el algoritmo de aproximación píxel a píxel utilizado para resolver colisiones horizontales exactas frente a un muro: ```gml if (place_meeting(x + hsp, y, obj_wall)) { while (!place_meeting(x + (hsp), y, obj_wall)) { x += (hsp); } hsp = ; } x += hsp; ```

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

Ejercicio 3 · 1 punto

La función `place_meeting(x, y, obj)` traslada físicamente la instancia a las coordenadas indicadas, modificando su posición real en pantalla y en memoria durante la comprobación.

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

Ingresa para guardar tu progreso — Ingresar