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) └─ AnimatedSprite2DUsar 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
AnimatedSprite2Dy en la propiedad Sprite Frames crea un recursoSpriteFrames. - 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):
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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()yvelocity.y < 0→ jump - Si
!is_on_floor()yvelocity.y >= 0→ fall - Si
is_on_floor()yabs(velocity.x) > umbral→ run - Si
is_on_floor()yabs(velocity.x) <= umbral→ idle
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
jumpsolo sivelocity.y < -EPSyfallsivelocity.y > EPS. - “Lock” temporal: mantener
jumphasta quevelocity.ysea 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.animationFlip 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_henAnimatedSprite2D(o invertir escala en el nodoVisual). - No escalar negativamente el
CharacterBody2Dcompleto, 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) * facingEl 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
CollisionShape2Dalineado al cuerpo (por ejemplo, que su base coincida con los pies). - Ajusta la posición del nodo
Visualo delAnimatedSprite2Dpara 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 └─ AnimationPlayer2) 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:textureoSprite2D:framesi usas atlas/frames, oVisual:positionpara un pequeño “bounce”. - Ajusta la velocidad con
speed_scaleo 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(oSprite2D+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_THRESHOLDy (opcional)VY_EPS. - Calcula
desiredcon 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ón | Qué debería pasar | Qué ajustar si falla |
|---|---|---|
| Te detienes lentamente | run → idle sin alternar | Sube RUN_THRESHOLD o aplica fricción más estable |
| Ápice del salto | jump → fall sin parpadeo | Usa VY_EPS o conserva animación cerca de 0 |
| Cambiar dirección | flip inmediato al correr | Actualiza facing con umbral y no con cualquier velocity.x |
| Sprite no coincide con colisión | Pies alineados al suelo | Ajusta Visual.position o el pivote/offset del arte |