Parte: 1 — Motores 2D y tu primer juego jugable · Fuente: Documentación de Godot 4 ⏱️ Duración estimada: 90 min · Nivel: Intermedio
Reemplazar el controlador del jugador basado en un if gigante por una máquina de estados finitos (FSM). Cada estado —IDLE, RUN, JUMP, FALL— tendrá su propia lógica de entrada, actualización por frame y salida, con transiciones explícitas entre ellos.
Al terminar, tu CharacterBody2D responderá igual que antes pero el código será legible, ampliable y libre de banderas cruzadas. Verás por qué la FSM es el patrón estándar para controlar personajes y cómo aplicarlo tanto con un enum simple como comparándolo con la variante basada en nodos.
Al finalizar, el alumno podrá:
enum de estados y una función por estado.enter / update / exit para cada estado.is_on_floor, velocity.y) e input.enum con la FSM por nodos y elegir según el proyecto.| # | Tema | Por qué importa |
|---|---|---|
| 1 | El problema del if gigante |
Los estados implícitos generan bugs difíciles de rastrear. |
| 2 | Patrón State aplicado al jugador | Aísla cada comportamiento en una unidad clara. |
| 3 | Estados Idle/Run/Jump/Fall | Cubren el ciclo completo de un plataformas 2D. |
| 4 | Tríada enter/update/exit | Ordena inicialización, lógica y limpieza por estado. |
| 5 | Transiciones explícitas | Hacen visible cuándo y por qué se cambia de estado. |
| 6 | FSM por enum | Simple, rápida y suficiente para un jugador. |
| 7 | FSM por nodos | Escala mejor cuando hay muchos estados complejos. |
| 8 | Integración con animación y física | Cada estado ajusta sprite y velocity. |
IDLE, RUN…). Clave: encapsula qué hacer y a qué otros estados puede ir._physics_process. Clave: aplica movimiento y evalúa transiciones.Continúa con el proyecto PlataformasCurso de clases anteriores. Necesitas la escena Jugador (un CharacterBody2D con AnimatedSprite2D y CollisionShape2D) y las acciones de input mover_izquierda, mover_derecha y saltar definidas en Project Settings > Input Map. Ten a mano la referencia de CharacterBody2D (https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html) y la guía sobre match en GDScript (https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#match).
Si tu AnimatedSprite2D aún no tiene las animaciones idle, run, jump y fall, créalas en el editor de SpriteFrames; pueden ser de un solo cuadro por ahora.
Refactorizaremos el controlador a una FSM por enum con una función por estado.
Jugador y selecciona el nodo raíz CharacterBody2D. Reemplaza su script por el siguiente esqueleto, que declara el enum y las variables base.extends CharacterBody2D
enum Estado { IDLE, RUN, JUMP, FALL }
const VELOCIDAD := 130.0
const FUERZA_SALTO := -320.0
const GRAVEDAD := 900.0
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
var estado: Estado = Estado.IDLE
var direccion: float = 0.0
_physics_process. Su única responsabilidad es leer input, aplicar gravedad, delegar en el estado activo y mover el cuerpo.func _physics_process(delta: float) -> void:
direccion = Input.get_axis("mover_izquierda", "mover_derecha")
if not is_on_floor():
velocity.y += GRAVEDAD * delta
match estado:
Estado.IDLE:
_estado_idle()
Estado.RUN:
_estado_run()
Estado.JUMP:
_estado_jump()
Estado.FALL:
_estado_fall()
if direccion != 0.0:
sprite.flip_h = direccion < 0.0
move_and_slide()
exit implícito, cambia el estado y lanza el enter (aquí, la animación correcta).func _cambiar_estado(nuevo: Estado) -> void:
if nuevo == estado:
return
estado = nuevo
match estado:
Estado.IDLE:
sprite.play("idle")
Estado.RUN:
sprite.play("run")
Estado.JUMP:
sprite.play("jump")
Estado.FALL:
sprite.play("fall")
IDLE y RUN. Ambos están en el suelo: frenan o mueven en horizontal y comparten las transiciones a salto y caída.func _estado_idle() -> void:
velocity.x = move_toward(velocity.x, 0.0, VELOCIDAD)
if Input.is_action_just_pressed("saltar") and is_on_floor():
velocity.y = FUERZA_SALTO
_cambiar_estado(Estado.JUMP)
elif direccion != 0.0:
_cambiar_estado(Estado.RUN)
func _estado_run() -> void:
velocity.x = direccion * VELOCIDAD
if Input.is_action_just_pressed("saltar") and is_on_floor():
velocity.y = FUERZA_SALTO
_cambiar_estado(Estado.JUMP)
elif not is_on_floor():
_cambiar_estado(Estado.FALL)
elif direccion == 0.0:
_cambiar_estado(Estado.IDLE)
JUMP y FALL. Permiten control horizontal en el aire y transicionan según el signo de velocity.y y el aterrizaje.func _estado_jump() -> void:
velocity.x = direccion * VELOCIDAD
if velocity.y >= 0.0:
_cambiar_estado(Estado.FALL)
func _estado_fall() -> void:
velocity.x = direccion * VELOCIDAD
if is_on_floor():
if direccion != 0.0:
_cambiar_estado(Estado.RUN)
else:
_cambiar_estado(Estado.IDLE)
print(Estado.keys()[estado]) dentro de _cambiar_estado para observar las transiciones en la consola de salida.WALL_SLIDE que reduzca la caída al empujar contra una pared (is_on_wall()).Dictionary para evitar el match de _cambiar_estado.Label de depuración el estado actual actualizado por frame.enum.Amplía la FSM con un estado DASH que impulse al jugador horizontalmente a alta velocidad durante 0.15 s al pulsar una acción dash, ignorando el input direccional mientras dura, y que vuelva a IDLE, RUN o FALL al terminar según el contexto. Criterio de aceptación: al pulsar dash en el suelo el personaje recorre una distancia fija reproduciendo una animación dash, no puede encadenar dos dashes sin tocar suelo, y las transiciones de salida son correctas verificadas por consola.
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| El personaje "vibra" entre RUN e IDLE | Se evalúan transiciones opuestas el mismo frame; ordena las condiciones y usa elif. |
| La animación no cambia al saltar | Olvidaste llamar a _cambiar_estado; el sprite.play solo ocurre allí. |
Invalid get index 'IDLE' |
Usas el nombre como texto; accede con Estado.IDLE, no "IDLE". |
| Salta en el aire indefinidamente | Falta la comprobación is_on_floor() antes de aplicar FUERZA_SALTO. |
| Nunca entra en FALL | No transicionas de JUMP a FALL; compara velocity.y >= 0.0. |
❓ ¿Por qué separar enter/update/exit si mi estado es simple? Porque cuando el estado crezca (efectos, sonidos, temporizadores) ya tendrás dónde colocar cada cosa sin reescribir la estructura.
❓ ¿enum o nodos para la FSM? Para un jugador con pocos estados el enum es más directo. Si prevés decenas de estados con lógica extensa, la variante por nodos aísla mejor cada archivo.
❓ ¿Dónde aplico la gravedad, en cada estado o fuera? Fuera, en _physics_process, para no repetirla. Los estados solo deciden velocity.x y las transiciones.
❓ ¿Puedo estar en dos estados a la vez? No: una FSM tiene un único estado activo. Si necesitas capas paralelas (ej. movimiento + ataque), usa dos FSM independientes.
match y enum: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.htmlClase 035 - Tilemaps y diseño de niveles 2D
Clase 037 - Enemigos e IA básica 2D: patrullas y persecución