Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 4: Controles, movimientos y manejo del jugador.
Refinamiento de controles y variables de movimiento
Clase 20 de 60 · 55 min
También disponible en: EN
La importancia del «Game Feel» y la precisión en el control
En el desarrollo de videojuegos de plataformas en dos dimensiones, la diferencia entre una experiencia frustrante y una gratificante rara vez radica en los gráficos o en la complejidad del argumento; reside casi en su totalidad en lo que en la industria se conoce como «game feel» (la sensación de juego) o respuesta de control. Cuando un jugador pulsa un botón en su teclado o mando, espera una respuesta inmediata, predecible y justa. Si el personaje tarda una fracción de segundo de más en detenerse, si no responde a un salto por haber pulsado una milésima de segundo antes de tocar el suelo, o si la trayectoria en el aire resulta rígida e inalterable, el jugador no culpará a sus propios reflejos: culpará al juego.
Hasta este punto del módulo, has construido las bases indispensables: capturar las entradas del teclado y gamepad, calcular la dirección horizontal, invertir visualmente al personaje mediante la escala del sprite y aplicar un impulso vertical simple para despegarse del suelo. No obstante, un movimiento con velocidad instantánea pura (donde el personaje pasa de 0 a máxima velocidad en un solo fotograma y se detiene en seco) puede resultar tosco o demasiado plano para ciertos estilos de juego, mientras que un salto de altura fija impide que el jugador calibre sus trayectorias para esquivar obstáculos a diferentes alturas.
El objetivo de esta lección es refinar esas mecánicas rudimentarias. Aprenderás a estructurar variables de ajuste milimétrico para la aceleración y la fricción, a permitir que la altura del salto dependa directamente del tiempo durante el cual el usuario mantenga pulsada la tecla (salto variable), y a incorporar dos técnicas de tolerancia fundamentales del diseño moderno: el tiempo coyote y el buffer de salto.
💡 Aunque en GameMaker existe la alternativa de programación visual denominada GML Visual (basada en bloques de acciones e iconos que encajan entre sí), a lo largo de este curso continuaremos utilizando GML Code. Escribir código textual directamente proporciona un control absoluto sobre el orden matemático de ejecución y simplifica de forma extraordinaria el ajuste fino de fórmulas físicas.
Inercia controlada: Aceleración y fricción horizontal
En la lección anterior sobre movimiento horizontal, asignamos directamente la velocidad deseada multiplicando la dirección por la velocidad máxima: cuando el jugador pulsaba la tecla derecha, la variable horizontal pasaba instantáneamente de cero a, por ejemplo, cuatro píxeles por fotograma. Para un control de estilo puramente arcade, esto ofrece reactividad inmediata, pero a menudo carece de peso o fluidez orgánica.
Para dotar al personaje de una transición suave sin sacrificar la agilidad, recurrimos a dos variables complementarias: aceleración y fricción. La aceleración determina cuántos píxeles por fotograma añadimos a la velocidad actual hasta alcanzar el límite máximo; la fricción determina cuántos píxeles por fotograma sustraemos a dicha velocidad cuando el jugador no presiona ninguna dirección, hasta hacerla regresar exactamente a cero.
Un error común entre principiantes consiste en aplicar restas directas para la fricción sin verificar si el valor cruza el cero. Por ejemplo, si el personaje se mueve a una velocidad de 0.2 hacia la derecha y aplicamos una fricción de 0.5 hacia la izquierda, la velocidad resultante sería -0.3, provocando que el personaje vibre o retroceda infinitamente hacia el lado contrario. Para evitar este problema, programamos una función de aproximación matemática o empleamos comparaciones lógicas precisas con funciones estándar de GML.
Variables de configuración horizontal en el evento Create
Abrimos nuestro objeto principal del jugador, `obj_player`, y accedemos a su evento **Create**. Vamos a reorganizar y enriquecer las variables de desplazamiento que creamos en lecciones previas para permitir una calibración cómoda desde un único punto del proyecto.
/// Evento Create de obj_player
// --- VARIABLES DE MOVIMIENTO HORIZONTAL ---
vel_x = 0; // Velocidad horizontal actual en píxeles por fotograma
vel_x_max = 3.5; // Velocidad horizontal máxima alcanzable
acel_x = 0.4; // Cuánto incrementa la velocidad por cada fotograma de pulsación
fric_x = 0.35; // Cuánto se reduce la velocidad por fotograma al soltar el control
// --- VARIABLES DE MOVIMIENTO VERTICAL Y GRAVEDAD ---
vel_y = 0; // Velocidad vertical actual
vel_y_max = 8.0; // Límite de caída terminal
gravedad = 0.35; // Fuerza constante hacia abajo
vel_salto = -6.5; // Impulso inicial de salto (valor negativo hacia arriba)Observa con atención los valores numéricos: `acel_x` está configurado en 0.4 y `fric_x` en 0.35. A una tasa de refresco estándar de 60 fotogramas por segundo, un incremento de 0.4 significa que el personaje tardará menos de diez fotogramas (aproximadamente una sexta parte de segundo) en pasar del reposo absoluto a su velocidad tope de 3.5. Esto confiere una sutil sensación de aceleración natural sin que el jugador sienta pesadez o retraso involuntario («input lag»).
Cálculo de la aceleración y desaceleración en el evento Step
Dentro del evento **Step** de `obj_player`, reemplazaremos la asignación directa de velocidad horizontal por un bloque condicional que verifique si existe o no intención de movimiento por parte del usuario.
/// Fragmento del evento Step de obj_player: Gestión horizontal
// 1. Obtención de entrada direccional (-1 para izquierda, 1 para derecha, 0 si está quieto)
var _tecla_der = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _tecla_izq = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _dir = _tecla_der - _tecla_izq;
// 2. Modificación de la velocidad horizontal mediante aceleración o fricción
if (_dir != 0) {
// El jugador desea moverse: aceleramos hacia la dirección deseada
vel_x += _dir * acel_x;
// Aseguramos que la velocidad no sobrepase el tope permitido
vel_x = clamp(vel_x, -vel_x_max, vel_x_max);
} else {
// El jugador soltó los controles: aplicamos fricción para detenerlo
if (vel_x > 0) {
vel_x = max(0, vel_x - fric_x);
} else if (vel_x < 0) {
vel_x = min(0, vel_x + fric_x);
}
}Analicemos la función nativa `clamp(val, min, max)` utilizada en el código. Esta función devuelve el valor evaluado asegurando que jamás sea inferior a `min` ni superior a `max`. En nuestro caso, impide que la suma continua de `acel_x` acelere al personaje de manera desmedida. En el bloque de fricción, `max(0, vel_x - fric_x)` garantiza que, al restar fricción a una velocidad positiva, esta nunca descienda por debajo de cero, evitando así el temblor o rebote inverso.
Salto variable: Adaptabilidad en el aire
En clásicos como Super Mario Bros., Celeste o Mega Man, la altura del salto depende de la duración de la pulsación: un toque breve genera un salto corto («hop») para evitar techos bajos o aterrizar sobre un saliente próximo, mientras que mantener el botón pulsado impulsa al personaje hasta el punto más elevado de su parábola. Si el salto tuviera siempre la misma altura fija, los jugadores perderían una enorme cantidad de opciones tácticas al diseñar rutas a través de las plataformas.
La manera matemática más elegante y reactiva de lograr este comportamiento sin alterar la gravedad base consiste en interrumpir la inercia ascendente en el instante exacto en que el jugador suelta el botón. Cuando soltamos la tecla de salto y el personaje todavía se está desplazando hacia arriba (es decir, su velocidad vertical `vel_y` es negativa), multiplicamos dicha velocidad por una fracción decimal (por ejemplo, 0.4 o 0.5), o bien la truncamos directamente a un umbral menor.

Implementación del corte de salto en GML
Para integrar este comportamiento, primero definimos una variable de proporción en el evento **Create** de `obj_player`, que representará cuánto impulso conservamos si se interrumpe la pulsación:
/// Añadido al evento Create de obj_player
multiplicador_corte_salto = 0.45; // Porcentaje de velocidad vertical conservado al soltar el botónPosteriormente, en el evento **Step**, gestionamos la lectura de la liberación de la tecla mediante `keyboard_check_released()`:
/// Fragmento del evento Step de obj_player: Salto variable
// Detección de liberación de las teclas de salto habituales
var _solto_salto = keyboard_check_released(vk_space) || keyboard_check_released(ord("W")) || keyboard_check_released(vk_up);
// Si el jugador suelta el botón mientras el personaje asciende, amortiguamos la subida
if (_solto_salto && vel_y < 0) {
vel_y = vel_y * multiplicador_corte_salto;
}Fíjate en la condición `vel_y < 0`. En los ejes de coordenadas 2D de GameMaker, el eje Y crece hacia abajo; por consiguiente, una velocidad negativa implica que el cuerpo se desplaza hacia la parte superior de la pantalla. Si el jugador suelta el botón mientras el personaje ya está cayendo de vuelta hacia el suelo (`vel_y >= 0`), la condición se evalúa como falsa y la gravedad regular sigue operando sin alteraciones abruptas.
Tolerancias de control: Tiempo Coyote y Buffer de salto
Los monitores modernos y los seres humanos tienen pequeñas demoras en milisegundos: tiempo de reacción física, tasa de actualización de la pantalla y el ciclo de refresco del motor de juego. Esto genera dos escenarios muy comunes que provocan una enorme sensación de frustración en los jugadores noveles de plataformas si no se corrigen en el código:
| Problema de ejecución habitual | Causa técnica en el motor | Solución de diseño de control |
|---|---|---|
| El jugador camina sobre una plataforma, sale por el borde y pulsa saltar justo al caer al vacío, pero el personaje no salta y se precipita. | El personaje dejó de colisionar con el suelo uno o dos fotogramas antes de procesar el botón de salto; por tanto, el motor considera que ya está en el aire. | Tiempo Coyote (Coyote Time): otorgar un margen de gracia de varios fotogramas tras abandonar el suelo en el que el salto sigue siendo válido. |
| El jugador está descendiendo hacia una plataforma y pulsa el botón de salto justo antes de aterrizar, pero al tocar el suelo se queda quieto. | La comprobación `keyboard_check_pressed` se produjo en el aire y expiró de inmediato; al llegar al suelo el botón ya no está recién presionado. | Buffer de entrada (Jump Buffering): registrar la intención de saltar durante unos pocos fotogramas previos al contacto y ejecutar el salto al tocar tierra. |
El término «Tiempo Coyote» rinde homenaje al clásico dibujo animado del Correcaminos, en el que el Coyote corre más allá del precipicio y permanece suspendido en el aire durante un breve instante antes de desplomarse. En videojuegos, este margen suele configurarse entre 4 y 8 fotogramas (entre 60 y 130 milisegundos a 60 FPS). Es un intervalo prácticamente imperceptible a simple vista, pero elimina por completo la sensación de que el juego «no detectó mi botón».
Temporizadores en GameMaker sin alarmas: Contadores por fotograma
Aunque GameMaker cuenta con un sistema de alarmas nativo (`alarm[0]`), para la física de personajes y la entrada de controles resulta mucho más fiable y legible utilizar variables numéricas enteras decrementadas en cada fotograma dentro del evento **Step**. Esto mantiene toda la lógica de control contenida y sincronizada en el mismo ciclo de ejecución.
Añadiremos los contadores en el evento **Create** de `obj_player`:
/// Nuevas variables en el evento Create de obj_player para tolerancias
// --- BUFFER DE SALTO ---
tiempo_buffer_max = 6; // Cantidad de fotogramas que recordaremos la pulsación de salto
contador_buffer = 0; // Contador regresivo activo
// --- TIEMPO COYOTE ---
tiempo_coyote_max = 6; // Cantidad de fotogramas tras caer en los que aún se permite saltar
contador_coyote = 0; // Contador regresivo activo
en_suelo = true; // Indicador de contacto con una superficie sólidaIntegración completa del sistema de movimiento pulido
Ahora que conocemos las responsabilidades de cada componente, observemos la estructura definitiva y ordenada del evento **Step** de `obj_player`. Por ahora, para probar estas tolerancias de forma autónoma antes de entrar al módulo exhaustivo de colisiones sólidas (Módulo 5), utilizaremos una comprobación básica de contacto con el suelo simulado basada en la posición vertical `y`.
/// Evento Step completo de obj_player con tolerancias y salto variable
// 1. CAPTURA DE ENTRADAS
var _tecla_der = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _tecla_izq = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _presiono_salto = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W"));
var _solto_salto = keyboard_check_released(vk_space) || keyboard_check_released(ord("W"));
var _dir = _tecla_der - _tecla_izq;
// 2. DETECCIÓN BÁSICA DE SUELO (Sustituido en el Módulo 5 por colisiones reales)
// Suponemos que el suelo de prueba está situado en la coordenada Y = 384
en_suelo = (y >= 384);
// 3. GESTIÓN DEL TIEMPO COYOTE
if (en_suelo) {
contador_coyote = tiempo_coyote_max; // Recargamos el margen mientras estemos pisando tierra
} else {
if (contador_coyote > 0) {
contador_coyote--; // Se agota poco a poco mientras caemos al vacío
}
}
// 4. GESTIÓN DEL BUFFER DE SALTO
if (_presiono_salto) {
contador_buffer = tiempo_buffer_max; // Guardamos la intención de saltar
} else if (contador_buffer > 0) {
contador_buffer--; // La intención caduca si pasa demasiado tiempo
}
// 5. MOVIMIENTO HORIZONTAL CON ACELERACIÓN Y FRICCIÓN
if (_dir != 0) {
vel_x += _dir * acel_x;
vel_x = clamp(vel_x, -vel_x_max, vel_x_max);
// Inversión visual del sprite según la dirección activa
image_xscale = _dir;
} else {
if (vel_x > 0) {
vel_x = max(0, vel_x - fric_x);
} else if (vel_x < 0) {
vel_x = min(0, vel_x + fric_x);
}
}
// 6. EJECUCIÓN DEL SALTO UTILIZANDO COYOTE Y BUFFER
if (contador_buffer > 0 && contador_coyote > 0) {
vel_y = vel_salto;
// Vaciamos inmediatamente los contadores para impedir saltos dobles accidentales
contador_buffer = 0;
contador_coyote = 0;
}
// 7. CORTE DE SALTO VARIABLE (ALTURA SEGÚN PULSACIÓN)
if (_solto_salto && vel_y < 0) {
vel_y = vel_y * multiplicador_corte_salto;
}
// 8. APLICACIÓN DE GRAVEDAD Y LÍMITES VERTICALES
if (!en_suelo) {
vel_y += gravedad;
if (vel_y > vel_y_max) {
vel_y = vel_y_max; // Limitamos la velocidad terminal de caída
}
} else {
// Si tocamos el suelo y no estamos ejecutando un salto nuevo, frenamos la caída vertical
if (vel_y > 0) {
vel_y = 0;
y = 384; // Aseguramos alineación exacta sobre la superficie de prueba
}
}
// 9. ACTUALIZACIÓN FINAL DE COORDENADAS
x += vel_x;
y += vel_y;Revisemos con atención el bloque número 6 del código anterior. La condición `contador_buffer > 0 && contador_coyote > 0` significa: «Si el jugador pulsó el botón de salto hace muy poco tiempo Y todavía cuenta con margen de gracia desde la última vez que tocó una plataforma sólida, el salto se dispara exitosamente». Al consumir de inmediato ambos contadores igualándolos a cero (`contador_buffer = 0; contador_coyote = 0;`), nos blindamos contra disparos repetidos en el mismo fotograma.
Comparativa de perfiles de movimiento
Ajustar las variables no consiste en elegir números al azar, sino en perseguir la identidad jugable que requiere tu nivel. La siguiente tabla presenta tres configuraciones arquetípicas comunes en el desarrollo 2D utilizando exactamente la estructura de código que acabamos de programar:
| Perfil jugable | acel_x / fric_x | vel_salto / gravedad | multiplicador_corte | Sensación en mandos |
|---|---|---|---|---|
| Arcade Clásico (Reactivo) | acel: 0.6 / fric: 0.5 | salto: -6.0 / grav: 0.35 | corte: 0.50 | Respuesta inmediata y giros muy rápidos. Ideal para juegos de acción vertiginosa. |
| Plataformas Pesado (Realista) | acel: 0.2 / fric: 0.15 | salto: -7.5 / grav: 0.45 | corte: 0.80 | Sensación de inercia y peso físico considerable. El salto tiene compromisos altos y cuesta corregir la trayectoria en el aire. |
| Precisión Moderna (Celeste / Meat Boy) | acel: 0.45 / fric: 0.4 | salto: -6.8 / grav: 0.38 | corte: 0.35 | Control milimétrico. Gran diferencia entre salto corto y salto largo; tolerancia de buffer alta (7 fotogramas). |
Compatibilidad con Gamepad
En la primera lección de este módulo vimos cómo utilizar las funciones `gamepad_axis_value()` y `gamepad_button_check()`. Para que nuestro refinamiento de controles beneficie también a los jugadores con mando USB o de consola, simplemente ampliamos las lecturas booleanas al inicio del evento Step:
/// Lectura combinada Teclado + Gamepad
var _gamepad_id = 0;
var _eje_h = gamepad_is_connected(_gamepad_id) ? gamepad_axis_value(_gamepad_id, gp_axislh) : 0;
// Consideramos zona muerta en el stick analógico para evitar derivas erráticas
var _stick_der = (_eje_h > 0.25);
var _stick_izq = (_eje_h < -0.25);
var _tecla_der = keyboard_check(vk_right) || keyboard_check(ord("D")) || gamepad_button_check(_gamepad_id, gp_padr) || _stick_der;
var _tecla_izq = keyboard_check(vk_left) || keyboard_check(ord("A")) || gamepad_button_check(_gamepad_id, gp_padl) || _stick_izq;
var _presiono_salto = keyboard_check_pressed(vk_space) || keyboard_check_pressed(ord("W")) || gamepad_button_check_pressed(_gamepad_id, gp_face1);
var _solto_salto = keyboard_check_released(vk_space) || keyboard_check_released(ord("W")) || gamepad_button_check_released(_gamepad_id, gp_face1);Al unificar las fuentes de entrada al inicio del fotograma en variables locales con prefijo de guion bajo (como `_tecla_der` o `_presiono_salto`), el resto del código físico permanece 100 % inalterado, sin importar qué periférico esté utilizando el jugador. Este desacoplamiento entre captura de datos y aplicación física es una de las mejores prácticas que mantendremos a lo largo de todo el curso.
Ejercicios prácticos
Realiza los siguientes ejercicios prácticos en tu proyecto para afianzar el dominio matemático de estas mecánicas de pulido:
1. **Modificación del corte de salto variable**: En el evento Create de `obj_player`, modifica el valor de `multiplicador_corte_salto`. Realiza una prueba con el valor en `0.1` y otra prueba con el valor en `0.8`. Observa la diferencia entre soltar el botón de inmediato y mantenerlo presionado. Comprobación: Con `0.1`, una pulsación relámpago apenas despegará al personaje unos pocos píxeles del suelo, demostrando un corte sumamente abrupto de la inercia ascendente.
2. **Calibración del margen Coyote**: Incrementa temporalmente la variable `tiempo_coyote_max` a `20` fotogramas en el evento Create. Ejecuta el juego, haz caminar al personaje por una plataforma elevada y presiona el botón de salto notablemente tarde, cuando el personaje ya esté flotando en el aire fuera del borde. Comprobación: Deberás notar que el personaje es capaz de saltar en pleno aire casi un tercio de segundo después de haber dejado de tocar la plataforma. Una vez comprobado su funcionamiento, redúcelo de nuevo a un valor natural (entre 5 y 7 fotogramas).
3. **Superficie resbaladiza (Hielo condicional)**: Declara en el evento Create una variable booleana llamada `en_hielo = false;`. En el evento Step, ajusta la lógica de fricción de modo que, si `en_hielo` es verdadero, la fricción horizontal se reduzca a una cuarta parte de su valor habitual (`fric_x * 0.25`). Coloca una tecla de prueba (como la tecla H) para alternar el valor de `en_hielo = !en_hielo;`. Comprobación: Al presionar la tecla H y correr, soltar el mando provocará que el personaje patine una distancia considerablemente mayor antes de detenerse por completo.
Resumen
- El 'game feel' o sensación de control es el factor determinante en la calidad y respuesta de un juego de plataformas en 2D.
- La inercia horizontal equilibrada se consigue calculando la aceleración progresiva con clamp() para topar la velocidad y aplicando fricción segura con min()/max() para evitar retrocesos al detenerse.
- El salto variable se implementa multiplicando la velocidad vertical ascendente (vel_y < 0) por una fracción al detectar keyboard_check_released().
- El tiempo coyote permite que el jugador ejecute un salto válido durante una pequeña ventana de fotogramas justo después de abandonar el borde de una plataforma.
- El buffer de salto guarda la intención de salto si se pulsa el botón unos fotogramas antes de llegar físicamente al suelo, disparándose de inmediato al aterrizar.
- Desacoplar la captura de entradas (teclado/gamepad) de la simulación física garantiza un código limpio, ordenado y mantenible.
Practica lo aprendido
Ejercicio 1 · 1 punto
¿Por qué en GameMaker se comprueba la condición `vel_y < 0` para amortiguar el salto en el momento en que el jugador suelta el botón?
Ejercicio 2 · 1 punto
Para mantener la velocidad horizontal dentro del rango permitido sin sobrepasar los límites se utiliza la función nativa . Además, para desacelerar una velocidad positiva sin que el valor baje de cero y provoque retrocesos involuntarios, se utiliza `vel_x = (0, vel_x - fric_x)`.
Ejercicio 3 · 1 punto
Relaciona cada variable o función con su función específica dentro del refinamiento del control:
- acel_x
- fric_x
- multiplicador_corte_salto
- clamp()
Ingresa para guardar tu progreso — Ingresar