Hoy vamos a aprender sobre las Máquinas de Estado o “State Machines” en C# y Unity.
Las máquinas de estados son una herramienta fundamental en programación de videojuegos. Si alguna vez has querido que un enemigo tenga múltiples comportamientos (como patrullar, perseguir, atacar o morir), una State Machine es la solución más limpia y escalable.
En este tutorial aprenderás:
- Qué es una máquina de estados
- Por qué es útil en desarrollo de videojuegos
- Cómo implementarla desde cero en Unity
- Un ejemplo completo de enemigos con varios estados
Tutorial de Unity Nivel: Intermedio.
9.1 ¿Qué es una máquina de estados?
Una máquina de estados (Finite State Machine, FSM) es un patrón de diseño que permite que un objeto cambie su comportamiento según su estado actual. Piensa en un enemigo que “piensa diferente” cuando patrulla, ataca o muere, cada estado define su comportamiento. Por esto, una Máquina de Estados (Finite State Machine, FSM) es la mejor forma de estructurar este tipo de lógica, evitando if/else anidados y facilitando la ampliación.
Una FSM describe un sistema que puede encontrarse en uno de un conjunto finito de estados, y transicionar de uno a otro en respuesta a eventos o condiciones.
Estructura típica
- Un estado base (IState o State)
- Varias clases concretas que implementan los otros estados (PatrullarState, PerseguirState, etc.)
- Un StateMachine que controla el flujo/transición de estados (ejemplo, si el jugador está cerca, pasar de Patrullar a Perseguir).
- Un contexto, como el enemigo, que usa la máquina de estados
¿Por qué usar FSM en Unity?
- Claridad: cada estado tiene su propia clase, lógica y responsabilidades.
- Escalabilidad: añadir nuevos estados no rompe el código existente.
- Mantenibilidad: depurar y comprender comportamientos es más sencillo.
- Reutilización: la misma FSM genérica puede servir para distintos tipos de IA.
9.2 Implementando una Máquina de Estados (FSM)
Primero, vamos a organizar nuestro código así:
Assets/ └── Scripts/ ├── FSM/ │ ├── IState.cs │ └── StateMachine.cs └── Enemigo/ ├── Enemigo.cs └── States/ ├── PatrullarState.cs ├── PerseguirState.cs ├── AtacarState.cs └── MorirState.cs
Interfaz de estado: IState.cs
Define la API mínima que debe implementar cada estado:
public interface IState { void Enter(); // Al entrar en el estado void Execute(); // Lógica continua llamada desde Update() void Exit(); // Al salir del estado }
Clase máquina de estados: StateMachine.cs
Gestiona el estado activo y permite cambiar de uno a otro:
public class StateMachine { private IState currentState; public void Initialize(IState startingState) { currentState = startingState; currentState.Enter(); } public void ChangeState(IState newState) { currentState.Exit(); currentState = newState; currentState.Enter(); } public void Update() { currentState?.Execute(); } }
Contexto: la clase Enemigo
using UnityEngine; public class Enemigo : MonoBehaviour { [Header("Referencias")] public Transform jugador; public float velocidad = 2f; public float rangoPersecucion = 5f; public float rangoAtaque = 2f; public int vida = 50; private StateMachine fsm; void Start() { fsm = new StateMachine(); // Iniciamos en Patrullar fsm.Initialize(new PatrullarState(this, fsm)); } void Update() { fsm.Update(); } public void RecibirDaño(int d) { vida -= d; if (vida <= 0) fsm.ChangeState(new MorirState(this, fsm)); } }
- Creamos la instancia de StateMachine en Start().
- Inicializamos con el estado Patrullar.
- Cada Update() delega en fsm.Update().
- RecibirDaño() fuerza la transición a Morir cuando la vida llega a cero.
Estado Patrullar: PatrullarState.cs
using UnityEngine; public class PatrullarState : IState { private readonly Enemigo enemigo; private readonly StateMachine fsm; private Vector3 puntoDestino; public PatrullarState(Enemigo enemigo, StateMachine fsm) { this.enemigo = enemigo; this.fsm = fsm; } public void Enter() { // Elegir un punto aleatorio cercano puntoDestino = enemigo.transform.position + Random.insideUnitSphere * 4f; puntoDestino.y = enemigo.transform.position.y; Debug.Log("Estado: Patrullar"); } public void Execute() { // Moverse hacia el punto enemigo.transform.position = Vector3.MoveTowards( enemigo.transform.position, puntoDestino, enemigo.velocidad * Time.deltaTime); // Si llegó, escoger nuevo destino if (Vector3.Distance(enemigo.transform.position, puntoDestino) < 0.1f) Enter(); // re-selecciona destino // Si el jugador está en rango de persecución, cambiar de estado float dist = Vector3.Distance( enemigo.transform.position, enemigo.jugador.position); if (dist <= enemigo.rangoPersecucion) fsm.ChangeState(new PerseguirState(enemigo, fsm)); } public void Exit() { Debug.Log("Saliendo de Patrullar"); } }
Estado Perseguir: PerseguirState.cs
using UnityEngine; public class PerseguirState : IState { private readonly Enemigo enemigo; private readonly StateMachine fsm; public PerseguirState(Enemigo enemigo, StateMachine fsm) { this.enemigo = enemigo; this.fsm = fsm; } public void Enter() { Debug.Log("Estado: Perseguir"); } public void Execute() { // Ir hacia el jugador enemigo.transform.position = Vector3.MoveTowards( enemigo.transform.position, enemigo.jugador.position, enemigo.velocidad * Time.deltaTime); float dist = Vector3.Distance( enemigo.transform.position, enemigo.jugador.position); // Si está en rango de ataque, cambiar a Atacar if (dist <= enemigo.rangoAtaque) fsm.ChangeState(new AtacarState(enemigo, fsm)); // Si el jugador huye más allá de rango de persecución, volver a Patrullar else if (dist > enemigo.rangoPersecucion) fsm.ChangeState(new PatrullarState(enemigo, fsm)); } public void Exit() { Debug.Log("Saliendo de Perseguir"); } }
Estado Atacar: AtacarState.cs
using UnityEngine; public class AtacarState : IState { private readonly Enemigo enemigo; private readonly StateMachine fsm; private float timer; private readonly float tiempoEntreAtaques = 1.2f; public AtacarState(Enemigo enemigo, StateMachine fsm) { this.enemigo = enemigo; this.fsm = fsm; } public void Enter() { timer = 0f; Debug.Log("Estado: Atacar"); } public void Execute() { timer += Time.deltaTime; if (timer >= tiempoEntreAtaques) { Disparar(); timer = 0f; } float dist = Vector3.Distance( enemigo.transform.position, enemigo.jugador.position); if (dist > enemigo.rangoAtaque) fsm.ChangeState(new PerseguirState(enemigo, fsm)); } private void Disparar() { Debug.Log("¡Enemigo ataca al jugador!"); // Se podría invocar un método jugador.RecibirDaño(...) } public void Exit() { Debug.Log("Saliendo de Atacar"); } }
Estado Morir: MorirState.cs
using UnityEngine; public class MorirState : IState { private readonly Enemigo enemigo; private readonly StateMachine fsm; public MorirState(Enemigo enemigo, StateMachine fsm) { this.enemigo = enemigo; this.fsm = fsm; } public void Enter() { Debug.Log("Estado: Morir"); // Aquí podrías reproducir animación de muerte Object.Destroy(enemigo.gameObject, 1f); // Destruye tras 1s } public void Execute() { /* No hay lógica continua */ } public void Exit() { /* Nunca saldrá de morir */ } }
Para hacerlo funcionar:
- Crea un prefab con el script Enemigo y un modelo sencillo (por ejemplo, una esfera).
- Coloca un objeto “Jugador” (otro GameObject) en la escena y asígnalo al campo jugador.
- Ajusta parámetros (velocidad, rangos, vida) en el Inspector.
- Ejecuta la escena y observa la consola para verificar la secuencia de estados.
- Para visualización avanzada, podrías dibujar gizmos o usar texto sobre el enemigo para mostrar el estado activo en pantalla.
9.3 Conclusión
Con una Máquina de Estados bien diseñada, tu IA será:
- Modular: cada comportamiento reside en su propia clase.
- Escalable: añadir nuevos estados no requiere tocar código existente.
- Legible: la lógica de transición es explícita y clara.
Este patrón es la base de arquitecturas más complejas (Behaviour Trees, Utility AI), ¡pero dominar la FSM es un paso esencial hacia enemigos realmente inteligentes en tus juegos Unity!
Extensiones y buenas prácticas
- Eventos: dispara un OnStateChanged para que otros sistemas (UI, audio) reaccionen.
- Animaciones: integra Animator en Enter() y espera triggers antes de Execute().
- Máquinas jerárquicas: FSM dentro de FSM para subestados (por ejemplo, Patrullar podría tener Iddle y Caminar).
- Data-Driven: carga parámetros de estados (velocidades, tiempos) desde ScriptableObjects.
- Testing: inyecta mocks de contexto para probar estados en aislamiento.
Ejercicios.
Para reforzar lo aprendido, es necesario practicarlo, por ello intenta realizar los siguientes ejercicios:
- Siguiendo los ejemplos de este tutorial, crea tu propia FSM para practicar y puedas entender claramente como funcionan.
Este Tutorial de Unity termina aquí. Acompáñanos en el siguiente tutorial donde veremos “Gestionando nuestro Código”.
Siguiente Tutorial de Unity: “10. Gestionando nuestro Código“
Unity Tutorial: “C# en Unity 2“
1. Los Métodos Clave de Unity
2. Herencia
3. Encapsulamiento y Constructores
4. Corrutinas en Unity
5. Delegados y Eventos
6. GameManager y Control de Flujo
7. ScriptableObjects en Unity
8. El Patrón Factory
9. Máquinas de Estado en Unity
10. Gestionando nuestro Código
Ver más Tutoriales