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.
Instalación
Sección titulada «Instalación»- Descarga
ss-gameforge-state-machinedesde la página de releases. - Copia
addons/ss-gameforge-state-machine/dentro deres://addons/. - Activa ss-gameforge-state-machine en Project Settings → Plugins.
Clases principales
Sección titulada «Clases principales»| Clase | Extiende | Descripción |
|---|---|---|
StateMachine | Node | Controlador del FSM — gestiona estados y transiciones. |
StateBase | Node | Clase base para cada estado. Sobreescribe los métodos de ciclo de vida aquí. |
StateMachine
Sección titulada «StateMachine»Propiedades
Sección titulada «Propiedades»| Propiedad | Tipo | Valor por defecto | Descripción |
|---|---|---|---|
default_state | StateBase | — | Estado activo al iniciar (_ready()). |
enable_debug_logging | bool | false | Muestra las transiciones en el panel de salida. |
current_state | StateBase | — | Estado activo actualmente (solo lectura). |
previous_state | StateBase | — | Último estado activo (solo lectura). |
Señales
Sección titulada «Señales»signal state_changed(from: String, to: String)Métodos
Sección titulada «Métodos»func change_to(new_state: String) -> void # Transiciona a un estado por nombrefunc change_to_previous() -> void # Vuelve al estado anteriorfunc get_state_history() -> Array[String] # Últimos 10 nombres de estado (debug)StateBase
Sección titulada «StateBase»Propiedades
Sección titulada «Propiedades»| Propiedad | Tipo | Descripción |
|---|---|---|
controlled_node | Node | El nodo que controla este estado. Lo asigna StateMachine automáticamente. |
state_machine | StateMachine | Referencia al FSM padre. Se asigna automáticamente. |
Métodos de ciclo de vida
Sección titulada «Métodos de ciclo de vida»Sobreescribe estos en tus clases de estado:
func start() -> void # Se llama al activar este estadofunc end() -> void # Se llama al salir de este estadofunc on_process(delta: float) -> void # Se llama cada framefunc on_physics_process(delta: float) -> voidfunc on_input(event: InputEvent) -> voidfunc on_unhandled_input(event: InputEvent) -> voidfunc on_unhandled_key_input(event: InputEvent) -> voidInicio rápido
Sección titulada «Inicio rápido»Estructura de escena
Sección titulada «Estructura de escena»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.
Scripts de estado
Sección titulada «Scripts de estado»extends StateBaseclass_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")extends StateBaseclass_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")Escuchar transiciones
Sección titulada «Escuchar transiciones»# En el nodo padre u otro scriptfunc _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])Casos de uso
Sección titulada «Casos de uso»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
Patrón: clase base de estado compartida
Sección titulada «Patrón: clase base de estado compartida»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.
Por qué este patrón
Sección titulada «Por qué este patrón»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.
Ejemplo: EnemyStateBase
Sección titulada «Ejemplo: EnemyStateBase»class_name EnemyStateBaseextends StateBase
## Acceso tipado — evita castear controlled_node en cada estadovar 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 falseEstados concretos usando la base compartida
Sección titulada «Estados concretos usando la base compartida»class_name EnemyPatrolStateextends 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()class_name EnemyChaseStateextends 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()class_name EnemyFallStateextends 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)Estructura de escena
Sección titulada «Estructura de escena»Enemy (CharacterBody2D)├── Sprite2D├── DetectionArea (Area2D)├── EdgeDetector (RayCast2D)└── StateMachine ├── PatrolState ← script EnemyPatrolState ├── ChaseState ← script EnemyChaseState └── FallState ← script EnemyFallStatePuntos clave
Sección titulada «Puntos clave»- Una base intermedia por tipo de personaje.
EnemyStateBasepara enemigos,PlayerStateBasepara el jugador, etc. - Los helpers devuelven
boolpara transiciones. Retornar temprano conif check_x(): returnmantiene 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.