Mi visión sobre la gestión de estados de acción en el desarrollo de videojuegos

20 de marzo de 2026
Daniel LuIngeniero Full-Stack | Creador de Contenido

En este artículo, a raíz de los errores comunes de sobrescritura y recuperación fallida de estados de acción, y basándome en mi experiencia en un equipo que está empezando, comparto cómo construir una máquina de estados de acción de alta extensibilidad y bajo acoplamiento en Unity mediante un Pool de Estados Activos y evaluación dinámica de prioridad.

CategoríasTecnologíaUnityDesarrollo Web

El problema: El bug de la superposición de estados de acción

Hace poco me uní a un equipo que está dando sus primeros pasos. Al ayudarles a organizar el sistema de combate y la lógica de representación, me encontré con un bug clásico y muy molesto.

El escenario era el siguiente: El personaje recibe un Debuff de "Congelación" que dura 5 segundos; en ese momento, la animación cambia a [Congelado] (el personaje se queda quieto temblando). En el segundo 2, el personaje recibe un golpe fuerte de un enemigo con efecto de aturdimiento, lo que activa una animación de [Aturdido] de 2 segundos (el personaje se lleva las manos a la cabeza mareado).

El verdadero problema ocurre cuando termina la animación de aturdimiento: Cuando acaban los 2 segundos de aturdimiento, según la lógica interna, al personaje todavía le queda 1 segundo de Debuff de "Congelación". Sin embargo, en lugar de volver a la animación de [Congelado], el personaje vuelve directamente al estado [Idle] predeterminado. Esto provoca que el jugador vea al personaje respirando normalmente en espera, pero no pueda moverlo (porque la lógica sigue "congelada"), creando una desconexión total entre lo que se ve y lo que ocurre.

Al revisar el código, vi que la razón era simple: el sistema anterior era basado en disparadores (triggers). Cuando se activaba el aturdimiento, se obligaba al Animator a reproducirlo; cuando terminaba, se le ordenaba volver al estado predeterminado de forma brusca, sin tener en cuenta otros efectos lógicos activos.

La ubicuidad del problema y las limitaciones de las soluciones tradicionales

Personalmente, creo que este problema es extremadamente común en el desarrollo de juegos. Siempre que haya sistemas complejos de Buffs o efectos de habilidades superpuestos, surgirá el reto de cómo gestionar la prioridad y la recuperación de las animaciones.

Muchos equipos, al principio y para ver resultados rápido, suelen adoptar dos prácticas que conllevan grandes riesgos y que yo suelo desaconsejar:

Riesgo A: Manipulación directa de variables del Animator

Cada sistema (Buffs, habilidades, daños) llama directamente a animator.SetBool("isFrozen", true). A medida que el sistema crece, es imposible saber qué parte del código puso el booleano en false y cuándo, lo que genera un caos en la máquina de estados difícil de depurar.

Riesgo B: Abuso de la red de transiciones de Unity Mecanim

La ventana del Animator es intuitiva cuando hay pocas líneas. Pero si vinculas la lógica de negocio a las transiciones —configurando decenas de condiciones como Speed > 0, isFrozen == true, isStunned == false—, el Animator acaba pareciendo una "telaraña". Añadir un solo estado nuevo requiere conectar infinitas líneas nuevas, y el coste de mantenimiento sube de forma exponencial.

Mi enfoque: Pool de estados activos y evaluación dinámica de prioridad

Para solucionar el solapamiento de estados y el desacoplamiento, introduje en el equipo una idea: el Pool de Estados Activos (Active State Pool) con evaluación dinámica de prioridad.

La idea central es una: La petición del estado debe estar totalmente separada de la ejecución del estado.

La capa de lógica (Buffs o sistemas de habilidades) solo se encarga de Registrar y Eliminar el estado de acción que desea. Un AnimationStateManager centralizado decide, en cada frame, qué orden de animación enviar al Animator.

Diagrama de flujo del mecanismo principal:

Loading diagram...

Bajo esta arquitectura, cuando el [Aturdido] de prioridad 100 termina y se elimina del pool, el administrador vuelve a evaluar y encuentra que la mayor prioridad restante es la de [Congelado] (prioridad 50). Entonces, ordena automáticamente al Animator volver a esa animación. El bug se resuelve de forma natural.

Práctica en Unity: Dejar que el Animator sea puro

Con este sistema, la forma de usar el Animator en Unity cambia. Podemos olvidarnos de las redes complejas de líneas y condiciones. El Animator pasa a ser un simple reproductor de acciones.

En el Animator Controller, solo tienes que arrastrar todos los estados (States) que necesites sin conectarlos con líneas (o conectarlos todos desde AnyState sin condiciones). Desde el código, controlas el cambio de estado directamente con animator.CrossFade() o animator.Play().

Ejemplo de pseudocódigo del administrador central:

// 1. Estructura de datos de la petición de estado
public class AnimStateRequest
{
    public string StateName; // Nombre en el Animator
    public int Priority;     // Prioridad
    public object Source;    // Origen (para validar al eliminar, ej: instancia del Buff)
}

// 2. Administrador centralizado
public class AnimationStateManager : MonoBehaviour
{
    private Animator _animator;
    private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
    private string _currentPlayingState;

    // Registrar estado
    public void AddStateRequest(string stateName, int priority, object source)
    {
        _activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
        EvaluateHighestPriorityState();
    }

    // Eliminar registro
    public void RemoveStateRequest(object source)
    {
        _activeStates.RemoveAll(req => req.Source == source);
        EvaluateHighestPriorityState();
    }

    // Evaluación dinámica del estado con mayor prioridad
    private void EvaluateHighestPriorityState()
    {
        if (_activeStates.Count == 0)
        {
            PlayAnimation("Idle"); // Estado por defecto
            return;
        }

        // Ordenar para encontrar la mayor prioridad
        _activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
        string targetState = _activeStates[0].StateName;

        if (_currentPlayingState != targetState)
        {
            PlayAnimation(targetState);
        }
    }

    // Control del Animator de Unity
    private void PlayAnimation(string stateName)
    {
        _currentPlayingState = stateName;
        // CrossFade para una transición suave de 0.1s
        _animator.CrossFade(stateName, 0.1f); 
    }
}

Evolución de la arquitectura: Mis ideas para sistemas complejos

Este sistema de Pool activo + Prioridad ofrece una gran escalabilidad. He considerado algunas extensiones interesantes para el futuro:

  1. Gestión de prioridades idénticas: Si hay dos peticiones con prioridad 50, se puede ampliar la lógica para que prime la última en llegar o la que tenga más duración restante.
  2. Máscaras de avatar (Avatar Mask): En juegos 3D, a veces necesitas que el tren superior dispare mientras el inferior corre o recibe un golpe. Se pueden crear dos managers independientes (UpperBodyPool y LowerBodyPool) y pasar diferentes índices de capa al CrossFade.
  3. Uso de Playable API: Para habilidades con tiempos muy estrictos, el Animator puede tener latencia. En esos casos, este manager se puede integrar con el Playable Graph de Unity para mezclar AnimationClips directamente por código.

Estas son mis reflexiones y conclusiones sobre la gestión de estados de acción basadas en mi experiencia reciente. Esta solución resolvió los problemas de nuestro equipo, pero no es necesariamente una "bala de plata"; debe ajustarse según el tipo de juego. Mi visión es limitada, así que si tenéis mejores soluciones o detectáis fallos en mi lógica, estaré encantado de debatir. Por favor, corregidme si me equivoco.


Artículo original del equipo iknowabit. Soporte técnico: Exploración de patrones de arquitectura avanzada en C# y motor Unity.