Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 6: Animaciones, sprites, fondos y tilemaps.
Colisiones precisas con Tilemaps
Clase 30 de 60 · 60 min
También disponible en: EN
De las colisiones por instancias a las colisiones por Tilemap
Durante el Módulo 5 construimos las bases físicas de nuestro personaje: aplicamos gravedad, calculamos la aceleración horizontal, controlamos el salto e implementamos colisiones sólidas utilizando objetos como obj_bloque u obj_suelo mediante funciones como place_meeting(). Aquella técnica es ideal para comprender la lógica fundamental de los vectores de velocidad y los contactos entre cuerpos geométricos. Sin embargo, en el desarrollo profesional de videojuegos 2D de plataformas, llenar un nivel extenso con cientos o miles de instancias individuales de objetos sólidos presenta desventajas críticas.
Cada instancia en GameMaker consume memoria del sistema y ciclos de procesador en cada fotograma para gestionar sus eventos internos, variables de posición y transformaciones. Si un nivel mide 4000 píxeles de ancho y colocas bloques de 16x16 píxeles como suelo y paredes, podrías terminar con más de 2000 instancias activas únicamente para la geometría estática. En la lección anterior descubriste que los Tilemaps dibujan escenarios enteros combinando gráficos en una sola superficie ultraligera. En esta lección daremos el paso definitivo: utilizaremos esa misma estructura de celdas para calcular colisiones matemáticas de máxima precisión y rendimiento inmejorable sin crear un solo objeto sólido en el escenario.
💡 Nota sobre GML Visual: Aunque GameMaker ofrece bloques visuales para consultar datos de capas, en proyectos medianos y grandes el cálculo de colisiones por celda se implementa casi universalmente en GML Code. Esto se debe a la necesidad de verificar múltiples puntos exactos de la caja de colisión en una sola fracción de segundo con total control matemático.
¿Cómo entiende GameMaker un Tilemap a nivel de datos?
Un Tilemap no es una imagen plana e indivisible; es una cuadrícula bidimensional de datos numéricos. Cada celda de la cuadrícula almacena un número entero denominado índice de tile (tile index). La celda vacía siempre tiene el índice 0. Cualquier celda que contenga un gráfico extraído de tu Tileset tendrá un índice entero positivo (1, 2, 3, etc.), según su posición en el recurso gráfico original.
Para detectar si nuestro personaje está tocando un obstáculo, no necesitamos comprobar si colisiona con una forma física compleja. Solo necesitamos responder a una pregunta matemática directa: ¿la coordenada de píxel a la que se va a mover el personaje coincide con una celda cuyo índice sea diferente de 0? GameMaker nos proporciona una función interna de alto rendimiento diseñada específicamente para este fin: tilemap_get_at_pixel().
| Función / Variable | Tipo de retorno | Descripción y propósito |
|---|---|---|
| layer_get_id(layer_name) | Layer ID | Obtiene el identificador único de una capa en la Room a partir de su nombre en cadena de texto. |
| layer_tilemap_get_id(layer_id) | Tilemap ID | Localiza y devuelve el elemento Tilemap que reside dentro de la capa indicada. |
| tilemap_get_at_pixel(tilemap_id, x, y) | Entero (Real) | Devuelve el índice del tile presente en las coordenadas (x, y) de la Room. Devuelve 0 si está vacío o -1 si está fuera de rango. |
| bbox_left, bbox_right, bbox_top, bbox_bottom | Entero (Real) | Variables integradas de solo lectura que indican las coordenadas exactas de los cuatro bordes de la máscara de colisión de la instancia. |

Preparación de la Room y captura del Tilemap en código
Antes de programar el movimiento, debemos asegurarnos de que la capa del Tilemap esté correctamente identificada en la Room. Existen dos enfoques habituales en la industria: usar una capa de tiles invisible exclusivamente para colisiones (donde solo colocas bloques de color sólido para delimitar muros), o utilizar directamente la misma capa gráfica donde pintaste el terreno en la lección 6.4. Para mantener este curso sencillo y ordenado, usaremos la capa donde dibujaste el nivel, la cual llamaremos "Tiles_Terreno" en el inspector de capas de tu Room.
Para interactuar con el Tilemap desde el objeto del jugador (obj_jugador), necesitamos almacenar el identificador de ese Tilemap al momento de crearse el personaje. Si ejecutáramos la búsqueda de la capa en cada fotograma dentro del evento Step, estaríamos desperdiciando ciclos de CPU innecesariamente. La buena práctica dicta obtener la referencia una sola vez en el evento Create.
// ====================================================================
// RECURSO: Objetos -> obj_jugador
// EVENTO: Create
// ====================================================================
// Variables de movimiento y físicas (continuidad del Módulo 5)
vel_x = 0; // Velocidad horizontal actual
vel_y = 0; // Velocidad vertical actual
vel_caminar = 2.5; // Velocidad máxima al desplazarse
gravedad = 0.25; // Fuerza constante hacia abajo
fuerza_salto = -6.5; // Impulso inicial hacia arriba
en_suelo = false; // Bandera para saber si tocamos tierra firme
// Obtención del mapa de celdas para colisiones
// 1. Buscamos el ID de la capa mediante su nombre exacto en el Room Editor
var _id_capa = layer_get_id("Tiles_Terreno");
// 2. Extraemos el ID del elemento Tilemap contenido en dicha capa
mapa_colision = layer_tilemap_get_id(_id_capa);Con mapa_colision inicializado, obj_jugador ya dispone del acceso directo a la memoria del mapa. Ahora debemos revisar a fondo cómo evaluar los bordes del personaje.
El problema de los puntos de muestreo y la caja de colisión (AABB)
Una instancia en GameMaker se posiciona mediante sus coordenadas (x, y), las cuales dependen de dónde colocaste el origen del sprite (por ejemplo, abajo al centro). Si solo comprobaras tilemap_get_at_pixel(mapa_colision, x, y), estarías preguntando por un único punto microscópico en la base del personaje. Como resultado, la cabeza, los costados o los hombros del jugador atravesarían paredes y techos sin activar ninguna colisión.
Para evitar que el personaje se incruste en la geometría, debemos consultar las variables integradas de la máscara de colisión: bbox_left, bbox_right, bbox_top y bbox_bottom. Estas variables devuelven la posición mundial exacta en píxeles de los extremos de la caja delimitadora (Axis-Aligned Bounding Box o AABB).
Sin embargo, hay un detalle vital para personajes de más de 16 píxeles de altura o anchura: si un personaje mide 32 píxeles de alto y solo compruebas sus esquinas superior e inferior, una plataforma delgada de 16 píxeles de grosor situada a la altura de su cintura pasaría desapercibida entre ambos puntos. Por esta razón, cuando el sprite supere el tamaño de una celda del mosaico, es indispensable verificar un punto intermedio en los costados o calcular la colisión a lo largo de todo el segmento.
Estructuración de una función auxiliar de colisión por coordenadas
Para no repetir llamadas complejas en cada línea del código de movimiento, crearemos un Script que tome una coordenada (px, py) y nos indique con un valor booleano simple (true o false) si esa posición contiene un bloque sólido de terreno. Este enfoque modular hace que el código sea limpio y legible para cualquier miembro del equipo.
// ====================================================================
// RECURSO: Scripts -> scr_colisiones_tilemap
// ====================================================================
/// @function es_celda_solida(_mapa, _px, _py)
/// @description Retorna true si las coordenadas dadas tocan un tile sólido (> 0).
/// @param {Id.TileMapElement} _mapa Identificador del Tilemap.
/// @param {Real} _px Coordenada X en píxeles.
/// @param {Real} _py Coordenada Y en píxeles.
function es_celda_solida(_mapa, _px, _py) {
// Consultamos el índice del mosaico en la coordenada dada
var _indice = tilemap_get_at_pixel(_mapa, _px, _py);
// El índice 0 representa una celda vacía (aire o sin colisión).
// Todo valor mayor a 0 corresponde a un gráfico sólido de nuestro nivel.
return (_indice > 0);
}Con esta función general, podemos evaluar con absoluta sencillez si varios puntos simultáneos tocan un bloque. Por ejemplo, para verificar el lado derecho del personaje en una posición futura horizontal, podemos evaluar la esquina superior derecha, el punto medio derecho y la esquina inferior derecha.
Resolución de colisiones horizontales paso a paso
Al igual que hicimos en el sistema clásico del Módulo 5, nunca debemos modificar directamente la variable x sumando vel_x sin antes resolver posibles colisiones. La secuencia lógica para el eje horizontal se compone de cuatro pasos:
1. Determinar el borde que avanza: Si vel_x > 0 nos movemos hacia la derecha, por lo que el frente de contacto es bbox_right. Si vel_x < 0 nos movemos hacia la izquierda, por lo que el frente es bbox_left.
2. Predecir el contacto: Comprobamos si la posición futura del borde (bbox_right + vel_x o bbox_left + vel_x) coincide con celdas sólidas en la parte superior, media o inferior del cuerpo.
3. Ajuste fino de píxel (Snapping): Si se detecta un impacto inminente, no dejamos flotar al jugador a distancia del muro ni permitimos que se incruste. Mediante un bucle simple o alineación matemática con el tamaño del tile (16 píxeles), aproximamos al jugador píxel por píxel hasta hacer contacto exacto con la pared.
4. Detención: Asignamos vel_x = 0 para anular la inercia antes de actualizar la posición x final.
Resolución de colisiones verticales y detección de suelo
Para el eje vertical el procedimiento es análogo, pero incorpora la interacción con la gravedad y el estado en_suelo. Si vel_y > 0 el personaje está cayendo, lo que significa que el borde evaluado es bbox_bottom. Si el borde inferior toca una celda sólida al descender:
• Aproximamos al jugador hacia abajo hasta que sus pies reposen exactamente sobre la superficie del tile. • Establecemos vel_y = 0 para cancelar la fuerza de caída acumulada por la gravedad. • Cambiamos en_suelo = true, permitiendo que el jugador vuelva a ejecutar la acción de salto.
Por el contrario, si vel_y < 0 el personaje está ascendiendo en un salto y su borde superior (bbox_top) impacta contra un techo. En ese instante cancelamos vel_y poniéndola a 0 para que empiece a caer inmediatamente, evitando que la cabeza atraviese el techo.
Implementación completa en el evento Step de obj_jugador
A continuación presentamos la integración completa del sistema. Observa cómo se calculan las lecturas del teclado, la aceleración por gravedad, las colisiones por bordes contra el Tilemap y finalmente el desplazamiento definitivo de las coordenadas x e y.
// ====================================================================
// RECURSO: Objetos -> obj_jugador
// EVENTO: Step
// ====================================================================
// --------------------------------------------------------------------
// 1. CAPTURA DE ENTRADAS DEL TECLADO
// --------------------------------------------------------------------
var _tecla_izq = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _tecla_der = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _tecla_salto = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
// Dirección calculada (-1, 0, 1)
var _dir = _tecla_der - _tecla_izq;
vel_x = _dir * vel_caminar;
// --------------------------------------------------------------------
// 2. APLICACIÓN DE GRAVEDAD Y SALTO
// --------------------------------------------------------------------
vel_y += gravedad;
// Salto condicionado a estar en contacto con el suelo
if (en_suelo && _tecla_salto) {
vel_y = fuerza_salto;
en_suelo = false;
}
// Asumimos inicialmente que no estamos en el suelo hasta comprobarlo abajo
en_suelo = false;
// --------------------------------------------------------------------
// 3. COLISIÓN HORIZONTAL CON EL TILEMAP
// --------------------------------------------------------------------
if (vel_x != 0) {
// Definimos qué borde evaluar según la dirección del movimiento
var _borde_x = (vel_x > 0) ? bbox_right : bbox_left;
// Puntos verticales a evaluar en dicho borde (superior, centro e inferior)
// Restamos 1 al pie (bbox_bottom - 1) para evitar falsos positivos con el suelo
var _y_superior = bbox_top;
var _y_medio = (bbox_top + bbox_bottom) * 0.5;
var _y_inferior = bbox_bottom - 1;
// Verificamos si en la posición siguiente hay un tile sólido
var _colision_h = es_celda_solida(mapa_colision, _borde_x + vel_x, _y_superior) ||
es_celda_solida(mapa_colision, _borde_x + vel_x, _y_medio) ||
es_celda_solida(mapa_colision, _borde_x + vel_x, _y_inferior);
if (_colision_h) {
// Aproximación píxel a píxel hasta el muro
var _paso_h = sign(vel_x);
while (!es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_superior) &&
!es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_medio) &&
!es_celda_solida(mapa_colision, _borde_x + _paso_h, _y_inferior)) {
x += _paso_h;
_borde_x += _paso_h;
}
vel_x = 0;
}
}
// Aplicamos el movimiento horizontal resuelto
x += vel_x;
// --------------------------------------------------------------------
// 4. COLISIÓN VERTICAL CON EL TILEMAP
// --------------------------------------------------------------------
if (vel_y != 0) {
// Definimos qué borde vertical evaluar (cabeza o pies)
var _borde_y = (vel_y > 0) ? bbox_bottom : bbox_top;
// Puntos horizontales a evaluar a lo largo del ancho del personaje
var _x_izq = bbox_left;
var _x_centro = (bbox_left + bbox_right) * 0.5;
var _x_der = bbox_right;
// Comprobamos colisión en la posición vertical futura
var _colision_v = es_celda_solida(mapa_colision, _x_izq, _borde_y + vel_y) ||
es_celda_solida(mapa_colision, _x_centro, _borde_y + vel_y) ||
es_celda_solida(mapa_colision, _x_der, _borde_y + vel_y);
if (_colision_v) {
// Aproximación píxel a píxel hacia el obstáculo vertical
var _paso_v = sign(vel_y);
while (!es_celda_solida(mapa_colision, _x_izq, _borde_y + _paso_v) &&
!es_celda_solida(mapa_colision, _x_centro, _borde_y + _paso_v) &&
!es_celda_solida(mapa_colision, _x_der, _borde_y + _paso_v)) {
y += _paso_v;
_borde_y += _paso_v;
}
// Si íbamos cayendo y colisionamos, tocamos el suelo firme
if (vel_y > 0) {
en_suelo = true;
}
vel_y = 0;
}
}
// Aplicamos el movimiento vertical resuelto
y += vel_y;Fíjate con atención en un detalle técnico de vital importancia presente en el chequeo horizontal: cuando calculamos _y_inferior, usamos `bbox_bottom - 1` en lugar de `bbox_bottom`. Si evalúas el píxel exacto de bbox_bottom durante una comprobación horizontal mientras estás parado en el suelo, ese punto coincidirá con la superficie superior del bloque de tierra, interpretando erróneamente que una pared te bloquea el paso hacia adelante. Restar 1 píxel eleva el sensor justo por encima de la superficie de apoyo, garantizando que el personaje pueda caminar sin atascarse.
Alineación matemática directa por cuadrícula (Alternativa a los bucles)
El bucle while para el ajuste fino de píxeles (snapping) es sumamente intuitivo y muy seguro para velocidades estándar de juego. No obstante, en motores 2D profesionales existe una alternativa matemática directa: calcular exactamente en qué múltiplo de la cuadrícula comienza la celda colisionada. Dado que los tiles tienen un tamaño constante (por ejemplo, 16x16 píxeles), el borde superior de un bloque sólido siempre se encuentra en una coordenada múltiplo exacto de 16.
Por ejemplo, si un personaje cae hacia el suelo y su bbox_bottom penetraría en una celda, la posición ajustada Y donde debe reposar el origen del sprite puede calcularse restando la distancia relativa entre el origen y el borde inferior: `y = (tile_y * 16) - 1 - (bbox_bottom - y)`. Para quienes comienzan en GameMaker, el método de bucle while con paso de un píxel (`sign(vel)`) presentado arriba es el más confiable y resistente a fallos de redondeo de punto flotante.
Ventajas comparativas frente al sistema de objetos sólidos
Ahora que tienes ambos sistemas en tu caja de herramientas de desarrollo, resulta esencial entender cuándo emplear cada uno conforme avances hacia los módulos de enemigos y diseño de niveles:
| Característica | Colisión con Objetos (Módulo 5) | Colisión con Tilemaps (Módulo 6) |
|---|---|---|
| Consumo de memoria | Alto: requiere una estructura de instancia completa por cada bloque del nivel. | Casi nulo: los datos se leen directamente de un arreglo de bytes en memoria gráfica. |
| Rendimiento de CPU | Disminuye notablemente en niveles extensos con miles de instancias. | Constante y ultraveloz (O(1)), sin importar si el nivel tiene 100 o 10 000 celdas. |
| Facilidad de edición | Laborioso: requiere colocar y alinear manualmente objetos individuales en la Room. | Inmediato: se pinta directamente con la paleta de Tilesets usando pinceles y capas. |
| Uso recomendado | Plataformas móviles, puertas rompibles, interruptores y trampas dinámicas. | Geometría sólida estática: muros, pisos, techos y bordes perimetrales del nivel. |
Ejercicios prácticos de consolidación
Lleva a la práctica lo aprendido completando las siguientes tres actividades estructuradas sobre el proyecto del juego retro de plataformas que venimos construyendo:
Actividad 1: Configurar la capa de colisiones y recuperar el identificador. Abre la Room principal de tu proyecto. En el editor de capas, asegúrate de que tu capa de terreno se llame rigurosamente "Tiles_Terreno" (o el nombre que prefieras, respetando mayúsculas y minúsculas). En el evento Create de obj_jugador, añade una comprobación con show_debug_message() que imprima en la consola el valor de mapa_colision. Criterio de comprobación: Al pulsar F5 y ejecutar el juego, la ventana de salida debe mostrar un número entero mayor o igual a 0. Si muestra -1, la capa no existe o está mal escrita.
Actividad 2: Migración completa y eliminación de instancias sólidas redundantes. En la Room del juego, selecciona y elimina todas las instancias de obj_suelo u obj_bloque que habías colocado en el Módulo 5, dejando únicamente el arte pintado en la capa de mosaicos. Asegúrate de que el evento Step de obj_jugador tenga el código de colisión por Tilemap explicado en esta lección. Criterio de comprobación: El personaje debe caminar, saltar y detenerse contra paredes y pisos exactamente con la misma solidez que antes, pero ahora la pestaña de depuración mostrará una drástica reducción en la cantidad de instancias vivas.
Actividad 3: Detección y respuesta al chocar con el techo. Añade una plataforma suspendida en el aire con tus tiles a una altura alcanzable con un salto completo. Salta directamente debajo de ella para evaluar el comportamiento del sensor superior. Criterio de comprobación: El personaje debe interrumpir de golpe su ascenso en el instante exacto en que la cabeza toca el tile inferior del techo y comenzar a descender limpiamente por efecto de la gravedad, sin quedarse congelado ni atravesar el obstáculo.
Resumen
- Los Tilemaps permiten manejar la geometría estática de un nivel entero sin crear instancias individuales, lo que ahorra cantidades masivas de memoria y procesamiento.
- La función layer_tilemap_get_id(layer_get_id('NombreCapa')) permite vincular el código del jugador con la cuadrícula de mosaicos de la Room.
- tilemap_get_at_pixel(mapa, x, y) examina las coordenadas del mundo y devuelve el índice del tile; un valor mayor a 0 confirma la presencia de un elemento sólido.
- Para colisionar con fidelidad se deben comprobar los extremos de la caja de colisión (bbox_left, bbox_right, bbox_top, bbox_bottom), incluyendo puntos intermedios en objetos de mayor altura.
- Restar 1 píxel al borde inferior (bbox_bottom - 1) en las comprobaciones laterales previene que el jugador confunda el suelo sobre el que está parado con un muro horizontal frontal.
- La resolución de colisiones se ejecuta en dos fases independientes (horizontal y luego vertical), asegurando la aproximación píxel por píxel antes de actualizar las coordenadas de posición.
Practica lo aprendido
Ejercicio 1 · 1 punto
¿Cuál es el índice numérico entero que GameMaker asigna siempre a una celda vacía (sin gráfico ni colisión) dentro de la cuadrícula de un Tilemap?
Ejercicio 2 · 1 punto
¿Por qué es una buena práctica almacenar el identificador del Tilemap (`mapa_colision`) en el evento Create en lugar de buscarlo dentro del evento Step?
Ejercicio 3 · 1 punto
Ordena cronológicamente los 4 pasos explicados en la lección para resolver las colisiones horizontales antes de actualizar la posición x del personaje:
- 1Determinar el borde que avanza según la dirección de la velocidad (bbox_right o bbox_left).
- 2Predecir el contacto comprobando si la posición futura del borde coincide con celdas sólidas.
- 3Detener la inercia asignando vel_x = 0 antes de actualizar la posición final.
- 4Realizar el ajuste fino de píxel (snapping) aproximando al jugador hasta hacer contacto con la pared.
Ingresa para guardar tu progreso — Ingresar