Introducción al Desarrollo de Videojuegos con GameMaker · Módulo 6: Animaciones, sprites, fondos y tilemaps.

Creación de la máquina de estados visuales

Clase 27 de 60 · 60 min

También disponible en: EN

Introducción a la máquina de estados visuales

En los módulos anteriores dotaste al jugador de movimiento horizontal, aceleración, gravedad, detección de suelo (`grounded`) y un sistema de colisiones sólido en los ejes horizontal y vertical. Sin embargo, un personaje que se desplaza deslizándose estáticamente o cuyas animaciones parpadean sin control carece de la respuesta táctil y estética fundamental en un juego de plataformas 2D retro. Para que un videojuego se sienta dinámico, el aspecto visual del personaje debe reflejar en cada cuadro de animación con absoluta precisión lo que está sucediendo a nivel físico y mecánico.

Muchos desarrolladores novatos cometen el error de insertar asignaciones directas de sprites dentro de cada comprobación de teclado (`if (keyboard_check(vk_right)) sprite_index = spr_player_run;`). Este enfoque degenera de inmediato en código espagueti: el sprite parpadea, la animación de salto se cancela antes de tiempo, el personaje corre en el aire o los cuadros quedan congelados al presionar dos teclas al mismo tiempo. Para resolver esta problemática de forma profesional y escalable en GameMaker, implementaremos una Máquina de Estados Visuales (Finite State Machine o FSM visual).

Concepto: Máquina de Estados Finitos (FSM) aplicada a la animación

Una Máquina de Estados Finitos es un modelo de computación que estipula que una entidad solo puede encontrarse exactamente en uno de un conjunto finito de estados en un momento determinado. En nuestro caso, el personaje no puede estar corriendo y descansando simultáneamente, ni puede considerarse en caída ascendente. En función de una serie de entradas (condiciones físicas como la velocidad horizontal `hsp`, la velocidad vertical `vsp` y el contacto con el suelo `grounded`), la máquina efectúa una transición de un estado a otro y actualiza las propiedades visuales correspondientes (`sprite_index`, `image_speed`, e `image_xscale`).

En GameMaker moderno, la forma más limpia, legible y optimizada de implementar estados es a través de un enumerador (`enum`). Un enumerador asigna nombres descriptivos a números enteros secuenciales entre bastidores, lo que evita el uso de cadenas de texto lentas y propensas a errores tipográficos (como `state = "running"`), garantizando que el compilador procese las comparaciones de manera instantánea.

Diagrama esquemático de una máquina de estados finitos que ilustra las transiciones lógicas entre los estados Idle, Run, Jump y Fall según el contacto con el suelo y las velocidades horizontal y vertical.
Diagrama de flujo de transiciones entre estados visuales en función de la velocidad y el contacto con el suelo. · Imagen generada con IA

Estandarización de recursos gráficos y colisiones

Antes de programar una sola línea de lógica visual, es imperativo establecer una regla de oro en el desarrollo de plataformas 2D: la coherencia espacial y dimensional de los sprites asociados a un mismo objeto. Cuando cambiamos el `sprite_index` de una instancia, GameMaker modifica automáticamente la máscara de colisión predeterminada y el punto de origen al nuevo sprite si no hemos tomado medidas preventivas. Si el sprite de correr tiene 32 píxeles de alto con origen en la base, y el sprite de salto tiene 40 píxeles de alto con origen en el centro, el personaje quedará incrustado en el suelo o flotará durante el salto, destruyendo la precisión física de las colisiones.

Recurso SpriteContenido y SubimágenesPunto de Origen (Origin)Configuración de Máscara (Collision Mask)
spr_player_idle4 a 6 cuadros (respiración o balanceo)Bottom Centre (x: centrado, y: fondo)Manual o Same as Mask (Bounding Box idéntico)
spr_player_run6 a 8 cuadros (ciclo de carrera)Bottom Centre (x: centrado, y: fondo)Mismo tamaño y desplazamiento que idle
spr_player_jump1 a 2 cuadros (impulso ascendente)Bottom Centre (x: centrado, y: fondo)Mismo tamaño y desplazamiento que idle
spr_player_fall1 a 2 cuadros (caída libre)Bottom Centre (x: centrado, y: fondo)Mismo tamaño y desplazamiento que idle

💡 Regla crítica: Para evitar que el personaje se atasque en plataformas o paredes al alternar entre sprites, asigna un sprite de colisión dedicado en la propiedad 'Collision Mask' del objeto 'obj_player' (por ejemplo, un rectángulo simple llamado 'spr_player_mask') o asegúrate de que todos los sprites de animación compartan con exactitud matemática el mismo origen y las mismas dimensiones de caja delimitadora (Bounding Box).

Configuración de variables en el Evento Create

En el objeto `obj_player`, dentro del evento Create, ya contamos con variables cinemáticas establecidas en módulos previos (`hsp`, `vsp`, `walk_spd`, `grav`, `jump_spd`, `grounded`). Añadiremos ahora el enumerador de estados, la variable que almacena el estado actual y una variable para rastrear el estado del cuadro anterior. Esta última nos permitirá detectar el momento exacto en el que ocurre una transición y reiniciar la subimagen `image_index` a cero, evitando que una animación comience a reproducirse por la mitad.

// ----------------------------------------------------------------------------
// OBJETO: obj_player
// EVENTO: Create
// ----------------------------------------------------------------------------

// Variables de movimiento y físicas (establecidas en módulos 4 y 5)
hsp = 0;
vsp = 0;
walk_spd = 4;
grav = 0.4;
jump_spd = -8.5;
grounded = false;

// Definición de los estados visuales del jugador
enum PLAYER_STATE {
    IDLE,
    RUN,
    JUMP,
    FALL
}

// Inicialización del estado actual y previo
state = PLAYER_STATE.IDLE;
state_previous = PLAYER_STATE.IDLE;

// Orientación horizontal del sprite (1 = derecha, -1 = izquierda)
facing = 1;

Observa que declaramos `facing = 1`. Aunque GameMaker utiliza la variable integrada `image_xscale` para escalar e invertir el dibujo de la instancia, almacenar la dirección deseada en una variable explícita (`facing`) antes de aplicarla a `image_xscale` permite desacoplar los controles de entrada de la renderización final y evita errores de escala no deseados.

Determinación del estado en el Evento Step

El flujo de ejecución en el evento Step debe estructurarse rigurosamente en etapas sucesivas. Si intentamos cambiar sprites mientras calculamos colisiones, el motor gráfico procesará datos inconsistentes. El orden canónico recomendado es el siguiente:

EtapaAcción PrincipalVariables Involucradas
1. Entrada (Inputs)Capturar pulsaciones del jugadorkey_left, key_right, key_jump
2. Lógica FísicaCalcular aceleración horizontal y gravedadmove_dir, hsp, vsp
3. Colisiones y TraslaciónResolver bloqueos con muros y mover 'x', 'y'x, y, grounded
4. Evaluación del EstadoDeterminar qué estado corresponde según las físicasstate
5. Asignación VisualAsignar sprites, resetear cuadros y orientarsprite_index, image_index, image_xscale

A continuación presentamos el bloque completo de evaluación de estados y asignación visual que debe integrarse en la sección final del evento Step de `obj_player`, justo después de resolver el movimiento horizontal, vertical y las colisiones con el suelo y paredes.

// ----------------------------------------------------------------------------
// OBJETO: obj_player
// EVENTO: Step (Sección final: Máquina de Estados Visuales)
// ----------------------------------------------------------------------------

// --- ETAPA 4: EVALUACIÓN DEL ESTADO SEGÚN CONDICIONES FÍSICAS ---
if (grounded) {
    // Si el jugador está sobre una superficie sólida
    if (hsp == 0) {
        state = PLAYER_STATE.IDLE;
    } else {
        state = PLAYER_STATE.RUN;
    }
} else {
    // Si el jugador está en el aire (no está grounded)
    if (vsp < 0) {
        state = PLAYER_STATE.JUMP;
    } else {
        state = PLAYER_STATE.FALL;
    }
}

// --- ETAPA 5: APLICACIÓN VISUAL Y CONTROL DE ANIMACIÓN ---
// Si hubo un cambio de estado en este cuadro, reiniciamos el índice de animación
if (state != state_previous) {
    image_index = 0;
}

// Selección del sprite según el estado determinado
switch (state) {
    case PLAYER_STATE.IDLE:
        sprite_index = spr_player_idle;
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.RUN:
        sprite_index = spr_player_run;
        // Ajuste opcional: la velocidad de animación se sincroniza con la velocidad real
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.JUMP:
        sprite_index = spr_player_jump;
        image_speed = 1.0;
        break;
        
    case PLAYER_STATE.FALL:
        sprite_index = spr_player_fall;
        image_speed = 1.0;
        break;
}

// --- ORIENTACIÓN HORIZONTAL (FACING) ---
// Solo actualizamos la dirección si el jugador se está moviendo activamente
if (hsp != 0) {
    facing = sign(hsp);
}

// Aplicamos la escala visual horizontal respetando la magnitud base
image_xscale = facing;

// Actualizamos el registro del estado previo para el siguiente cuadro
state_previous = state;

Análisis profundo de la lógica de asignación

Desglosemos las decisiones de arquitectura implementadas en el bloque anterior para comprender el porqué de cada estructura:

1. **Jerarquía Grounded vs. Aire:** La primera bifurcación condicional comprueba `grounded`. Si el jugador está en el aire, carece de relevancia si se está presionando la tecla de caminar; las físicas verticales mandan sobre la animación. Separar el aire de la tierra elimina de raíz el defecto habitual de que el personaje reproduzca el ciclo de caminar mientras cae al vacío.

2. **Diferenciación de Salto y Caída por signo de velocidad:** La velocidad vertical `vsp` es negativa cuando el cuerpo asciende en la pantalla (debido a que en GameMaker las coordenadas `y` aumentan hacia abajo) y positiva cuando desciende bajo el efecto de la gravedad. Al distinguir `vsp < 0` (impulso ascendente) de `vsp >= 0` (caída por gravedad), dotamos a la caída de una pose gráfica estirada o tensa que añade inmensa fluidez visual sin necesidad de lógica extra.

3. **La comprobación `state != state_previous`:** Este es el mecanismo clave para erradicar el parpadeo de animaciones. Si un sprite tiene 6 cuadros y asignamos `image_index = 0` en cada paso sin verificar si el estado cambió, GameMaker forzará el cuadro 0 en cada ciclo, paralizando la animación en una pose estática. Al ejecutar `image_index = 0` únicamente en el cuadro exacto donde `state` difiere de `state_previous`, garantizamos que cada nueva animación inicie limpia desde su primer cuadro sin interferir en los ciclos subsecuentes.

4. **El uso de la función `sign()` para la orientación:** La función nativa `sign(valor)` retorna `1` si el argumento es positivo, `-1` si es negativo y `0` si es cero. Al comprobar `if (hsp != 0) facing = sign(hsp);`, nos aseguramos de que cuando el jugador se detiene (`hsp == 0`), `facing` retenga su último valor válido (1 o -1), impidiendo que el sprite colapse a un ancho horizontal de cero y desaparezca de la vista.

Casos prácticos de optimización y solución de problemas

Ejercicios prácticos de consolidación

A continuación, realiza los siguientes tres ejercicios en tu proyecto para poner en práctica y validar la arquitectura de la máquina de estados visuales:

**Ejercicio 1: Implementación visual del estado de caída independiente.** Importa o crea en el Sprite Editor de GameMaker un sprite simple llamado `spr_player_fall` de 32x32 píxeles, con origen en 'Bottom Centre'. Asegúrate de que el personaje tenga una pose distintiva de caída (brazos extendidos hacia arriba o capa ondeando). Configura la máquina de estados para que al saltar y alcanzar el punto máximo de la trayectoria (apogeo del salto donde `vsp >= 0`), la transición hacia `spr_player_fall` se ejecute de manera fluida antes de tocar el suelo. **Criterio de validación:** Al saltar, debes observar claramente la transición de `spr_player_jump` mientras el personaje sube, a `spr_player_fall` en cuanto empieza a descender, y a `spr_player_idle` o `spr_player_run` en el fotograma exacto en el que toca una plataforma sólida.

**Ejercicio 2: Sincronización dinámica de la velocidad de animación con la velocidad del suelo.** Modifica el bloque del estado `PLAYER_STATE.RUN` en el evento Step para que `image_speed` no sea una constante fija de 1.0, sino un valor proporcional a la magnitud absoluta de la velocidad horizontal (`abs(hsp)`). Si el jugador camina lentamente (por ejemplo, con un stick de gamepad a la mitad o en pendientes), los pies deben moverse despacio; si corre a máxima velocidad (`walk_spd`), la animación debe reproducirse a velocidad normal. **Criterio de validación:** Formula la relación matemática `image_speed = abs(hsp) / walk_spd;`. Comprueba que al frenar progresivamente, los pasos del personaje reduzcan su frecuencia visual sin detenerse abruptamente hasta alcanzar el reposo total.

**Ejercicio 3: Depuración visual en pantalla (Debug GUI State Overlay).** Abre el evento 'Draw GUI' de `obj_player` e implementa la función `draw_text()` para mostrar en la esquina superior izquierda de la pantalla el nombre del estado actual en texto legible, junto con las variables `hsp`, `vsp`, `grounded` y `image_index`. **Criterio de validación:** Ejecuta el juego y verifica en tiempo real que el texto coincida exactamente con la postura visual del personaje en cada frame. Comprueba que no se produzcan saltos erráticos de estado mientras corres, saltas, caes de salientes sin pulsar el salto o colisionas lateralmente contra paredes.

Resumen

  • Una Máquina de Estados Finitos (FSM) visual previene parpadeos y solapamientos de animación desacoplando la lectura física de la renderización.
  • El uso de 'enum' en GML Code proporciona nombres simbólicos optimizados y legibles para cada postura del personaje (IDLE, RUN, JUMP, FALL).
  • Todos los sprites asignados a una misma instancia deben compartir de forma uniforme su origen ('Bottom Centre') y su máscara de colisión para evitar atascos en la geometría.
  • La condición 'state != state_previous' es obligatoria para reiniciar 'image_index = 0' únicamente al momento exacto de la transición entre estados.
  • La dirección del sprite se gestiona asignando 'sign(hsp)' a una variable auxiliar de orientación ('facing') aplicada a 'image_xscale' únicamente cuando la velocidad horizontal no es cero.

Practica lo aprendido

Ejercicio 1 · 1 punto

¿Por qué es necesario comprobar la condición `state != state_previous` antes de reiniciar `image_index = 0` en el evento Step?

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

Ejercicio 2 · 1 punto

Completa el siguiente fragmento de código de la etapa de evaluación en el evento Step para determinar el estado visual cuando el personaje está en el aire: ```gml if (grounded) { // Lógica en el suelo } else { // Si el jugador está en el aire (no está grounded) if ( < 0) { state = PLAYER_STATE.JUMP; } else { state = PLAYER_STATE.; } } ```

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

Ejercicio 3 · 1 punto

Ordena de forma cronológica los bloques de ejecución de la máquina de estados visuales dentro del evento Step, según el diseño presentado en la lección:

  1. 1Evaluar las condiciones físicas (grounded, hsp, vsp) para asignar el valor de state.
  2. 2Guardar el estado actual en la variable state_previous para el cuadro siguiente.
  3. 3Asignar el sprite_index e image_speed correspondiente mediante la estructura switch (state).
  4. 4Actualizar la variable facing según el movimiento y asignarla a image_xscale.
  5. 5Comprobar si state != state_previous para reiniciar image_index a cero en caso de transición.
Practicas sin cuenta: no se guarda nada. Regístrate para guardar tu progreso.

Ingresa para guardar tu progreso — Ingresar