Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 5: Colisiones, gravedad y plataformas.
Optimización del código de movimiento y colisiones
Clase 25 de 60 · 55 min
También disponible en: EN
Introducción a la refactorización y optimización en GameMaker
A lo largo de las cuatro lecciones anteriores de este módulo, construiste progresivamente el motor físico de tu personaje: primero incorporaste una aceleración gravitatoria constante, después implementaste la verificación de colisiones horizontales con obstáculos sólidos, luego resolviste la detención vertical ante suelos y techos, y finalmente integraste la bandera booleana para identificar si el personaje se encuentra apoyado de forma estable en el terreno. Si bien el personaje ya salta, corre y colisiona correctamente en el entorno de pruebas, es muy habitual que el código del evento Step comience a verse desorganizado, denso y propenso a errores al momento de querer agregar nuevas mecánicas.
Este fenómeno se conoce en el desarrollo de software como deuda técnica. Ocurre cuando implementamos características de manera secuencial y acumulativa, agregando líneas una tras otra sin replantear la estructura global. La refactorización es el proceso sistemático de reestructurar el código existente sin alterar su comportamiento externo visible para el jugador. En este capítulo no inventaremos mecánicas nuevas desde la perspectiva del usuario; en su lugar, tomaremos todo el sistema de movimiento y colisiones construido en el objeto del jugador (obj_player) y lo transformaremos en una solución modular, legible, optimizada y lista para escalar hacia las animaciones complejas, enemigos y plataformas móviles que exploraremos en los siguientes módulos.
Diagnóstico del código actual: Identificando la deuda técnica
Para optimizar con éxito, el primer paso consiste en realizar una autopsia constructiva del código que hemos reunido hasta este punto. Generalmente, un desarrollador principiante concentra toda la lógica física directamente en el evento Step de obj_player. Al abrir ese evento, encontramos una secuencia continua de captura de teclado, cálculos matemáticos, aplicaciones de gravedad, dos bucles while independientes para aproximación píxel a píxel y comprobaciones auxiliares de contacto con el suelo.
Examinemos cómo luce típicamente este bloque acumulado antes de aplicar criterios de ingeniería de software:
// --- ESTADO PREVIO (SIN REFACTORIZAR) EN obj_player: Evento Step ---
// 1. Captura de teclas
var _key_left = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _key_jump = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
// 2. Cálculo de movimiento horizontal
var _move = _key_right - _key_left;
hsp = _move * walk_speed;
// 3. Aplicación de gravedad
vsp = vsp + grv;
// 4. Salto
if (place_meeting(x, y + 1, obj_solid))
{
if (_key_jump)
{
vsp = jump_speed;
}
}
// 5. Colisión Horizontal
if (place_meeting(x + hsp, y, obj_solid))
{
while (!place_meeting(x + sign(hsp), y, obj_solid))
{
x = x + sign(hsp);
}
hsp = 0;
}
x = x + hsp;
// 6. Colisión Vertical
if (place_meeting(x, y + vsp, obj_solid))
{
while (!place_meeting(x, y + sign(vsp), obj_solid))
{
y = y + sign(vsp);
}
vsp = 0;
}
y = y + vsp;
// 7. Estado en el suelo
is_grounded = place_meeting(x, y + 1, obj_solid);Aunque funcional, este script monolítico presenta serios problemas de mantenimiento. En primer lugar, mezcla múltiples responsabilidades en un solo archivo plano: la entrada de usuario, las leyes del mundo físico, la resolución espacial geométrica y el estado del actor ocurren revueltos. En segundo lugar, existe redundancia: realizamos la comprobación place_meeting(x, y + 1, obj_solid) en el paso del salto y volvemos a evaluar una llamada idéntica al final del script para actualizar la variable is_grounded. Cada llamada a funciones de colisión requiere cómputo geométrico por parte del motor; duplicarlas sin necesidad desperdicia ciclos de CPU.
En tercer lugar, si más adelante decidimos incorporar un enemigo que camina y obedece a la gravedad, nos veríamos tentados a copiar y pegar todo el bloque de colisiones en el objeto enemigo. Duplicar código es uno de los mayores errores en el desarrollo de videojuegos: si descubres un fallo en el cálculo de colisiones o deseas ajustar cómo los cuerpos interactúan con esquinas complejas, tendrías que modificar cada objeto individualmente en lugar de actualizar un único componente centralizado.
Separación de responsabilidades: Funciones y modularidad
El principio de responsabilidad única establece que cada módulo o función debe encargarse de una tarea específica y bien delimitada. En un videojuego de plataformas bidimensional, el ciclo de vida del personaje en cada fotograma puede dividirse claramente en cuatro fases ordenadas y secuenciales: primero, recolectar las intenciones del jugador (captura de controles); segundo, calcular las fuerzas e impulsos deseados (física y aceleraciones); tercero, resolver las restricciones espaciales impuestas por el escenario (colisiones con sólidos); y cuarto, actualizar la posición definitiva y la representación visual (sprites y animaciones).

En GameMaker moderno existen dos formas canónicas de modularizar nuestra lógica mediante GML Code: a través de funciones globales declaradas dentro de un recurso Script en el Asset Browser, o mediante métodos locales asignados como variables dentro del evento Create del propio objeto. Para la lógica exclusiva del jugador que gestiona entradas y estados internos, los métodos de instancia resultan sumamente limpios y encapsulados. Sin embargo, para la resolución física de colisiones contra sólidos, una función modular alojada en un Script resulta superior, ya que nos permitirá reutilizar exactamente la misma matemática tanto para el protagonista como para aliados, enemigos o proyectiles físicos.
Creación del Script de colisiones reutilizable
Vamos a crear nuestro primer script de utilidad física general. En el Asset Browser de GameMaker, haz clic derecho sobre la carpeta Scripts (o en el espacio raíz si aún no organizas por carpetas) y selecciona Create -> Script. Nombra a este recurso como scr_physics_utilities.
Dentro de este script, escribiremos una función global con sintaxis moderna de GML denominada player_collision_and_move. Esta función no solo concentrará los algoritmos de detección de impacto en ambos ejes cardinales, sino que también aplicará el desplazamiento efectivo a las coordenadas de la instancia que la invoque, devolviendo un código limpio y consistente.
/// @function player_collision_and_move(_collision_object)
/// @description Resuelve colisiones horizontales y verticales contra un objeto sólido y actualiza la posición.
/// @param {Asset.GMObject} _collision_object El objeto contra el cual comprobar el impacto.
function player_collision_and_move(_collision_object)
{
// --- RESOLUCIÓN HORIZONTAL ---
if (place_meeting(x + hsp, y, _collision_object))
{
// Avanzamos píxel a píxel hacia el borde exacto de la colisión
while (!place_meeting(x + sign(hsp), y, _collision_object))
{
x += sign(hsp);
}
// Al hacer contacto directo, anulamos el impulso lateral acumulado
hsp = 0;
}
// Aplicamos el desplazamiento horizontal resultante en el eje X
x += hsp;
// --- RESOLUCIÓN VERTICAL ---
if (place_meeting(x, y + vsp, _collision_object))
{
// Avanzamos píxel a píxel hacia la superficie de parada (suelo o techo)
while (!place_meeting(x, y + sign(vsp), _collision_object))
{
y += sign(vsp);
}
// Al hacer contacto directo, anulamos la velocidad de caída o ascenso
vsp = 0;
}
// Aplicamos el desplazamiento vertical resultante en el eje Y
y += vsp;
}Analicemos por qué esta estructura es técnicamente sólida. La función recibe el parámetro _collision_object. Esto significa que no dependemos rígidamente de obj_solid: en fases más avanzadas del proyecto podríamos pasar como argumento un objeto padre que englobe bloques de piedra, puertas cerradas o plataformas rompibles, manteniendo intacto el algoritmo de movimiento.
Estructuración del evento Create: Inicialización impecable
Un error común al programar en GameMaker es declarar variables sobre la marcha en el evento Step. Para asegurar un rendimiento óptimo de memoria y evitar advertencias del compilador, todas las variables que definen el comportamiento de obj_player deben declararse explícitamente en el evento Create. Además, organizaremos estas variables por categorías semánticas mediante comentarios bien definidos para que cualquier miembro del equipo pueda comprender el propósito de cada magnitud de inmediato.
// --- obj_player: Evento Create ---
// 1. Constantes y parámetros de configuración de movimiento
walk_speed = 3.5; // Velocidad horizontal base en píxeles por fotograma
grv = 0.35; // Constante gravitacional aplicada cada fotograma
jump_speed = -7.5; // Impulso inicial ascendente (negativo hacia arriba)
max_fall_speed = 9.0; // Velocidad terminal de caída para evitar atravesar suelos
// 2. Variables dinámicas de velocidad en tiempo real
hsp = 0.0; // Velocidad horizontal actual acumulada
vsp = 0.0; // Velocidad vertical actual acumulada
// 3. Variables de estado físico
is_grounded = false; // Indica si el personaje está físicamente sobre suelo sólido
// 4. Variables de lectura de entrada (inicializadas por consistencia)
key_left = false;
key_right = false;
key_jump = false;
// 5. Métodos locales de comportamiento del jugador
get_input = function()
{
key_left = keyboard_check(vk_left) || keyboard_check(ord("A"));
key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
key_jump = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
};
calculate_movement = function()
{
// Dirección resultante del movimiento horizontal (-1, 0, o 1)
var _move_direction = key_right - key_left;
hsp = _move_direction * walk_speed;
// Aplicación de gravedad limitada por la velocidad terminal
if (vsp < max_fall_speed)
{
vsp += grv;
}
else
{
vsp = max_fall_speed;
}
// Ejecución de salto condicionado exclusivamente a estar en suelo firme
if (is_grounded && key_jump)
{
vsp = jump_speed;
is_grounded = false;
}
};Observa con atención cómo hemos introducido la variable max_fall_speed. En un juego real, si un personaje cae desde una altura monumental sin un tope de velocidad terminal, vsp continuará aumentando indefinidamente debido a la gravedad constante. Si en un solo fotograma vsp alcanzara un valor desproporcionado (por ejemplo, 40 píxeles por fotograma), la comprobación espacial podría saltarse geometrías delgadas o exigir decenas de ciclos dentro del bucle while para aproximar al personaje, provocando microtirones de procesamiento. Delimitar la velocidad terminal no solo es realista en términos físicos, sino que garantiza que los bucles de colisión nunca consuman más tiempo de ejecución del previsto.
El nuevo evento Step: Un orquestador limpio y conciso
Con los métodos auxiliares declarados en el Create y la función física ubicada en el Script, el evento Step de obj_player deja de ser una lista enredada de operaciones aritméticas y se transforma en un orquestador legible de nivel ejecutivo. Cualquier programador que revise este archivo podrá entender la totalidad del flujo del personaje en apenas diez segundos de lectura.
// --- obj_player: Evento Step (Refactorizado) ---
// Fase 1: Recolección de entradas del jugador
get_input();
// Fase 2: Cálculo de intenciones físicas y aplicación de aceleración
calculate_movement();
// Fase 3: Resolución de colisiones y traslación en el mundo con sólidos
player_collision_and_move(obj_solid);
// Fase 4: Actualización del estado de apoyo para el siguiente fotograma
is_grounded = place_meeting(x, y + 1, obj_solid);Compara mentalmente esta versión con la versión inicial. Si surge un error en los controles del teclado, te diriges directamente al método get_input en el evento Create. Si el personaje no se detiene correctamente ante un muro, inspeccionas scr_physics_utilities. Si deseas cambiar las propiedades de peso o altura de salto, modificas los parámetros numéricos de Create sin riesgo alguno de desconfigurar la matemática de impacto. Cada pieza de software reside en su lugar legítimo.
Optimización matemática y consideraciones de precisión en GameMaker
Al trabajar con números de coma flotante (números con decimales como 3.5 o 0.35) en GameMaker, surgen detalles técnicos que todo desarrollador debe dominar para evitar anomalías visuales y de colisión. Examinemos las mejores prácticas para mantener la física precisa y la pantalla nítida:
En primer lugar, está la disyuntiva entre place_meeting e instance_place. La función place_meeting(x, y, obj) únicamente evalúa si existe una intersección geométrica entre las máscaras de colisión, devolviendo un booleano (true o false). Por el contrario, instance_place(x, y, obj) debe identificar, buscar en la lista de instancias activas de la sala y devolver el identificador único (id) de la instancia concreta con la que chocamos. Ejecutar instance_place dentro de un bucle while consume una cantidad significativamente mayor de recursos. Para bloquear el movimiento ante paredes y suelos estándar, place_meeting es siempre la herramienta matemáticamente adecuada y óptima.
En segundo lugar, se encuentra el fenómeno del 'temblor' o 'jitter' sub-pixel. En videojuegos con estética retro y gráficos pixel art de baja resolución (como 320x180 o 640x360 escalados en monitores modernos), mover las variables x e y con valores fraccionarios puede hacer que GameMaker dibuje el sprite a caballo entre dos píxeles de pantalla, generando un artefacto donde el sprite parpadea o se desdibuja ligeramente. La solución correcta no es redondear las variables x e y en la física —hacerlo destruiría la suavidad de las aceleraciones y caídas—, sino delegar el redondeo visual exclusivamente al momento de dibujar.
// --- obj_player: Evento Draw (Opcional para Pixel Art perfecto) ---
// En lugar de dejar que GameMaker dibuje en coordenadas fraccionarias por defecto,
// forzamos el renderizado en la posición redondeada más cercana:
draw_sprite_ext(
sprite_index,
image_index,
floor(x),
floor(y),
image_xscale,
image_yscale,
image_angle,
image_blend,
image_alpha
);Al usar floor(x) y floor(y) únicamente dentro de la función de dibujado, la física del motor continúa operando con absoluta precisión matemática de punto flotante en segundo plano, mientras que los ojos del jugador perciben una imagen nítida que calza de manera inmaculada con la rejilla de píxeles del mundo.
Tabla comparativa de patrones de diseño de código
La siguiente tabla resume los patrones comúnmente utilizados por principiantes frente a las soluciones limpias y profesionales recomendadas para el desarrollo continuo de videojuegos 2D:
| Aspecto | Antipatrón (Código desordenado) | Solución Refactorizada (Código profesional) | Beneficio directo |
|---|---|---|---|
| Estructura del Step | Decenas de líneas seguidas con lógica de entrada, cálculo y bucles mezclados. | Fases delimitadas llamadas a través de métodos concisos (get_input, calculate_movement). | Claridad absoluta; lectura rápida del flujo principal del fotograma. |
| Detección de Colisiones | Código duplicado en cada objeto interactivo (jugador, enemigos, proyectiles). | Función modularizada en un Script independiente reutilizable por parámetros. | Mantenimiento centralizado: corregir un fallo una sola vez lo resuelve para todos. |
| Control de Velocidad | Aceleración por gravedad infinita sin cota máxima de caída. | Variable de velocidad terminal (max_fall_speed) que acota el incremento de vsp. | Previene bloqueos en bucles while y el traspaso inadvertido de suelos delgados. |
| Declaración de Variables | Variables creadas dentro de sentencias if o en mitad del bucle de ejecución. | Declaración formal y tipificada en el evento Create con valores iniciales limpios. | Eliminación de advertencias de compilación y mayor consistencia de memoria. |
| Dibujado en Pixel Art | Redondeo forzado de las variables físicas x e y dentro del Step. | Física con decimales intacta y redondeo posicional únicamente en el evento Draw. | Movimiento suave y orgánico sin artefactos de distorsión visual en los sprites. |
Perspectiva de herramientas: GML Code vs GML Visual
Es importante mencionar que en GameMaker puedes construir videojuegos utilizando tanto GML Code (programación escrita) como GML Visual (sistema de bloques interactivos de lógica visual). En GML Visual, este proceso de modularización se implementa mediante el bloque 'Declare Function' y el bloque 'Execute Script'. No obstante, cuando los algoritmos de colisión involucran comprobaciones iterativas de aproximación píxel a píxel mediante bucles while anidados con cálculos vectoriales sign(), la representación visual mediante bloques suele ocupar una cantidad masiva de espacio en pantalla, volviéndose engorrosa de auditar. GML Code proporciona una densidad de información, precisión sintáctica y velocidad de refactorización incomparablemente superior para sistemas físicos de plataformas, razón por la cual es el estándar de la industria indie contemporánea.
💡 Regla de oro de la optimización: Nunca optimices a ciegas sin necesidad demostrada. Usar bucles while para aproximar colisiones píxel a píxel es perfectamente seguro y eficiente siempre y cuando las velocidades máximas estén debidamente acotadas mediante una velocidad terminal razonable (por ejemplo, entre 8 y 12 píxeles por fotograma).
Ejercicios prácticos de consolidación
Pon a prueba tus habilidades de refactorización y robustecimiento de código implementando las siguientes tres actividades estructuradas sobre tu proyecto actual.
Resumen
- La refactorización transforma código funcional pero desorganizado en una arquitectura limpia, legible y fácilmente ampliable sin alterar el resultado visible del juego.
- Separar responsabilidades en fases secuenciales (captura de entrada, cálculo de fuerzas, resolución de colisiones y actualización de estados) previene la aparición de errores cruzados.
- Los métodos declarados en el evento Create permiten encapsular comportamientos propios del objeto de forma modular y prolija.
- Las funciones alojadas en recursos Script desacoplan la física del objeto, permitiendo que jugadores, enemigos y objetos caigan y choquen usando exactamente el mismo algoritmo.
- Implementar límites como una velocidad terminal de caída (max_fall_speed) optimiza el uso de CPU y previene que los bucles de colisión fallen o atraviesen paredes.
- Para gráficos Pixel Art, conservar los decimales en la física y redondear las coordenadas mediante floor() exclusivamente en el evento Draw evita distorsiones y vibraciones de pantalla.
- Con el motor de física y colisiones completamente saneado y optimizado, el proyecto cuenta con la base técnica requerida para abordar la gestión de animaciones por estados en el siguiente módulo.
Practica lo aprendido
Ejercicio 1 · 1 punto
Ordena cronológicamente las cuatro fases secuenciales en las que se divide el ciclo de vida del personaje en cada fotograma dentro del juego, según el principio de responsabilidad única:
- 1Calcular las fuerzas e impulsos deseados (física y aceleraciones)
- 2Resolver las restricciones espaciales impuestas por el escenario (colisiones con sólidos)
- 3Actualizar la posición definitiva y la representación visual (sprites y animaciones)
- 4Recolectar las intenciones del jugador (captura de controles)
Ejercicio 2 · 1 punto
En la función modular player_collision_and_move(_collision_object), ¿cuál es la ventaja de recibir _collision_object como parámetro en lugar de utilizar directamente obj_solid?
Ejercicio 3 · 1 punto
En el desarrollo de software, la acumulación desorganizada de código al agregar características sin replantear su estructura global se conoce como . Por su parte, el proceso sistemático de reestructurar el código existente sin alterar su comportamiento externo visible para el jugador se denomina .
Ingresa para guardar tu progreso — Ingresar