Animaciones 2D y sincronización con el estado del jugador

Capítulo 8

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Objetivo: animar al jugador según su estado

En un juego 2D, la animación no debería “vivir sola”: debe responder al estado del personaje (quieto, corriendo, saltando, cayendo). Para lograrlo de forma estable, conviene separar dos cosas: (1) la lógica de movimiento (ya implementada en capítulos anteriores) y (2) una capa de máquina de estados de animación que decide qué animación reproducir según condiciones simples como: is_on_floor(), velocity.x y velocity.y.

En este capítulo verás dos enfoques comunes en Godot 4: AnimatedSprite2D (rápido y directo con SpriteFrames) y AnimationPlayer (más flexible si quieres animar varias propiedades además del sprite). En ambos casos, la clave para evitar “parpadeos” es: no reiniciar la animación si ya está sonando y priorizar estados (por ejemplo, jump/fall por encima de run/idle).

Opción A: AnimatedSprite2D (SpriteFrames) para idle/run/jump/fall

1) Estructura recomendada de nodos

Una estructura típica para el jugador (solo lo relevante a animación) podría ser:

Player (CharacterBody2D)  [script: player.gd]  ├─ CollisionShape2D  └─ Visual (Node2D)      └─ AnimatedSprite2D

Usar un nodo intermedio Visual ayuda a ajustar la alineación del sprite sin tocar la colisión (lo veremos más abajo).

2) Crear SpriteFrames y animaciones

  • Selecciona AnimatedSprite2D y en la propiedad Sprite Frames crea un recurso SpriteFrames.
  • Dentro del editor de SpriteFrames, crea animaciones llamadas exactamente: idle, run, jump, fall.
  • Arrastra los frames correspondientes a cada animación.

3) Ajustar FPS y loop por animación

Buenas prácticas típicas (ajústalo a tu arte):

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • idle: 6–10 FPS, loop activado.
  • run: 10–16 FPS, loop activado.
  • jump: 8–12 FPS, loop desactivado si es un “impulso” corto; o loop activado si es una pose sostenida.
  • fall: 8–12 FPS, loop activado (suele ser una pose o ciclo corto).

Si jump es una animación no-loop (por ejemplo, 3–6 frames) y quieres que luego pase a fall, lo más simple es que el estado cambie por velocity.y (cuando sea positiva, caer) en lugar de esperar a animation_finished.

Máquina de estados simple (condiciones) para elegir animación

1) Definir estados y reglas de prioridad

Una máquina de estados mínima puede basarse en estas reglas (en orden de prioridad):

  • Si !is_on_floor() y velocity.y < 0jump
  • Si !is_on_floor() y velocity.y >= 0fall
  • Si is_on_floor() y abs(velocity.x) > umbralrun
  • Si is_on_floor() y abs(velocity.x) <= umbralidle

El umbral evita que pequeñas variaciones numéricas (o fricción) hagan alternar entre idle y run.

2) Implementación práctica en GDScript (sin parpadeos)

La idea es calcular un desired_anim y reproducirla solo si cambia respecto a la actual.

extends CharacterBody2D  @onready var anim: AnimatedSprite2D = $Visual/AnimatedSprite2D  const RUN_THRESHOLD := 10.0  var facing := 1 # 1 derecha, -1 izquierda  func _physics_process(delta: float) -> void:     # ... aquí va tu lógica de movimiento ya existente ...     _update_facing()     _update_animation()  func _update_facing() -> void:     # Actualiza dirección solo si hay intención real de moverse     if abs(velocity.x) > RUN_THRESHOLD:         facing = sign(velocity.x)         anim.flip_h = (facing < 0)  func _update_animation() -> void:     var desired := "idle"      if not is_on_floor():         if velocity.y < 0.0:             desired = "jump"         else:             desired = "fall"     else:         if abs(velocity.x) > RUN_THRESHOLD:             desired = "run"         else:             desired = "idle"      # Evitar parpadeo: no reiniciar si ya está en esa animación     if anim.animation != desired:         anim.play(desired)

Este patrón evita el parpadeo típico que ocurre cuando se llama play() cada frame. También evita que idle “robe” prioridad a jump/fall porque primero se evalúa el aire.

3) Evitar cambios rápidos jump↔fall cerca del ápice

En el punto más alto del salto, velocity.y puede oscilar cerca de 0, provocando cambios muy rápidos entre jump y fall. Dos soluciones simples:

  • Umbral vertical: considerar jump solo si velocity.y < -EPS y fall si velocity.y > EPS.
  • “Lock” temporal: mantener jump hasta que velocity.y sea claramente positiva.

Ejemplo con umbral:

const VY_EPS := 5.0  if not is_on_floor():     if velocity.y < -VY_EPS:         desired = "jump"     elif velocity.y > VY_EPS:         desired = "fall"     else:         # Mantén la animación actual cerca del ápice         desired = anim.animation

Flip horizontal (mirar a izquierda/derecha) sin romper colisiones

Para un personaje 2D, lo habitual es voltear solo el sprite, no el cuerpo físico. Por eso se recomienda:

  • Aplicar flip_h en AnimatedSprite2D (o invertir escala en el nodo Visual).
  • No escalar negativamente el CharacterBody2D completo, porque puede complicar colisiones, raycasts o nodos hijos.

Dos enfoques:

Enfoque 1: flip_h del AnimatedSprite2D

anim.flip_h = (facing < 0)

Enfoque 2: invertir escala del nodo Visual

@onready var visual: Node2D = $Visual  visual.scale.x = abs(visual.scale.x) * facing

El enfoque 2 es útil si además del sprite quieres voltear otros elementos visuales (por ejemplo, un arma como hijo de Visual), manteniendo intacta la física.

Alineación: sprite y colisión deben “pisar” el mismo suelo

Un problema común es que la colisión esté bien, pero el sprite parezca “flotar” o “hundirse”. Para corregirlo sin tocar la física:

  • Deja el CollisionShape2D alineado al cuerpo (por ejemplo, que su base coincida con los pies).
  • Ajusta la posición del nodo Visual o del AnimatedSprite2D para que los pies del dibujo coincidan con la base de la colisión.
  • Revisa el offset/pivote de tus sprites: si los frames no comparten un punto de anclaje consistente, verás “bamboleo” al cambiar de animación.

Checklist rápido de alineación

  • Activa la visualización de colisiones en el editor/depuración para comparar pies vs. collider.
  • En tu spritesheet, procura que todos los frames tengan el mismo “suelo” (misma línea de pies).
  • Si una animación (por ejemplo, jump) tiene frames más altos, ajusta el arte o usa un offset consistente; evita compensar con código por frame salvo necesidad.

Opción B: AnimationPlayer (cuando quieres animar más que frames)

AnimationPlayer es ideal si además de cambiar frames quieres animar propiedades como: posición del arma, escala de squash/stretch, color, visibilidad de efectos, etc. Una configuración común es usar un Sprite2D (o AnimatedSprite2D) y que AnimationPlayer controle qué frame/propiedad se muestra, o que dispare efectos sincronizados.

1) Estructura de nodos ejemplo

Player (CharacterBody2D)  ├─ CollisionShape2D  └─ Visual (Node2D)      ├─ Sprite2D      └─ AnimationPlayer

2) Crear animaciones en AnimationPlayer

  • Crea animaciones: idle, run, jump, fall.
  • En cada una, agrega pistas (tracks) para las propiedades que quieras animar. Por ejemplo, Sprite2D:texture o Sprite2D:frame si usas atlas/frames, o Visual:position para un pequeño “bounce”.
  • Ajusta la velocidad con speed_scale o con la duración de la animación.

3) Cambiar animación sin reiniciar (mismo patrón anti-parpadeo)

@onready var ap: AnimationPlayer = $Visual/AnimationPlayer  func _play_anim(name: String) -> void:     if ap.current_animation != name:         ap.play(name)

Luego reutilizas la misma lógica de selección de estado (suelo, velocity.x, velocity.y) para decidir name.

Práctica guiada: integrar animación con tu controlador actual

Paso 1: añade el nodo visual y el animador

  • Crea Visual (Node2D) como hijo del jugador.
  • Mueve dentro AnimatedSprite2D (o Sprite2D + AnimationPlayer).
  • Asegúrate de que la colisión quede como hijo directo del jugador (no dentro de Visual).

Paso 2: crea las animaciones y nómbralas de forma consistente

El código dependerá de strings como "idle", "run", "jump", "fall". Mantén esos nombres idénticos en el editor para evitar errores.

Paso 3: implementa la función de selección de animación

  • Define RUN_THRESHOLD y (opcional) VY_EPS.
  • Calcula desired con prioridad aire > suelo.
  • Reproduce solo si cambia.

Paso 4: añade flip horizontal basado en la última dirección válida

Actualiza facing solo cuando abs(velocity.x) supere el umbral, así el personaje no “tiembla” mirando a ambos lados cuando se detiene.

Paso 5: prueba casos límite

SituaciónQué debería pasarQué ajustar si falla
Te detienes lentamenterun → idle sin alternarSube RUN_THRESHOLD o aplica fricción más estable
Ápice del saltojump → fall sin parpadeoUsa VY_EPS o conserva animación cerca de 0
Cambiar direcciónflip inmediato al correrActualiza facing con umbral y no con cualquier velocity.x
Sprite no coincide con colisiónPies alineados al sueloAjusta Visual.position o el pivote/offset del arte

Ahora responde el ejercicio sobre el contenido:

¿Qué práctica ayuda a evitar el “parpadeo” al cambiar animaciones según el estado del jugador en Godot?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Para evitar parpadeos, se elige una animación objetivo según condiciones (suelo, velocidad) y se llama a play solo cuando el nombre cambia. Además, priorizar estados (aire sobre suelo) evita cambios inconsistentes.

Siguiente capítulo

Enemigos y patrones de comportamiento 2D con GDScript

Arrow Right Icon
Portada de libro electrónico gratuitaGodot desde Cero: Crea tu Primer Juego 2D con GDScript
67%

Godot desde Cero: Crea tu Primer Juego 2D con GDScript

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.