Código limpio y escalable con máquina de estados godot 4
Aprende a estructurar una máquina de estados godot 4 modular en GDScript. Elimina IFs anidados, organiza animaciones y crea mecánicas sostenibles.
Desarrollar mecánicas de gameplay sin una estructura bien definida es la receta directa para crear scripts gigantescos e imposibles de mantener. Cuando un personaje necesita caminar, saltar, esquivar, atacar y recibir daño, la tendencia de quien está empezando es acumular decenas de variables booleanas y estructuras condicionales dentro del método _process o _physics_process. A medida que el proyecto crece, modificar el comportamiento del salto termina rompiendo la esquiva o bloqueando el ataque. Para resolver esta complejidad de forma elegante, implementar una máquina de estados godot 4 es el patrón de diseño arquitectónico más eficiente y sostenible.
Una máquina de estados finitos (FSM, por sus siglas en inglés Finite State Machine) aísla cada comportamiento en su propia lógica independiente. El jugador no puede atacar y esquivar simultáneamente si esos comportamientos son estados mutuamente exclusivos. En este tutorial práctico, aprenderás a construir un sistema flexible en Godot 4.7.2 usando GDScript totalmente orientado a objetos y nodos, garantizando que tu código se mantenga limpio, escalable e inmune al código espagueti.
¿Por qué evitar el código espagueti con IFs anidados en GDScript?
Al inicio del desarrollo de un prototipo, verificar el estado del jugador con comandos condicionales parece inofensivo. Un código simple que comprueba si el jugador está en el suelo para permitir el salto funciona bien en las primeras horas de trabajo. El problema surge cuando el diseño del videojuego requiere nuevas mecánicas.
Considera el aumento de la complejidad ciclomática cuando añadimos un doble salto, un deslizamiento en la pared y una animación de ataque con tiempo de recarga. Con estructuras condicionales simples, cada fotograma de renderizado del videojuego necesita evaluar una red de comprobaciones del tipo if is_on_floor() and not is_attacking and is_dashing. Las trampas de este modelo monolítico incluyen:
- Errores de estado fantasma: el personaje ejecuta animaciones de carrera mientras está cayendo por un precipicio porque una variable booleana no se restableció en el momento correcto.
- Dificultad de depuración: rastrear qué condición falló entre decenas de bloques
if/elseanidados consume horas en sesiones de depuración. - Acoplamiento excesivo: modificar la física de un movimiento exige alterar un script de 800 líneas que maneja tanto el audio como las colisiones y el inventario.
Al adoptar el patrón State, cada estado se convierte en una entidad autónoma con responsabilidad única. El estado de salto se encarga únicamente del impulso vertical y de la transición a la caída; el estado de ataque gestiona exclusivamente las cajas de colisión de daño y los tiempos de la animación.
¿Cómo crear una máquina de estados godot 4 desde cero?

Para implementar la arquitectura de forma limpia en Godot 4.7.2, utilizaremos la propia jerarquía de nodos del motor. La idea es tener un nodo administrador principal (StateMachine) y varios nodos hijos, donde cada nodo hijo representa un estado concreto heredado de una clase base (State).
Primero, crea el script base state.gd. Servirá como la interfaz abstracta para todos los estados del videojuego. Ninguna lógica específica de juego entra aquí, solo la definición de los métodos del ciclo de vida:
class_name State
extends Node
signal transitioned(state_script: State, new_state_name: String)
func enter() -> void:
pass
func exit() -> void:
pass
func update(_delta: float) -> void:
pass
func physics_update(_delta: float) -> void:
pass
func handle_input(_event: InputEvent) -> void:
pass
Observa que usamos la instrucción class_name State para que cualquier nodo en Godot pueda reconocer este tipo de dato. La señal transitioned será emitida por el estado individual cuando decida que es momento de cambiar a otro estado, pasando el nombre del estado de destino como parámetro.
A continuación, creamos el script del administrador principal, state_machine.gd. Este nodo gestiona qué estado está activo en el momento y redirige los eventos del motor (_process, _physics_process, _unhandled_input) únicamente al estado actual:
class_name StateMachine
extends Node
@export var initial_state: State
var current_state: State
var states: Dictionary = {}
func _ready() -> void:
await owner.ready
for child in get_children():
if child is State:
states[child.name.to_lower()] = child
child.transitioned.connect(_on_child_transitioned)
if initial_state:
initial_state.enter()
current_state = initial_state
func _process(delta: float) -> void:
if current_state:
current_state.update(delta)
func _physics_process(delta: float) -> void:
if current_state:
current_state.physics_update(delta)
func _unhandled_input(event: InputEvent) -> void:
if current_state:
current_state.handle_input(event)
func _on_child_transitioned(state: State, new_state_name: String) -> void:
if state != current_state:
return
var new_state: State = states.get(new_state_name.to_lower())
if not new_state:
push_error("Estado no encontrado: " + new_state_name)
return
if current_state:
current_state.exit()
new_state.enter()
current_state = new_state
Este enfoque garantiza un desacoplamiento total. El nodo StateMachine no necesita saber qué hace cada estado en detalle; solo administra quién tiene el control en cada fotograma.
¿Cómo estructurar los nodos y scripts en el árbol de la escena en Godot 4.7.2?
La organización dentro del editor de Godot 4 es simple y visual. Utilizaremos un nodo de personaje CharacterBody2D como ejemplo de entidad controlada.
La estructura de la escena del jugador en el panel de nodos debe organizarse así:
- Player (
CharacterBody2D) - CollisionShape2D
- Sprite2D
- AnimationPlayer
- StateMachine (
Node)- Idle (
Nodecon scriptplayer_idle_state.gd) - Move (
Nodecon scriptplayer_move_state.gd) - Jump (
Nodecon scriptplayer_jump_state.gd)
- Idle (
Ahora veamos cómo se escribe un estado concreto en GDScript. A continuación está el código para el estado player_idle_state.gd:
extends State
@export var player: CharacterBody2D
@export var animation_player: AnimationPlayer
func enter() -> void:
if animation_player:
animation_player.play("idle")
func physics_update(_delta: float) -> void:
if not player.is_on_floor():
transitioned.emit(self, "jump")
return
var input_dir := Input.get_axis("ui_left", "ui_right")
if input_dir != 0:
transitioned.emit(self, "move")
func handle_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept") and player.is_on_floor():
transitioned.emit(self, "jump")
Y aquí tenemos el estado de movimiento player_move_state.gd:
extends State
@export var player: CharacterBody2D
@export var animation_player: AnimationPlayer
@export var move_speed: float = 200.0
func enter() -> void:
if animation_player:
animation_player.play("walk")
func physics_update(delta: float) -> void:
if not player.is_on_floor():
transitioned.emit(self, "jump")
return
var input_dir := Input.get_axis("ui_left", "ui_right")
if input_dir == 0:
transitioned.emit(self, "idle")
return
player.velocity.x = input_dir * move_speed
player.move_and_slide()
Observa la elegancia de este enfoque. Cuando el jugador se mueve, el estado Move aplica la velocidad y ejecuta move_and_slide(). Cuando la entrada del teclado cesa, el propio estado emite la señal para cambiar a Idle. Cada archivo tiene menos de 40 líneas y hace exactamente una cosa.
¿Cómo gestionar transiciones de estado complejas y pasar datos entre estados?
A medida que la complejidad del videojuego aumenta, los estados simples de máquina finita pueden necesitar compartir contexto o realizar transiciones temporales. Existen tres técnicas fundamentales para abordar escenarios avanzados en videojuegos comerciales.
| Técnica | Aplicación ideal | Ventaja principal |
|---|---|---|
| FSM clásica | Movimiento básico (Caminar, Correr, Saltar) | Simplicidad y aislamiento directo de lógica |
| Máquina con pila (Pushdown Automaton) | Menús de pausa, efectos de daño (hitstun) | Permite regresar al estado anterior exacto sin perder contexto |
| Estados jerárquicos (HFSM) | Enemigos complejos y mecánicas de combate | Comparte lógica común (ej.: estar en el aire) entre múltiples subestados |
Para pasar datos específicos durante una transición —como la intensidad de un golpe recibido o el vector de empuje (knockback)—, puedes expandir el método enter de tu script base para recibir un diccionario opcional de argumentos:
# En el script base State.gd
func enter(_msg: Dictionary = {}) -> void:
pass
En el estado de daño (player_hurt_state.gd), el argumento se puede procesar en el momento en que el personaje entra al estado:
extends State
@export var player: CharacterBody2D
var knockback_vector: Vector2 = Vector2.ZERO
func enter(msg: Dictionary = {}) -> void:
if msg.has("knockback"):
knockback_vector = msg["knockback"]
player.velocity = knockback_vector
func physics_update(delta: float) -> void:
player.velocity = player.velocity.move_toward(Vector2.ZERO, 500 * delta)
player.move_and_slide()
if player.velocity.length() < 10.0:
transitioned.emit(self, "idle")
Al emitir la señal de transición dentro del administrador, basta con incluir el diccionario de parámetros adicionales. Esto evita el uso de variables globales estáticas para la comunicación de eventos puntuales.
¿Cómo depurar y probar transiciones de estado durante el gameplay?

Trabajar con máquinas de estado ofrece una gran ventaja para el diagnóstico de fallas, ya que siempre sabes exactamente qué estado está activo. Sin embargo, en videojuegos de acción rápida, las transiciones ocurren en fracciones de segundo.
Una excelente práctica durante el desarrollo es agregar un nodo de interfaz simple Label sobre la cabeza del personaje para mostrar el estado actual en tiempo real. Puedes conectar la actualización de este texto en el propio evento de transición de StateMachine:
# Dentro de state_machine.gd para fines de depuración
@export var debug_label: Label
func _on_child_transitioned(state: State, new_state_name: String) -> void:
# ... lógica de transición existente ...
if debug_label:
debug_label.text = new_state.name
Al probar el videojuego en la ventana de ejecución, verifica las siguientes trampas comunes:
- Bucle infinito de transición: ocurre cuando el estado A emite una transición hacia B dentro de
enter(), y B emite hacia A inmediatamente en el mismo fotograma. Agrega un registro de alerta en_on_child_transitionedpara rastrear cambios frecuentes. - Olvidar limpiar conexiones de señales: si tu estado se conecta a señales externas (como un temporizador de tiempo de recarga), recuerda desconectar esas señales dentro del método
exit(), garantizando que eventos antiguos no detonen reacciones cuando el estado ya no esté activo. - Nodos desactivados accidentalmente: asegúrate de que la propiedad
process_modede los nodos de la máquina de estados respete la pausa del videojuego cuando la escena se ponga en pausa.
Conclusión
Implementar una máquina de estados godot 4 modular y orientada a nodos es la elección más sólida para garantizar que tu proyecto de videojuego continúe organizado y libre de errores difíciles de rastrear. Al dividir responsabilidades en scripts ligeros heredados de la clase base State, creas un entorno donde agregar nuevas mecánicas, ajustar animaciones y refactorizar comportamientos se convierte en una tarea rápida y segura. Aplica este patrón en la arquitectura de tu próximo personaje o inteligencia artificial y experimenta la claridad de tener un código limpio en GDScript.