Ir al contenido

Máquina de Estados

ss-gameforge-state-machine provee un FSM ligero para gestionar estados de lógica de juego. Cada estado es un nodo separado con sus propios métodos de ciclo de vida, manteniendo el código organizado y fácil de extender.

  1. Descarga ss-gameforge-state-machine desde la página de releases.
  2. Copia addons/ss-gameforge-state-machine/ dentro de res://addons/.
  3. Activa ss-gameforge-state-machine en Project Settings → Plugins.
ClaseExtiendeDescripción
StateMachineNodeControlador del FSM — gestiona estados y transiciones.
StateBaseNodeClase base para cada estado. Sobreescribe los métodos de ciclo de vida aquí.
PropiedadTipoValor por defectoDescripción
default_stateStateBaseEstado activo al iniciar (_ready()).
enable_debug_loggingboolfalseMuestra las transiciones en el panel de salida.
current_stateStateBaseEstado activo actualmente (solo lectura).
previous_stateStateBaseÚltimo estado activo (solo lectura).
signal state_changed(from: String, to: String)
func change_to(new_state: String) -> void # Transiciona a un estado por nombre
func change_to_previous() -> void # Vuelve al estado anterior
func get_state_history() -> Array[String] # Últimos 10 nombres de estado (debug)
PropiedadTipoDescripción
controlled_nodeNodeEl nodo que controla este estado. Lo asigna StateMachine automáticamente.
state_machineStateMachineReferencia al FSM padre. Se asigna automáticamente.

Sobreescribe estos en tus clases de estado:

func start() -> void # Se llama al activar este estado
func end() -> void # Se llama al salir de este estado
func on_process(delta: float) -> void # Se llama cada frame
func on_physics_process(delta: float) -> void
func on_input(event: InputEvent) -> void
func on_unhandled_input(event: InputEvent) -> void
func on_unhandled_key_input(event: InputEvent) -> void
Player (CharacterBody2D)
└── StateMachine (StateMachine)
├── IdleState (script de StateBase)
├── RunState (script de StateBase)
└── JumpState (script de StateBase)

Asigna StateMachine.default_state al nodo IdleState en el inspector.

idle_state.gd
extends StateBase
class_name IdleState
func start() -> void:
controlled_node.velocity = Vector2.ZERO
func on_physics_process(delta: float) -> void:
if Input.is_action_pressed("ui_right") or Input.is_action_pressed("ui_left"):
state_machine.change_to("RunState")
if Input.is_action_just_pressed("ui_accept"):
state_machine.change_to("JumpState")
run_state.gd
extends StateBase
class_name RunState
func on_physics_process(delta: float) -> void:
var direction := Input.get_axis("ui_left", "ui_right")
controlled_node.velocity.x = direction * 200.0
controlled_node.move_and_slide()
if direction == 0:
state_machine.change_to("IdleState")
# En el nodo padre u otro script
func _ready() -> void:
$StateMachine.state_changed.connect(_on_state_changed)
func _on_state_changed(from: String, to: String) -> void:
print("Transición: %s%s" % [from, to])

El FSM funciona para cualquier objeto de juego con estados discretos:

  • Personajes — Idle / Correr / Saltar / Atacar / Muerto
  • Enemigos — Patrullar / Perseguir / Atacar / Aturdido
  • Flujos de UI — Menú / Cargando / Gameplay / Pausa / Game Over
  • Cinemáticas — Intro / Diálogo / Outro

Cuando un personaje tiene muchos estados que comparten lógica común — gravedad, lectura de input, verificaciones de transición — repetir ese código en cada estado se convierte en un problema de mantenimiento. La solución es una clase base intermedia que se sitúa entre StateBase y tus estados concretos.

Sin él, cada estado duplica las mismas verificaciones:

# Repetido en IdleState, RunState, CrouchState...
func on_physics_process(delta: float) -> void:
if not controlled_node.is_on_floor():
state_machine.change_to("FallState")
if Input.is_action_just_pressed("jump") and controlled_node.is_on_floor():
state_machine.change_to("JumpState")

Con una base intermedia, esa lógica vive en un solo lugar y todos los estados la heredan.

enemy_state_base.gd
class_name EnemyStateBase
extends StateBase
## Acceso tipado — evita castear controlled_node en cada estado
var enemy: Enemy:
set(value): controlled_node = value
get: return controlled_node as Enemy
## ── Helpers compartidos ─────────────────────────────
func apply_gravity() -> void:
if not enemy.is_on_floor():
enemy.velocity.y += enemy.stats.gravity
enemy.velocity.y = minf(enemy.velocity.y, enemy.stats.max_fall_speed)
func face_direction(dir: float) -> void:
if not is_zero_approx(dir):
enemy.sprite.scale.x = sign(dir)
func is_player_in_range() -> bool:
return enemy.detection_area.has_overlapping_bodies()
func get_player_direction() -> float:
var player := enemy.detection_area.get_overlapping_bodies().front() as Node2D
if player:
return sign(player.global_position.x - enemy.global_position.x)
return 0.0
func check_alert_transition() -> bool:
if is_player_in_range():
state_machine.change_to(enemy.states.CHASING)
return true
return false
func check_fall_transition() -> bool:
if not enemy.is_on_floor():
state_machine.change_to(enemy.states.FALLING)
return true
return false
enemy_patrol_state.gd
class_name EnemyPatrolState
extends EnemyStateBase
var _direction := 1.0
func start() -> void:
_direction = enemy.sprite.scale.x
func on_physics_process(delta: float) -> void:
apply_gravity()
if check_alert_transition(): return
if check_fall_transition(): return
# Invertir en los bordes
if enemy.edge_detector.is_colliding():
_direction *= -1.0
enemy.velocity.x = _direction * enemy.stats.patrol_speed
face_direction(_direction)
enemy.move_and_slide()
enemy_chase_state.gd
class_name EnemyChaseState
extends EnemyStateBase
func on_physics_process(delta: float) -> void:
apply_gravity()
if not is_player_in_range():
state_machine.change_to(enemy.states.PATROLLING)
return
if check_fall_transition(): return
var dir := get_player_direction()
enemy.velocity.x = dir * enemy.stats.chase_speed
face_direction(dir)
enemy.move_and_slide()
enemy_fall_state.gd
class_name EnemyFallState
extends EnemyStateBase
func on_physics_process(delta: float) -> void:
apply_gravity()
enemy.move_and_slide()
if enemy.is_on_floor():
state_machine.change_to(enemy.states.PATROLLING)
Enemy (CharacterBody2D)
├── Sprite2D
├── DetectionArea (Area2D)
├── EdgeDetector (RayCast2D)
└── StateMachine
├── PatrolState ← script EnemyPatrolState
├── ChaseState ← script EnemyChaseState
└── FallState ← script EnemyFallState
  • Una base intermedia por tipo de personaje. EnemyStateBase para enemigos, PlayerStateBase para el jugador, etc.
  • Los helpers devuelven bool para transiciones. Retornar temprano con if check_x(): return mantiene la lógica del estado lineal y fácil de leer.
  • El acceso tipado no cuesta nada. Reutiliza controlled_node — solo añade un getter tipado para comodidad.
  • Los estados concretos permanecen enfocados. Cada estado describe únicamente lo que lo hace diferente. La infraestructura compartida es invisible.